Skip to content

Utilities

Human-readable value types and ID generators for common programming tasks.

UUIDv7

Time-ordered UUIDs (RFC 9562) — the recommended ID format for internal use. UUIDv7 embeds a millisecond-precision Unix timestamp, is monotonically sortable, and can be compactly encoded with Base62.

scala
import wvlet.uni.util.UUIDv7

// Generate a new UUIDv7
val id = UUIDv7.newUUIDv7()
println(id)  // e.g., "019078e5-3f6a-7b1c-8d4e-2f9a0b1c3d5e"

// Compact 22-character Base62 representation (URL-safe, sort-order preserving)
val short: String = id.toBase62  // e.g., "0K9mFb3xYz1Qw5Rv8NpJ2a"

// Parse back from Base62
val recovered = UUIDv7.fromBase62(short)

Timestamps and Conversion

scala
val id = UUIDv7.newUUIDv7()

// Extract timestamp
val millis: Long = id.timestamp

// Convert to/from java.util.UUID
val uuid: java.util.UUID = id.toUUID
val back: UUIDv7 = UUIDv7.fromUUID(uuid)

// Binary representation
val bytes: Array[Byte] = UUIDv7.toBytes(id)   // 16 bytes
val fromBytes: UUIDv7 = UUIDv7.fromBytes(bytes)

// Parse from standard UUID string
val parsed = UUIDv7.fromString("019078e5-3f6a-7b1c-8d4e-2f9a0b1c3d5e")

Custom Generator

scala
// Create an independent generator (useful for testing or isolated contexts)
val gen = UUIDv7.createGenerator()
val id = gen.newUUIDv7()

// With a custom random source
val gen2 = UUIDv7.createGenerator(new scala.util.Random(42))

Base62

Sort-order preserving encoding using the 0-9A-Za-z alphabet (62 characters). Encodes 128-bit values to compact 22-character URL-safe strings. Use UUIDv7.toBase62 / UUIDv7.fromBase62 for the common case.

scala
import wvlet.uni.util.{Base62, UUIDv7}

// Most common usage: via UUIDv7
val id = UUIDv7.newUUIDv7()
val encoded: String = id.toBase62                           // 22 chars
val decoded: UUIDv7 = UUIDv7.fromBase62(encoded)

// Encode / decode raw 128-bit values
val raw: String = Base62.encode128bits(0x0123456789abcdefL, 0xfedcba9876543210L)
val (hi, low): (Long, Long) = Base62.decode128bits(raw)

// Validate
Base62.isValid("0K9mFb3xYz1Qw5Rv8NpJ2a")  // true

CrockfordBase32

Crockford's Base32 encoding — the case-insensitive, human-friendly alphabet (it excludes I, L, O, U to avoid ambiguity) that backs ULID. Use it directly when you need to encode raw 128-bit or 48-bit values in the same format.

scala
import wvlet.uni.util.CrockfordBase32

// Encode / decode a 128-bit value (26 characters)
val s: String = CrockfordBase32.encode128bits(hi = 0x0123456789abcdefL, low = 0xfedcba9876543210L)
val (hi, low): (Long, Long) = CrockfordBase32.decode128bits(s)

// Decode a 48-bit value (e.g. a ULID timestamp prefix)
val millis: Long = CrockfordBase32.decode48bits("01ARZ3NDEK")

// Validate
CrockfordBase32.isValidBase32("01arz3ndektsv4rrffq69g5fav")  // true

For ID generation prefer ULID (which encodes with this alphabet) or Base62; reach for CrockfordBase32 only when you need the raw codec.

NanoId

Compact, URL-safe random IDs for cases where time-ordering is not needed. Useful as user-facing tokens, short links, or external keys that map back to an internal UUIDv7.

scala
import wvlet.uni.util.NanoId

// Generate a default NanoId (21 chars, A-Za-z0-9_-)
val id: String = NanoId.generate()  // e.g., "V1StGXR8_Z5jdHi6B-myT"

// Custom size
val short = NanoId.generate(10)

// Custom alphabet and size
val hex = NanoId.generate("0123456789abcdef", 32)

ID Strategy

Use UUIDv7 as the internal primary key — it is time-ordered, monotonic, and embeds a timestamp. For user-facing URLs and APIs, either:

  • Expose the UUIDv7 in Base62 format (22 chars, sortable, reversible), or
  • Generate a NanoId as an external key and maintain a mapping to the internal UUIDv7.

ULID

Universally Unique Lexicographically Sortable Identifiers — 128-bit IDs encoded as 26-character Crockford Base32 strings. ULID predates UUIDv7 and serves a similar purpose; prefer UUIDv7 for new projects.

scala
import wvlet.uni.util.ULID

// Generate a new ULID
val id = ULID.newULID
println(id)  // e.g., "01arz3ndektsv4rrffq69g5fav"
ULID API details

Parsing and Validation

scala
val parsed = ULID.fromString("01arz3ndektsv4rrffq69g5fav")
ULID.isValid("01arz3ndektsv4rrffq69g5fav")  // true

Extracting Timestamps

scala
val id = ULID.newULID
val millis: Long = id.epochMillis
val instant: java.time.Instant = id.toInstant
val historical = ULID.ofMillis(1609459200000L)

Converting to/from UUID

scala
import java.util.UUID

val ulid = ULID.newULID
val uuid: UUID = ulid.toUUID
val back: ULID = ULID.fromUUID(uuid)
val bytes: Array[Byte] = ulid.toBytes  // 16 bytes
val fromBytes: ULID = ULID.fromBytes(bytes)

PrefixedULID

