128BITmoonuuididentifier notes
MoonBit · RFC 9562

field notes · eleven sections

A short guide

From the first parse to a generator over your own clock, one runnable block at a time. Every output below is copied from a real run.

01Install

Add the package, then import it where you use it. The module is moonbitstack/moonuuid, and the import brings it in as @moonuuid.

shell
moon add moonbitstack/moonuuid
moon.pkg
import {
  "moonbitstack/moonuuid",
}

02Read one

parse takes text and returns a Uuid. Text that is not an identifier comes back as a Refused, so a function that parses has to say what it does with that. fn main raise lets the refusal out.

moonbit
fn main raise {
  let id = @moonuuid.parse("c232ab00-9414-11ec-b3c8-9f6bdeced846")
  println(id)
}
prints
c232ab00-9414-11ec-b3c8-9f6bdeced846

03Every spelling

Hyphens are optional. Case does not matter. Braces and the urn:uuid: prefix are set aside before the digits are read.

moonbit
fn main raise {
  for text in [
    "c232ab00-9414-11ec-b3c8-9f6bdeced846",
    "C232AB00941411ECB3C89F6BDECED846",
    "{C232AB00-9414-11EC-B3C8-9F6BDECED846}",
    "urn:uuid:c232ab00-9414-11ec-b3c8-9f6bdeced846",
  ] {
    println(@moonuuid.parse(text))
  }
}
prints
c232ab00-9414-11ec-b3c8-9f6bdeced846
c232ab00-9414-11ec-b3c8-9f6bdeced846
c232ab00-9414-11ec-b3c8-9f6bdeced846
c232ab00-9414-11ec-b3c8-9f6bdeced846

04Write it back

to_string gives the canonical form. hex drops the hyphens. urn adds the prefix. All three write lowercase.

moonbit
fn main raise {
  let id = @moonuuid.parse("c232ab00-9414-11ec-b3c8-9f6bdeced846")
  println(id.to_string())
  println(id.hex())
  println(id.urn())
}
prints
c232ab00-9414-11ec-b3c8-9f6bdeced846
c232ab00941411ecb3c89f6bdeced846
urn:uuid:c232ab00-9414-11ec-b3c8-9f6bdeced846

05Look inside

An identifier answers with the fields its bits name. time is 100-nanosecond intervals since 1582 for versions 1 and 6, and milliseconds since 1970 for version 7. version is None when the variant is not RFC 9562's, where the version bits mean nothing.

moonbit
fn main raise {
  let id = @moonuuid.parse("c232ab00-9414-11ec-b3c8-9f6bdeced846")
  println(id.version())    // Some(1)
  println(id.variant())    // Rfc
  println(id.time())       // 138648505420000000
  println(id.clock_seq())  // 13256
  println(id.node())       // 175285648414790
}
prints
Some(1)
Rfc
138648505420000000
13256
175285648414790

06A clock and a random source

The library never reaches for the clock or for randomness. Generator::new takes a clock that gives nanoseconds since 1970 and a random source, a moonrand Source: @system.source from moonrand/system in a program. It keeps what the time-based versions need between calls. Here the clock is fixed and the source replays fixed bytes, so every run prints the same lines.

moonbit
fn main raise {
  let gen = @moonuuid.Generator::new(
    clock=() => 1_761_350_400_000_000_000L,
    random=@moonrand.Replay::new(Bytes::makei(64, i => (i * 37 + 11).to_byte())),
  )
  println(gen.v4())
}
prints
0b30557a-9fc4-490e-b358-7da2c7ec1136
moonbit
fn main raise {
  let gen = @moonuuid.Generator::new(
    clock=() => 1_761_350_400_000_000_000L,
    random=@moonrand.Replay::new(Bytes::makei(64, i => (i * 37 + 11).to_byte())),
  )
  for _ in 0..<3 {
    println(gen.v7())
  }
}
prints
019a18aa-3800-74c1-957a-9fc4e90e3358
019a18aa-3800-74c1-957a-9fc57da2c7ec
019a18aa-3800-74c1-957a-9fc611365b80

Within one millisecond the counter goes up with a fresh random tail. A clock that steps back uses the millisecond after the last one, so the order holds.

07Time-based, field by field

ticks_of turns nanoseconds since 1970 into the 100-nanosecond intervals since 1582 that versions 1 and 6 count. Version 6 lays the timestamp's high bits first, so identifiers sort in the order they were made; version 1 keeps the older layout.

moonbit
fn main {
  let ticks = @moonuuid.ticks_of(1_761_350_400_000_000_000L)
  println(@moonuuid.v1(ticks~, clock_seq=0x2A3B, node=0x001A2B3C4D5EL))
  println(@moonuuid.v6(ticks~, clock_seq=0x2A3B, node=0x001A2B3C4D5EL))
}
prints
8cacc000-b135-11f0-aa3b-001a2b3c4d5e
1f0b1358-cacc-6000-aa3b-001a2b3c4d5e

08From a name

Versions 3 and 5 hash a namespace and a name: MD5 for version 3, SHA-1 for version 5. The same pair always gives the same identifier. The name is hashed as UTF-8, so text in any language is fine.

moonbit
fn main {
  println(@moonuuid.v5(@moonuuid.namespace_url, "https://example.org/"))
  println(@moonuuid.v5(@moonuuid.namespace_dns, "李雷"))
}
prints
527dda32-a0de-5105-a042-cb475b5f7d11
97ce974b-b58a-513c-994e-06af9b649d7f

namespace_dns, namespace_url, namespace_oid and namespace_x500 are the four namespaces RFC 9562 assigns.

09Your own layout

Version 8 keeps 122 bits of the sixteen bytes you pass and writes only the version and variant over them, so the layout is yours.

moonbit
fn main raise {
  let custom = Bytes::makei(16, i => (i * 17 + 3).to_byte())
  println(@moonuuid.v8(custom))
}
prints
03142536-4758-897a-8b9c-adbecfe0f102

10When it refuses

Refused says what was wrong. Malformed points at the character that stopped the parse; Size gives the two lengths.

moonbit
fn main {
  try {
    ignore(@moonuuid.parse("not-a-uuid"))
  } catch {
    e => println(e)
  }
  try {
    ignore(@moonuuid.v4(Bytes::make(3, 0)))
  } catch {
    e => println(e)
  }
}
prints
"n" is not a hexadecimal digit at 0
16 bytes wanted, 3 given

11JSON

An identifier writes itself as the canonical string, and reads any spelling back. This section needs moonbitlang/core/json in the package's imports.

moonbit
fn main raise {
  let id = @moonuuid.parse("c232ab00-9414-11ec-b3c8-9f6bdeced846")
  let j = Json(id)
  println(j.stringify())
  let back : @moonuuid.Uuid = @json.from_json(j)
  println(back)
}
prints
"c232ab00-9414-11ec-b3c8-9f6bdeced846"
c232ab00-9414-11ec-b3c8-9f6bdeced846