diff --git a/core/encoding/uuid/definitions.odin b/core/encoding/uuid/definitions.odin index 1208e26f4..b54965e23 100644 --- a/core/encoding/uuid/definitions.odin +++ b/core/encoding/uuid/definitions.odin @@ -8,6 +8,11 @@ EXPECTED_LENGTH :: 8 + 4 + 4 + 4 + 12 + 4 VERSION_BYTE_INDEX :: 6 VARIANT_BYTE_INDEX :: 8 +VERSION_7_TIME_MASK :: 0xffffffff_ffff0000_00000000_00000000 +VERSION_7_TIME_SHIFT :: 80 +VERSION_7_COUNTER_MASK :: 0x00000000_00000fff_00000000_00000000 +VERSION_7_COUNTER_SHIFT :: 64 + Read_Error :: enum { None, Invalid_Length, diff --git a/core/encoding/uuid/generation.odin b/core/encoding/uuid/generation.odin index 9b790714c..7fe0bbd13 100644 --- a/core/encoding/uuid/generation.odin +++ b/core/encoding/uuid/generation.odin @@ -4,6 +4,7 @@ import "core:crypto/legacy/md5" import "core:crypto/legacy/sha1" import "core:math/rand" import "core:mem" +import "core:time" /* Generate a version 3 UUID. @@ -158,3 +159,86 @@ generate_v5 :: proc { generate_v5_bytes, generate_v5_string, } + +/* +Generate a version 7 UUID. + +This UUID will be pseudorandom, save for 6 pre-determined version and variant +bits and a 48 bit timestamp. + +It is designed with time-based sorting in mind, such as for database usage, as +the highest bits are allocated from the timestamp of when it is created. + +Returns: +- result: The generated UUID. +*/ +generate_v7 :: proc() -> (result: Identifier) { + unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 + + temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT + + bytes_generated := rand.read(result[6:]) + assert(bytes_generated == 10, "RNG failed to generate 10 bytes for UUID v7.") + + result |= transmute(Identifier)temporary + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x70 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} + +/* +Generate a version 7 UUID with an incremented counter. + +This UUID will be pseudorandom, save for 6 pre-determined version and variant +bits, a 48 bit timestamp, and 12 bits of counter state. + +It is designed with time-based sorting in mind, such as for database usage, as +the highest bits are allocated from the timestamp of when it is created. + +This procedure is preferable if you are generating hundreds or thousands of +UUIDs as a batch within the span of a millisecond. Do note that the counter +only has 12 bits of state, thus `counter` cannot exceed the number 4,095. + +Example: + + import "core:uuid" + + // Create a batch of UUIDs all at once. + batch: [dynamic]uuid.Identifier + + for i: u16 = 0; i < 1000; i += 1 { + my_uuid := uuid.generate_v7_counter(i) + append(&batch, my_uuid) + } + +Inputs: +- counter: A 12-bit value, incremented each time a UUID is generated in a batch. + +Returns: +- result: The generated UUID. +*/ +generate_v7_counter :: proc(counter: u16) -> (result: Identifier) { + assert(counter <= 0x0fff, "This implementation of the version 7 UUID does not support counters in excess of 12 bits (4,095).") + unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 + + temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT + temporary |= cast(u128be)counter << VERSION_7_COUNTER_SHIFT + + bytes_generated := rand.read(result[8:]) + assert(bytes_generated == 8, "RNG failed to generate 8 bytes for UUID v7.") + + result |= transmute(Identifier)temporary + + result[VERSION_BYTE_INDEX] &= 0x0F + result[VERSION_BYTE_INDEX] |= 0x70 + + result[VARIANT_BYTE_INDEX] &= 0x3F + result[VARIANT_BYTE_INDEX] |= 0x80 + + return +} diff --git a/core/encoding/uuid/reading.odin b/core/encoding/uuid/reading.odin index 0c0274e53..c72f5791e 100644 --- a/core/encoding/uuid/reading.odin +++ b/core/encoding/uuid/reading.odin @@ -95,3 +95,34 @@ variant :: proc "contextless" (id: Identifier) -> (variant: Variant_Type) #no_bo return .Unknown } } + +/* +Get the timestamp of a version 7 UUID. + +Inputs: +- id: The identifier. + +Returns: +- timestamp: The timestamp, in milliseconds since the UNIX epoch. +*/ +time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { + time_bits := transmute(u128be)id & VERSION_7_TIME_MASK + return cast(u64)(time_bits >> VERSION_7_TIME_SHIFT) +} + +/* +Get the 12-bit counter value of a version 7 UUID. + +The UUID must have been generated with a counter, otherwise this procedure will +return random bits. + +Inputs: +- id: The identifier. + +Returns: +- counter: The 12-bit counter value. +*/ +counter_v7 :: proc "contextless" (id: Identifier) -> (counter: u16) { + counter_bits := transmute(u128be)id & VERSION_7_COUNTER_MASK + return cast(u16)(counter_bits >> VERSION_7_COUNTER_SHIFT) +} diff --git a/tests/core/encoding/uuid/test_core_uuid.odin b/tests/core/encoding/uuid/test_core_uuid.odin index 717c5599d..b5e8e90cc 100644 --- a/tests/core/encoding/uuid/test_core_uuid.odin +++ b/tests/core/encoding/uuid/test_core_uuid.odin @@ -1,13 +1,16 @@ package test_core_uuid -import "core:testing" import "core:encoding/uuid" +import "core:log" +import "core:testing" +import "core:time" @(test) test_version_and_variant :: proc(t: ^testing.T) { v3 := uuid.generate_v3(uuid.Namespace_DNS, "") v4 := uuid.generate_v4() v5 := uuid.generate_v5(uuid.Namespace_DNS, "") + v7 := uuid.generate_v7() testing.expect_value(t, uuid.version(v3), 3) testing.expect_value(t, uuid.variant(v3), uuid.Variant_Type.RFC_4122) @@ -15,6 +18,8 @@ test_version_and_variant :: proc(t: ^testing.T) { testing.expect_value(t, uuid.variant(v4), uuid.Variant_Type.RFC_4122) testing.expect_value(t, uuid.version(v5), 5) testing.expect_value(t, uuid.variant(v5), uuid.Variant_Type.RFC_4122) + testing.expect_value(t, uuid.version(v7), 7) + testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122) } @(test) @@ -48,6 +53,31 @@ test_namespaced_uuids :: proc(t: ^testing.T) { } } +@(test) +test_v7 :: proc(t: ^testing.T) { + v7_a := uuid.generate_v7() + time.sleep(10 * time.Millisecond) + v7_b := uuid.generate_v7() + time.sleep(10 * time.Millisecond) + v7_c := uuid.generate_v7() + + time_bits_a := uuid.time_v7(v7_a) + time_bits_b := uuid.time_v7(v7_b) + time_bits_c := uuid.time_v7(v7_c) + + log.debugf("A: %02x, %i", v7_a, time_bits_a) + log.debugf("B: %02x, %i", v7_b, time_bits_b) + log.debugf("C: %02x, %i", v7_c, time_bits_c) + + testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") + testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") + + v7_with_counter := uuid.generate_v7_counter(0x555) + log.debugf("D: %02x", v7_with_counter) + testing.expect_value(t, uuid.counter_v7(v7_with_counter), 0x555) +} + @(test) test_writing :: proc(t: ^testing.T) { id: uuid.Identifier