Skip to content

Getting started#

Generating new ULIDs#

Here is an example of generating a ULID. Generating ULIDs require a ULIDGen which is a ZLayer. You can create a ULIDGen using ULIDGen.live.

import com.bilalfazlani.zioUlid._

val program = for {
  // make a new ULID
  ulid <- ULID.nextULID

  // .toString gives a string representation of the ULID
  _ <- Console.printLine(ulid.toString)
} yield ()

val run = program.provide(ULIDGen.live)

.toString gives us the string representation of ULID

Warning

If system datetime is set to more than August 10,889, then fiber will die with UnsupportedSystemDateTime exception

Important

Since ULIDGen is a stateful layer, you should have only one instance of ULIDGen in your application. You can ensure that by using ULIDGen.live only once in your application, ideally at at the start/root of your application.

Timestamp#

To know when a ULID was generated, we can get the timestamp from a ULID. Its a Long value representing Unix epoch milliseconds.

// get the timestamp of a ULID
val timestampMillis: Long = ulid.timestamp

// convert the millis to a isntant or datetime
val datetime = ZonedDateTime.ofInstant(
  Instant.ofEpochMilli(timestampMillis),
  ZoneId.of("Z")
)

Comparing and sorting ULIDs#

Since ULIDs are lexicographically sortable, we can compare them with each other and sort them.

import com.bilalfazlani.zioUlid.ULID

val program =
  for {
    // make new ULIDs
    ulid1 <- ULID.nextULID
    ulid2 <- ULID.nextULID

    // compare two ULIDs
    equals = ulid1 == ulid2
    lessThan = ulid1 < ulid2
    greaterThan = ulid1 > ulid2

    // sort ULIDs
    sortedUlids = List(ulid2, ulid1).sorted
  } yield ()

Binary encoding#

There are two binary representations of ULIDs. One is a Chunk[Byte] of size 16 and the other a tuple (pair) of two 64 bit Long values.

import com.bilalfazlani.zioUlid.ULID

val program = for {
  // make a new ULID
  ulid <- ULID.nextULID

  // get a bytes representation of a ULID
  bytes: Chunk[Byte] = ulid.bytes

  // get a tuple (high, low) representation of a ULID
  tuple: (Long, Long) = ulid.tuple
} yield ()

Converting a Tuple[Long, Long] to ULID#

You can easily convert a tuple of two Longs into a ULID

// decode a ULID from a tuple
val ulid: ULID = ULID(123412312L, 2134234423L)

This is a direct operation and does not need any validations as any two Longs are a valid ULID

Parsing a String#

Parsing a string into a ULID returns Either[ULIDStringParsingError, ULID]. It can fail with following errors

  • ULIDStringParsingError.InvalidLength when string length is not 26
  • ULIDStringParsingError.InvalidCharacters when string contains characters which are not supported by encoding
  • ULIDStringParsingError.OverflowValue when string contains a value which is greater than 128 bits
import com.bilalfazlani.zioUlid.{ULID, ULIDStringParsingError}
import ULIDStringParsingError._

private def getInputString: String = ???

// decode a ULID from a string
val ulid: Either[ULIDStringParsingError, ULID] = ULID(getInputString)

ulid.fold(
  {
    case InvalidCharacters(_) =>
      println("Invalid characters in the string")
    case InvalidLength(_) =>
      println(s"Invalid length of string")
    case OverflowValue(string) =>
      println(s"Overflow value $string")
  },
  ulid => println(ulid.toString)
)

It is also possible to validate a string using pattern matching

// validate a string as a ULID using pattern matching
getInputString match {
  case ULID(string)  => println(s"Valid ULID: $string")
  case invalidString => println(s"Invalid ULID: $invalidString")
}

Parsing chunk of bytes#

When parsing bytes into a ULID, validation of chunk size is performed. If size is not 16, then Left[ULIDBytesParsingError.InvalidBytesLength] is returned.

import com.bilalfazlani.zioUlid._

private def getInputBytes: Chunk[Byte] = ???

// decode a ULID from bytes
val ulid: Either[ULIDBytesParsingError.InvalidBytesLength, ULID] = ULID(
  getInputBytes
)

Similar to validation of Strings, its also possible to validate bytes using pattern matching

// validate bytes as a ULID using pattern matching
getInputBytes match {
  case validBytes @ ULID(string) => println("Valid ULID: " + string)
  case invalidBytes              => println("bytes are invalid")
}

Parsing Chunk[Byte] with separate timestamp#

You can create a ULID from a timestamp (Long) and a Chunk[Byte]. Chunk should be of size 10. Timestamp should be <= 48 bits.

import com.bilalfazlani.zioUlid._

// create a ULID from a timestamp and bytes
val ulid: Either[ULIDBytesParsingError, ULID] =
  ULID(getTimestamp, getRandomBytes)

This will return an Either[ULIDBytesParsingError, ULID] with following errors

  • ULIDBytesParsingError.InvalidBytesLength when chunk size is not 10
  • ULIDBytesParsingError.InvalidTimestamp when timestamp is greater than 48 bits