A ULID tagged with a human-readable type prefix — handy for self-describing IDs like user:01arz3ndektsv4rrffq69g5fav where the prefix identifies the entity. The string form is <prefix>:<ulid> (delimiter :), and values sort by prefix first, then by the embedded ULID timestamp.

scala
import wvlet.uni.util.PrefixedULID

// Generate
val id: PrefixedULID = PrefixedULID.newPrefixedULID("user")
println(id)                    // "user:01arz3ndektsv4rrffq69g5fav"

// Or directly as a string
val s: String = PrefixedULID.newPrefixedULIDString("order")

// Access parts
id.prefix                      // "user"
id.ulid                        // the underlying ULID

// Parse back (splits on the last ':' so prefixes may contain ':')
val parsed = PrefixedULID.fromString("user:01arz3ndektsv4rrffq69g5fav")

DataSize

Human-readable data size representation with parsing and unit conversion.

scala
import wvlet.uni.util.DataSize

// Parse from string
val size = DataSize("128MB")

// Create from bytes
val fromBytes = DataSize(1073741824L)  // 1GB

// Auto-select best unit
val succinct = DataSize.succinct(1536000L)
println(succinct)  // "1.46MB"

Unit Conversion

scala
val size = DataSize("2.5GB")

size.toBytes                              // 2684354560L
size.valueOf(DataSizeUnit.MEGABYTE)       // 2560.0
size.convertTo(DataSizeUnit.MEGABYTE)     // DataSize(2560.0, MEGABYTE)
size.mostSuccinctDataSize                 // DataSize(2.5, GIGABYTE)

Units

UnitSymbolBytes
BYTEB1
KILOBYTEkB1,024
MEGABYTEMB1,048,576
GIGABYTEGB1,073,741,824
TERABYTETB1,099,511,627,776
PETABYTEPB1,125,899,906,842,624

Count

Human-readable large number representation.

scala
import wvlet.uni.util.Count

// Parse from string
val count = Count("1.5M")

// Auto-select best unit
val succinct = Count.succinct(1500000L)
println(succinct)  // "1.50M"

// Raw value
count.toLong  // 1500000L

Unit Conversion

scala
val count = Count(2500000L)

count.mostSuccinctCount                // Count(2.50, MILLION)
count.convertTo(CountUnit.THOUSAND)    // Count(2500, THOUSAND)
count.valueOf(CountUnit.MILLION)       // 2.5

Units

UnitSymbolValue
ONE(none)1
THOUSANDK1,000
MILLIONM1,000,000
BILLIONB1,000,000,000
TRILLIONT1,000,000,000,000
QUADRILLIONQ1,000,000,000,000,000

ElapsedTime

Human-readable duration representation with unit conversion.

scala
import wvlet.uni.util.ElapsedTime

// Parse from string
val duration = ElapsedTime("5ms")
val timeout  = ElapsedTime("30s")
val window   = ElapsedTime("2.5h")

// Create from measurements
val fromNanos  = ElapsedTime.succinctNanos(1500000L)
println(fromNanos)   // "1.50ms"

val fromMillis = ElapsedTime.succinctMillis(3600000L)
println(fromMillis)  // "1.00h"

// Measure elapsed time
val start = System.nanoTime()
// ... do work ...
val elapsed = ElapsedTime.nanosSince(start)
println(elapsed)  // e.g., "42.30ms"

Unit Conversion

scala
val duration = ElapsedTime("90s")

duration.toMillis                                       // 90000.0
duration.valueIn(java.util.concurrent.TimeUnit.MINUTES) // 1.5
duration.convertToMostSuccinctTimeUnit                  // ElapsedTime(1.50, MINUTES)

Units

UnitSymbolExample
NANOSECONDSns"500ns"
MICROSECONDSus"100us"
MILLISECONDSms"5ms"
SECONDSs"30s"
MINUTESm"5m"
HOURSh"2.5h"
DAYSd"1d"

StopWatch

A simple elapsed-time timer. It starts running on creation and reports seconds as a Double.

scala
import wvlet.uni.util.StopWatch

val sw = StopWatch()
// ... do work ...
val seconds: Double = sw.getElapsedTime   // elapsed since creation/reset

sw.stop                                   // pause; returns the last interval
sw.resume                                 // continue measuring
sw.reset                                  // restart from zero
println(sw.reportElapsedTime)             // e.g. "0.42 sec."

Timer

For micro-benchmarking, extend the Timer trait and wrap code in time / nested block. A TimeReport captures total and per-block timing, and repeat runs blocks multiple times to smooth out JIT/GC noise.

scala
import wvlet.uni.util.Timer

class Benchmark extends Timer:
  val report = time("search", repeat = 10) {
    block("linear") {
      // candidate A
    }
    block("binary") {
      // candidate B
    }
  }

  // Inspect per-block statistics
  report("linear").average   // average seconds over the repeats
  report("binary").min       // min / max / median / elapsedSeconds also available

val b = Benchmark()
println(b.report)            // formatted multi-line timing report

Best Practices

  1. Use UUIDv7 for internal IDs — time-ordered, monotonic, with an embedded timestamp
  2. Use Base62 encoding for user-facing UUIDv7 — compact (22 chars), URL-safe, sort-order preserving
  3. Use NanoId for opaque external tokens — map them back to internal UUIDv7 when needed
  4. Use succinct formatting in logs and UIs for readability (DataSize.succinct, Count.succinct)
  5. Parse user input with the string constructors — they handle common formats ("128MB", "1.5M", "30s")
  6. All value types implement Comparable for sorting and comparison

Released under the Apache 2.0 License.