Skip to content

Commit e0a7f87

Browse files
authored
rand: add uuid_v7(), session function, simplify uuid_v4() (#24313)
1 parent 0bcfd0f commit e0a7f87

4 files changed

Lines changed: 149 additions & 58 deletions

File tree

‎vlib/rand/rand.c.v‎

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,97 @@
11
module rand
22

3-
const clock_seq_hi_and_reserved_valid_values = [`8`, `9`, `a`, `b`]!
3+
import time
44

55
// uuid_v4 generates a random (v4) UUID
66
// See https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)
7+
// See https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-4
78
pub fn uuid_v4() string {
8-
return internal_uuid_v4(mut default_rng)
9+
rand_1 := default_rng.u64()
10+
rand_2 := default_rng.u64()
11+
return internal_uuid(4, rand_1, rand_2)
912
}
1013

11-
@[direct_array_access]
12-
fn internal_uuid_v4(mut rng PRNG) string {
13-
buflen := 36
14+
@[direct_array_access; inline]
15+
fn internal_uuid(version u8, rand_1 u64, rand_2 u64) string {
16+
// 0 1 2 3
17+
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
18+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19+
// | rand_1 |
20+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21+
// | rand_1 | ver | rand_1 |
22+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23+
// |var| rand_2 |
24+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25+
// | rand_2 |
26+
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27+
28+
mut parts := [8]u16{}
29+
parts[0] = u16(rand_1 >> 48)
30+
parts[1] = u16(rand_1 >> 32)
31+
parts[2] = u16(rand_1 >> 16)
32+
parts[3] = u16(rand_1)
33+
parts[4] = u16(rand_2 >> 48)
34+
parts[5] = u16(rand_2 >> 32)
35+
parts[6] = u16(rand_2 >> 16)
36+
parts[7] = u16(rand_2)
37+
38+
parts[3] = (parts[3] & 0x0FFF) | (u16(version) << 12) // set version
39+
parts[4] = (parts[4] & 0x3FFF) | 0x8000 // set variant = 0b10
40+
1441
mut buf := unsafe { malloc_noscan(37) }
15-
mut i_buf := 0
16-
mut x := u64(0)
17-
mut d := u8(0)
18-
for i_buf < buflen {
19-
mut c := 0
20-
x = rng.u64()
21-
// do most of the bit manipulation at once:
22-
x &= 0x0F0F0F0F0F0F0F0F
23-
x += 0x3030303030303030
24-
// write the ASCII codes to the buffer:
25-
for c < 8 && i_buf < buflen {
26-
d = u8(x)
27-
unsafe {
28-
buf[i_buf] = if d > 0x39 { d + 0x27 } else { d }
42+
mut start := 0
43+
unsafe {
44+
for i in 0 .. 8 {
45+
val := parts[i]
46+
buf[start] = hex_chars[(val >> 12) & 0xF]
47+
buf[start + 1] = hex_chars[(val >> 8) & 0xF]
48+
buf[start + 2] = hex_chars[(val >> 4) & 0xF]
49+
buf[start + 3] = hex_chars[val & 0xF]
50+
start += 4
51+
// insert `_` at specified locations
52+
if start in [8, 13, 18, 23]! {
53+
buf[start] = `_`
54+
start++
2955
}
30-
i_buf++
31-
c++
32-
x = x >> 8
3356
}
57+
buf[36] = 0
58+
return buf.vstring_with_len(36)
3459
}
35-
// there are still some random bits in x:
36-
x = x >> 8
37-
d = u8(x)
38-
unsafe {
39-
// From https://www.ietf.org/rfc/rfc4122.txt :
40-
// >> Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved
41-
// >> to zero and one, respectively.
42-
// all nibbles starting with 10 are: 1000, 1001, 1010, 1011 -> hex digits `8`, `9`, `a`, `b`
43-
// these are stored in clock_seq_hi_and_reserved_valid_values, choose one of them at random:
44-
buf[19] = clock_seq_hi_and_reserved_valid_values[d & 0x03]
45-
// >> Set the four most significant bits (bits 12 through 15) of the
46-
// >> time_hi_and_version field to the 4-bit version number from Section 4.1.3.
47-
buf[14] = `4`
48-
buf[8] = `-`
49-
buf[13] = `-`
50-
buf[18] = `-`
51-
buf[23] = `-`
52-
buf[buflen] = 0 // ensure the string will be 0 terminated, just in case
53-
// for i in 0..37 { println('i: ${i:2} | ${buf[i].ascii_str()} | ${buf[i].hex()} | ${buf[i]:08b}') }
54-
return buf.vstring_with_len(buflen)
55-
}
60+
}
61+
62+
// uuid_v7 generates a time-ordered (v7) UUID
63+
// See https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7
64+
pub fn uuid_v7() string {
65+
timestamp_48 := u64(time.now().unix_milli()) << 16
66+
rand_1 := timestamp_48 | default_rng.u16()
67+
rand_2 := default_rng.u64()
68+
return internal_uuid(7, rand_1, rand_2)
69+
}
70+
71+
pub struct UUIDSession {
72+
mut:
73+
counter u8 // 6 bits session counter
74+
}
75+
76+
// new_uuid_v7_session create a new session for generating uuid_v7.
77+
// The 12 bits `rand_a` in the RFC 9652, is replaced by 6 bits
78+
// sub-millisecond timestamp + 6 bits session counter.
79+
// See https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=78c5e141e9c139fc2ff36a220334e4aa25e1b0eb
80+
pub fn new_uuid_v7_session() UUIDSession {
81+
return UUIDSession{}
82+
}
83+
84+
// next get a new uuid_v7 from current session.
85+
pub fn (mut u UUIDSession) next() string {
86+
timestamp := u64(time.now().unix_nano())
87+
// make place for holding 4 bits `version`
88+
timestamp_shift_4bits := (timestamp & 0xFFFF_FFFF_FFFF_0000) | ((timestamp & 0x0000_0000_0000_FFFF) >> 4)
89+
rand_1 := (timestamp_shift_4bits & 0xFFFF_FFFF_FFFF_FFC0) | u64(u.counter & 0x3F) // 6 bits session counter
90+
rand_2 := default_rng.u64()
91+
92+
u.counter++
93+
94+
return internal_uuid(7, rand_1, rand_2)
5695
}
5796

5897
const ulid_encoding = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'

‎vlib/rand/rand.v‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ pub fn read(mut buf []u8) {
701701
}
702702

703703
const english_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
704-
const hex_chars = 'abcdef0123456789'
704+
const hex_chars = '0123456789abcdef'
705705
const ascii_chars = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\^_`abcdefghijklmnopqrstuvwxyz{|}~'
706706

707707
// ulid generates an unique lexicographically sortable identifier.

‎vlib/rand/random_identifiers_test.v‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,58 @@ fn test_rand_uuid_v4() {
2121
}
2222
}
2323

24+
// uuid_v7:
25+
fn test_rand_uuid_v7() {
26+
uuid1 := rand.uuid_v7()
27+
uuid2 := rand.uuid_v7()
28+
uuid3 := rand.uuid_v7()
29+
assert uuid1 != uuid2
30+
assert uuid1 != uuid3
31+
assert uuid2 != uuid3
32+
assert uuid1.len == 36
33+
assert uuid2.len == 36
34+
assert uuid3.len == 36
35+
for i in 0 .. 1000 {
36+
x := rand.uuid_v7()
37+
// check the version field is always 7:
38+
assert x[14] == `7`
39+
// and variant field is always 0b10:
40+
assert x[19] in [`8`, `9`, `a`, `b`]
41+
}
42+
}
43+
44+
// uuid_v7_session:
45+
fn test_rand_uuid_v7_session() {
46+
mut u := rand.new_uuid_v7_session()
47+
uuid1 := u.next()
48+
uuid2 := u.next()
49+
uuid3 := u.next()
50+
assert uuid1 != uuid2
51+
assert uuid1 != uuid3
52+
assert uuid2 != uuid3
53+
assert uuid1.len == 36
54+
assert uuid2.len == 36
55+
assert uuid3.len == 36
56+
mut prev_counter := `3`
57+
for i in 0 .. 1000 {
58+
x := u.next()
59+
// check the version field is always 7:
60+
assert x[14] == `7`
61+
// and variant field is always 0b10:
62+
assert x[19] in [`8`, `9`, `a`, `b`]
63+
64+
// verify counter increase
65+
assert x[17] == prev_counter
66+
if prev_counter == `9` {
67+
prev_counter = `a`
68+
} else if prev_counter == `f` {
69+
prev_counter = `0`
70+
} else {
71+
prev_counter++
72+
}
73+
}
74+
}
75+
2476
// ulids:
2577
fn test_ulids_are_unique() {
2678
ulid1 := rand.ulid()

‎vlib/rand/random_numbers_test.v‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -302,20 +302,20 @@ fn test_rand_string() {
302302
fn test_rand_hex() {
303303
rand.seed([u32(0), 1])
304304
outputs := [
305-
'847b633d9f9765c1a84d38035',
306-
'efdef342641958db89cfdb4e1',
307-
'704ee34204d29e9e99aca0ae0',
308-
'0c8e1fd5472f65fc4b9668adf',
309-
'3349538378c2023ef7f14dfbe',
310-
'ae4080a0cb4cbb0693c68037b',
311-
'90e3a7be588b3dfeb3663c97f',
312-
'f25a82eb559ab6f0288bd8590',
313-
'649f579cb93e9f414d9f40539',
314-
'553a210a52bcbfbafb0783850',
315-
'3daef80b45ef518d30c6db6db',
316-
'56a187106e6e5fb88761024a5',
317-
'b5cd8b7a24054d7dc66e62f88',
318-
'306eed0c4207d8db185f04afd',
305+
'ead1c993f5fdcb270ea39e69b',
306+
'453459a8ca7fbe31ef2531a47',
307+
'd6a449a86a38f4f4ff0206046',
308+
'62e4753bad85cb52a1fcce035',
309+
'99afb9e9de2868945d57a3514',
310+
'04a6e60621a2116cf92ce69d1',
311+
'f6490d14bee1935419cc92fd5',
312+
'58b0e841bbf01c568ee13ebf6',
313+
'caf5bdf21f94f5a7a3f5a6b9f',
314+
'bb908760b8121510516de9eb6',
315+
'93045e61ab45b7e3962c31c31',
316+
'bc07ed76c4c4b51eedc768a0b',
317+
'1b23e1d08a6ba3d32cc4c85ee',
318+
'96c44362a86d3e317eb56a053',
319319
]
320320
for output in outputs {
321321
assert rand.hex(25) == output

0 commit comments

Comments
 (0)