Improve time-related API in uuid package

- Let timestamps be specified by the user.
- Change `time_v*` to `raw_time_v*` and implement an API that returns
  timestamps from the `time` package.
This commit is contained in:
Feoramund
2024-06-22 18:21:32 -04:00
parent ea771d0cb7
commit 9b265b2309
3 changed files with 134 additions and 80 deletions
+31 -19
View File
@@ -11,25 +11,27 @@ Inputs:
- clock_seq: The clock sequence, a number which must be initialized to a random number once in the lifetime of a system. - clock_seq: The clock sequence, a number which must be initialized to a random number once in the lifetime of a system.
- node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system. - node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system.
If one is not provided or available, 48 bits of random state will take its place. If one is not provided or available, 48 bits of random state will take its place.
- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time.
Returns: Returns:
- result: The generated UUID. - result: The generated UUID.
*/ */
generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil) -> (result: Identifier) { generate_v1 :: proc(clock_seq: u16, node: Maybe([6]u8) = nil, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) {
assert(clock_seq <= 0x3FFF, "The clock sequence can only hold 14 bits of data; no number greater than 16,383.") assert(clock_seq <= 0x3FFF, "The clock sequence can only hold 14 bits of data; no number greater than 16,383.")
unix_time_in_hns_intervals := time.to_unix_nanoseconds(time.now()) / 100 unix_time_in_hns_intervals := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 100
timestamp := cast(u64le)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals)
timestamp_octets := transmute([8]u8)timestamp
result[0] = timestamp_octets[0] uuid_timestamp := cast(u64le)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals)
result[1] = timestamp_octets[1] uuid_timestamp_octets := transmute([8]u8)uuid_timestamp
result[2] = timestamp_octets[2]
result[3] = timestamp_octets[3]
result[4] = timestamp_octets[4]
result[5] = timestamp_octets[5]
result[6] = timestamp_octets[6] >> 4 result[0] = uuid_timestamp_octets[0]
result[7] = timestamp_octets[6] << 4 | timestamp_octets[7] result[1] = uuid_timestamp_octets[1]
result[2] = uuid_timestamp_octets[2]
result[3] = uuid_timestamp_octets[3]
result[4] = uuid_timestamp_octets[4]
result[5] = uuid_timestamp_octets[5]
result[6] = uuid_timestamp_octets[6] >> 4
result[7] = uuid_timestamp_octets[6] << 4 | uuid_timestamp_octets[7]
if realized_node, ok := node.?; ok { if realized_node, ok := node.?; ok {
mutable_node := realized_node mutable_node := realized_node
@@ -79,12 +81,13 @@ Inputs:
If unspecified, it will be replaced with random bits. If unspecified, it will be replaced with random bits.
- node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system. - node: An optional 48-bit spatially unique identifier, specified to be the IEEE 802 address of the system.
If one is not provided or available, 48 bits of random state will take its place. If one is not provided or available, 48 bits of random state will take its place.
- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time.
Returns: Returns:
- result: The generated UUID. - result: The generated UUID.
*/ */
generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil) -> (result: Identifier) { generate_v6 :: proc(clock_seq: Maybe(u16) = nil, node: Maybe([6]u8) = nil, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) {
unix_time_in_hns_intervals := time.to_unix_nanoseconds(time.now()) / 100 unix_time_in_hns_intervals := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 100
timestamp := cast(u128be)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals) timestamp := cast(u128be)(HNS_INTERVALS_BETWEEN_GREG_AND_UNIX + unix_time_in_hns_intervals)
@@ -128,12 +131,15 @@ bits and a 48 bit timestamp.
It is designed with time-based sorting in mind, such as for database usage, as 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. the highest bits are allocated from the timestamp of when it is created.
Inputs:
- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time.
Returns: Returns:
- result: The generated UUID. - result: The generated UUID.
*/ */
generate_v7 :: proc() -> (result: Identifier) { generate_v7_basic :: proc(timestamp: Maybe(time.Time) = nil) -> (result: Identifier) {
assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR)
unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6 unix_time_in_milliseconds := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 1e6
temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT
@@ -152,7 +158,7 @@ generate_v7 :: proc() -> (result: Identifier) {
} }
/* /*
Generate a version 7 UUID with an incremented counter. Generate a version 7 UUID that has an incremented counter.
This UUID will be pseudorandom, save for 6 pre-determined version and variant This UUID will be pseudorandom, save for 6 pre-determined version and variant
bits, a 48 bit timestamp, and 12 bits of counter state. bits, a 48 bit timestamp, and 12 bits of counter state.
@@ -178,14 +184,15 @@ Example:
Inputs: Inputs:
- counter: A 12-bit value, incremented each time a UUID is generated in a batch. - counter: A 12-bit value, incremented each time a UUID is generated in a batch.
- timestamp: A timestamp from the `core:time` package, or `nil` to use the current time.
Returns: Returns:
- result: The generated UUID. - result: The generated UUID.
*/ */
generate_v7_counter :: proc(counter: u16) -> (result: Identifier) { generate_v7_with_counter :: proc(counter: u16, timestamp: Maybe(time.Time) = nil) -> (result: Identifier) {
assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR) assert(.Cryptographic in runtime.random_generator_query_info(context.random_generator), NO_CSPRNG_ERROR)
assert(counter <= 0x0fff, "This implementation of the version 7 UUID does not support counters in excess of 12 bits (4,095).") 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 unix_time_in_milliseconds := time.to_unix_nanoseconds(timestamp.? or_else time.now()) / 1e6
temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT
temporary |= cast(u128be)counter << VERSION_7_COUNTER_SHIFT temporary |= cast(u128be)counter << VERSION_7_COUNTER_SHIFT
@@ -203,3 +210,8 @@ generate_v7_counter :: proc(counter: u16) -> (result: Identifier) {
return return
} }
generate_v7 :: proc {
generate_v7_basic,
generate_v7_with_counter,
}
+53 -10
View File
@@ -1,6 +1,7 @@
package uuid package uuid
import "base:runtime" import "base:runtime"
import "core:time"
/* /*
Convert a string to a UUID. Convert a string to a UUID.
@@ -127,7 +128,7 @@ node :: proc "contextless" (id: Identifier) -> (node: [6]u8) {
} }
/* /*
Get the timestamp of a version 1 UUID. Get the raw timestamp of a version 1 UUID.
Inputs: Inputs:
- id: The identifier. - id: The identifier.
@@ -135,7 +136,7 @@ Inputs:
Returns: Returns:
- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. - timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15.
*/ */
time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { raw_time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) {
timestamp_octets: [8]u8 timestamp_octets: [8]u8
timestamp_octets[0] = id[0] timestamp_octets[0] = id[0]
@@ -151,6 +152,39 @@ time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: u64) {
return cast(u64)transmute(u64le)timestamp_octets return cast(u64)transmute(u64le)timestamp_octets
} }
/*
Get the timestamp of a version 1 UUID.
Inputs:
- id: The identifier.
Returns:
- timestamp: The timestamp of the UUID.
*/
time_v1 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) {
delta := cast(time.Duration)(raw_time_v1(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100
return time.time_add({}, delta)
}
/*
Get the raw timestamp of a version 6 UUID.
Inputs:
- id: The identifier.
Returns:
- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15.
*/
raw_time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: u64) {
temporary := transmute(u128be)id
timestamp |= cast(u64)(temporary & 0xFFFFFFFF_FFFF0000_00000000_00000000 >> 68)
timestamp |= cast(u64)(temporary & 0x00000000_00000FFF_00000000_00000000 >> 64)
return timestamp
}
/* /*
Get the timestamp of a version 6 UUID. Get the timestamp of a version 6 UUID.
@@ -160,13 +194,23 @@ Inputs:
Returns: Returns:
- timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15. - timestamp: The timestamp, in 100-nanosecond intervals since 1582-10-15.
*/ */
time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { time_v6 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) {
temporary := transmute(u128be)id delta := cast(time.Duration)(raw_time_v6(id) - HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100
return time.time_add({}, delta)
}
timestamp |= cast(u64)(temporary & 0xFFFFFFFF_FFFF0000_00000000_00000000 >> 68) /*
timestamp |= cast(u64)(temporary & 0x00000000_00000FFF_00000000_00000000 >> 64) Get the raw timestamp of a version 7 UUID.
return timestamp Inputs:
- id: The identifier.
Returns:
- timestamp: The timestamp, in milliseconds since the UNIX epoch.
*/
raw_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)
} }
/* /*
@@ -178,9 +222,8 @@ Inputs:
Returns: Returns:
- timestamp: The timestamp, in milliseconds since the UNIX epoch. - timestamp: The timestamp, in milliseconds since the UNIX epoch.
*/ */
time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: u64) { time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: time.Time) {
time_bits := transmute(u128be)id & VERSION_7_TIME_MASK return time.time_add({}, cast(time.Duration)(raw_time_v7(id) * 1e6))
return cast(u64)(time_bits >> VERSION_7_TIME_SHIFT)
} }
/* /*
+50 -51
View File
@@ -67,87 +67,86 @@ test_legacy_namespaced_uuids :: proc(t: ^testing.T) {
test_v1 :: proc(t: ^testing.T) { test_v1 :: proc(t: ^testing.T) {
context.random_generator = crypto.random_generator() context.random_generator = crypto.random_generator()
point_a := time.time_add({}, 1 * time.Second)
point_b := time.time_add({}, 3 * time.Second)
point_c := time.time_add({}, 5 * time.Second)
CLOCK :: 0x3A1A CLOCK :: 0x3A1A
v1_a := uuid.generate_v1(CLOCK) v1_a := uuid.generate_v1(CLOCK, nil, point_a)
time.sleep(10 * time.Millisecond) v1_b := uuid.generate_v1(CLOCK, nil, point_b)
v1_b := uuid.generate_v1(CLOCK) v1_c := uuid.generate_v1(CLOCK, nil, point_c)
time.sleep(10 * time.Millisecond)
v1_c := uuid.generate_v1(CLOCK)
testing.expect_value(t, uuid.clock_seq(v1_a), CLOCK) testing.expect_value(t, uuid.clock_seq(v1_a), CLOCK)
time_bits_a := uuid.time_v1(v1_a) time_a := uuid.time_v1(v1_a)
time_bits_b := uuid.time_v1(v1_b) time_b := uuid.time_v1(v1_b)
time_bits_c := uuid.time_v1(v1_c) time_c := uuid.time_v1(v1_c)
time_a := time.Time { _nsec = cast(i64)((time_bits_a - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } log.debugf("A: %02x, %v", v1_a, time_a)
time_b := time.Time { _nsec = cast(i64)((time_bits_b - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } log.debugf("B: %02x, %v", v1_b, time_b)
time_c := time.Time { _nsec = cast(i64)((time_bits_c - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } log.debugf("C: %02x, %v", v1_c, time_c)
log.debugf("A: %02x, %i, %v", v1_a, time_bits_a, time_a) testing.expect(t, time.diff(time_a, time_b) > 0, "The time on the later-generated v1 UUID is earlier than its successor.")
log.debugf("B: %02x, %i, %v", v1_b, time_bits_b, time_b) testing.expect(t, time.diff(time_b, time_c) > 0, "The time on the later-generated v1 UUID is earlier than its successor.")
log.debugf("C: %02x, %i, %v", v1_c, time_bits_c, time_c) testing.expect(t, time.diff(time_a, time_c) > 0, "The time on the later-generated v1 UUID is earlier than its successor.")
testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.")
testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.")
testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v1 UUID are lesser than the earlier UUID.")
} }
@(test) @(test)
test_v6 :: proc(t: ^testing.T) { test_v6 :: proc(t: ^testing.T) {
context.random_generator = crypto.random_generator() context.random_generator = crypto.random_generator()
point_a := time.time_add({}, 1 * time.Second)
point_b := time.time_add({}, 3 * time.Second)
point_c := time.time_add({}, 5 * time.Second)
CLOCK :: 0x3A1A CLOCK :: 0x3A1A
v6_a := uuid.generate_v6(CLOCK) v6_a := uuid.generate_v6(CLOCK, nil, point_a)
time.sleep(10 * time.Millisecond) v6_b := uuid.generate_v6(CLOCK, nil, point_b)
v6_b := uuid.generate_v6(CLOCK) v6_c := uuid.generate_v6(CLOCK, nil, point_c)
time.sleep(10 * time.Millisecond)
v6_c := uuid.generate_v6(CLOCK)
testing.expect_value(t, uuid.clock_seq(v6_a), CLOCK) testing.expect_value(t, uuid.clock_seq(v6_a), CLOCK)
time_bits_a := uuid.time_v6(v6_a) time_a := uuid.time_v6(v6_a)
time_bits_b := uuid.time_v6(v6_b) time_b := uuid.time_v6(v6_b)
time_bits_c := uuid.time_v6(v6_c) time_c := uuid.time_v6(v6_c)
time_a := time.Time { _nsec = cast(i64)((time_bits_a - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } log.debugf("A: %02x, %v", v6_a, time_a)
time_b := time.Time { _nsec = cast(i64)((time_bits_b - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } log.debugf("B: %02x, %v", v6_b, time_b)
time_c := time.Time { _nsec = cast(i64)((time_bits_c - uuid.HNS_INTERVALS_BETWEEN_GREG_AND_UNIX) * 100) } log.debugf("C: %02x, %v", v6_c, time_c)
log.debugf("A: %02x, %i, %v", v6_a, time_bits_a, time_a) testing.expect(t, time.diff(time_a, time_b) > 0, "The time on the later-generated v6 UUID is earlier than its successor.")
log.debugf("B: %02x, %i, %v", v6_b, time_bits_b, time_b) testing.expect(t, time.diff(time_b, time_c) > 0, "The time on the later-generated v6 UUID is earlier than its successor.")
log.debugf("C: %02x, %i, %v", v6_c, time_bits_c, time_c) testing.expect(t, time.diff(time_a, time_c) > 0, "The time on the later-generated v6 UUID is earlier than its successor.")
testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.")
testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.")
testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v6 UUID are lesser than the earlier UUID.")
} }
@(test) @(test)
test_v7 :: proc(t: ^testing.T) { test_v7 :: proc(t: ^testing.T) {
context.random_generator = crypto.random_generator() context.random_generator = crypto.random_generator()
v7_a := uuid.generate_v7() point_a := time.time_add({}, 1 * time.Second)
time.sleep(10 * time.Millisecond) point_b := time.time_add({}, 3 * time.Second)
v7_b := uuid.generate_v7() point_c := time.time_add({}, 5 * time.Second)
time.sleep(10 * time.Millisecond)
v7_c := uuid.generate_v7()
time_bits_a := uuid.time_v7(v7_a) v7_a := uuid.generate_v7(point_a)
time_bits_b := uuid.time_v7(v7_b) v7_b := uuid.generate_v7(point_b)
time_bits_c := uuid.time_v7(v7_c) v7_c := uuid.generate_v7(point_c)
log.debugf("A: %02x, %i", v7_a, time_bits_a) time_a := uuid.time_v7(v7_a)
log.debugf("B: %02x, %i", v7_b, time_bits_b) time_b := uuid.time_v7(v7_b)
log.debugf("C: %02x, %i", v7_c, time_bits_c) time_c := uuid.time_v7(v7_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.") log.debugf("A: %02x, %v", v7_a, time_a)
testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") log.debugf("B: %02x, %v", v7_b, time_b)
testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.") log.debugf("C: %02x, %v", v7_c, time_c)
v7_with_counter := uuid.generate_v7_counter(0x555) testing.expect(t, time.diff(time_a, time_b) > 0, "The time on the later-generated v7 UUID is earlier than its successor.")
testing.expect(t, time.diff(time_b, time_c) > 0, "The time on the later-generated v7 UUID is earlier than its successor.")
testing.expect(t, time.diff(time_a, time_c) > 0, "The time on the later-generated v7 UUID is earlier than its successor.")
v7_with_counter := uuid.generate_v7(0x555)
log.debugf("D: %02x", v7_with_counter) log.debugf("D: %02x", v7_with_counter)
testing.expect_value(t, uuid.counter_v7(v7_with_counter), 0x555) testing.expect_value(t, uuid.counter_v7(v7_with_counter), 0x555)
} }
@(test) @(test)