diff --git a/vlib/rand/rand.v b/vlib/rand/rand.v index 9dd0ddaa0c..227f93c6d2 100644 --- a/vlib/rand/rand.v +++ b/vlib/rand/rand.v @@ -6,6 +6,8 @@ module rand import rand.util import rand.wyrand +import time + // Configuration struct for creating a new instance of the default RNG. pub struct PRNGConfigStruct { seed []u32 = util.time_seed_array(2) @@ -182,3 +184,51 @@ pub fn uuid_v4() string { } return string(buf, buflen) } + +const( + ulid_encoding = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" +) + +// rand.ulid generates an Unique Lexicographically sortable IDentifier. +// See https://github.com/ulid/spec . +// NB: ULIDs can leak timing information, if you make them public, because +// you can infer the rate at which some resource is being created, like +// users or business transactions. +// (https://news.ycombinator.com/item?id=14526173) +pub fn ulid() string { + buflen := 26 + mut buf := malloc(27) + // time section + mut t := time.utc().unix_time_milli() + mut i := 9 + for i >= 0 { + unsafe{ + buf[i] = ulid_encoding[t & 0x1F] + } + t = t >> 5 + i-- + } + // first rand set + mut x := default_rng.u64() + i = 10 + for i < 19 { + unsafe{ + buf[i] = ulid_encoding[x & 0x1F] + } + x = x >> 5 + i++ + } + // second rand set + x = default_rng.u64() + for i < 26 { + unsafe{ + buf[i] = ulid_encoding[x & 0x1F] + } + x = x >> 5 + i++ + } + unsafe{ + buf[26] = 0 + } + return string(buf,buflen) +} diff --git a/vlib/rand/random_identifiers_test.v b/vlib/rand/random_identifiers_test.v new file mode 100644 index 0000000000..926c313aec --- /dev/null +++ b/vlib/rand/random_identifiers_test.v @@ -0,0 +1,78 @@ +import time +import rand + +// uuid_v4: +fn test_rand_uuid_v4() { + uuid1 := rand.uuid_v4() + uuid2 := rand.uuid_v4() + uuid3 := rand.uuid_v4() + assert uuid1 != uuid2 + assert uuid1 != uuid3 + assert uuid2 != uuid3 + assert uuid1.len == 36 + assert uuid2.len == 36 + assert uuid3.len == 36 + assert uuid1[14] == `4` + assert uuid2[14] == `4` + assert uuid3[14] == `4` +} + +// ulids: +fn test_ulids_are_unique() { + ulid1 := rand.ulid() + ulid2 := rand.ulid() + ulid3 := rand.ulid() + assert ulid1.len == 26 + assert ulid2.len == 26 + assert ulid3.len == 26 + assert ulid1 != ulid2 + assert ulid1 != ulid3 + assert ulid2 != ulid3 +} + +fn test_ulids_max_start_character_is_ok() { + ulid1 := rand.ulid() + // the largest valid ULID encoded in Base32 is 7ZZZZZZZZZZZZZZZZZZZZZZZZZ + assert (int(ulid1[0]) - 48) <= 7 +} + +fn test_ulids_generated_in_the_same_millisecond_have_the_same_prefix() { + mut t1 := time.utc() + mut t2 := time.utc() + mut ulid1 := '' + mut ulid2 := '' + mut ulid3 := '' + for { + t1 = time.utc() + ulid1 = rand.ulid() + ulid2 = rand.ulid() + ulid3 = rand.ulid() + t2 = time.utc() + if t1.unix_time_milli() == t2.unix_time_milli() { + break + } + } + ulid1_prefix := ulid1[0..10] + ulid2_prefix := ulid2[0..10] + ulid3_prefix := ulid3[0..10] + assert ulid1_prefix == ulid2_prefix + assert ulid1_prefix == ulid3_prefix +} + +fn test_ulids_should_be_lexicographically_ordered_when_not_in_same_millisecond() { + ulid1 := rand.ulid() + time.sleep_ms(1) + ulid2 := rand.ulid() + time.sleep_ms(1) + ulid3 := rand.ulid() + mut all := [ulid3, ulid2, ulid1] + // eprintln('all before: $all') + all.sort() + // eprintln('all after: $all') + s1 := all[0] + s2 := all[1] + s3 := all[2] + assert s1 == ulid1 + assert s2 == ulid2 + assert s3 == ulid3 +} diff --git a/vlib/rand/random_numbers_test.v b/vlib/rand/random_numbers_test.v index 533cf67866..6d6e5506ad 100644 --- a/vlib/rand/random_numbers_test.v +++ b/vlib/rand/random_numbers_test.v @@ -175,18 +175,3 @@ fn test_rand_f64_in_range() { assert value < max } } - -fn test_rand_uuid_v4() { - uuid1 := rand.uuid_v4() - uuid2 := rand.uuid_v4() - uuid3 := rand.uuid_v4() - assert uuid1 != uuid2 - assert uuid1 != uuid3 - assert uuid2 != uuid3 - assert uuid1.len == 36 - assert uuid2.len == 36 - assert uuid3.len == 36 - assert uuid1[14] == `4` - assert uuid2[14] == `4` - assert uuid3[14] == `4` -} diff --git a/vlib/time/time.v b/vlib/time/time.v index ea7021aa32..3474d30ae1 100644 --- a/vlib/time/time.v +++ b/vlib/time/time.v @@ -138,27 +138,8 @@ pub fn (t Time) smonth() string { // new_time returns a time struct with calculated Unix time. pub fn new_time(t Time) Time { - return Time{ - year: t.year - month: t.month - day: t.day - hour: t.hour - minute: t.minute - second: t.second - unix: u64(t.unix_time()) - microsecond: t.microsecond - } - // TODO Use the syntax below when it works with reserved keywords like `unix` - // return { - // t | - // unix:t.unix_time() - // } -} - -// unix_time returns Unix time. -pub fn (t Time) unix_time() int { if t.unix != 0 { - return int(t.unix) + return t } tt := C.tm{ tm_sec: t.second @@ -168,7 +149,20 @@ pub fn (t Time) unix_time() int { tm_mon: t.month - 1 tm_year: t.year - 1900 } - return make_unix_time(tt) + utime := u64(make_unix_time(tt)) + return { t | unix: utime } +} + +// unix_time returns Unix time. +[inline] +pub fn (t Time) unix_time() int { + return int(t.unix) +} + +// unix_time_milli returns Unix time with millisecond resolution. +[inline] +pub fn (t Time) unix_time_milli() u64 { + return t.unix * 1000 + u64(t.microsecond/1000) } // add_seconds returns a new time struct with an added number of seconds. diff --git a/vlib/time/time_test.v b/vlib/time/time_test.v index ae220129f0..f9fe6c6c2f 100644 --- a/vlib/time/time_test.v +++ b/vlib/time/time_test.v @@ -192,3 +192,21 @@ fn test_utc() { assert now.microsecond >= 0 assert now.microsecond < 1000000 } + +fn test_unix_time() { + t1 := time.utc() + time.sleep_ms(50) + t2 := time.utc() + ut1 := t1.unix_time() + ut2 := t2.unix_time() + assert ut2 - ut1 < 2 + // + utm1 := t1.unix_time_milli() + utm2 := t2.unix_time_milli() + assert (utm1 - u64(ut1)*1000) < 1000 + assert (utm2 - u64(ut2)*1000) < 1000 + // + //println('utm1: $utm1 | utm2: $utm2') + assert utm2 - utm1 > 2 + assert utm2 - utm1 < 999 +}