Skip to content

Commit 2d3c2a9

Browse files
authored
feat: Generate V6 from custom time (#172)
* Add NewV6WithTime * Refactor generateV6 * fix NewV6WithTime doc comment * fix: remove fmt.Println from test --------- Co-authored-by: nicumicle <[email protected]>
1 parent 0e97ed3 commit 2d3c2a9

File tree

4 files changed

+170
-7
lines changed

4 files changed

+170
-7
lines changed

time.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,16 @@ func (t Time) UnixTime() (sec, nsec int64) {
4545
func GetTime() (Time, uint16, error) {
4646
defer timeMu.Unlock()
4747
timeMu.Lock()
48-
return getTime()
48+
return getTime(nil)
4949
}
5050

51-
func getTime() (Time, uint16, error) {
52-
t := timeNow()
51+
func getTime(customTime *time.Time) (Time, uint16, error) {
52+
var t time.Time
53+
if customTime == nil { // When not provided, use the current time
54+
t = timeNow()
55+
} else {
56+
t = *customTime
57+
}
5358

5459
// If we don't have a clock sequence already, set one.
5560
if clockSeq == 0 {

time_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package uuid
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestGetTime(t *testing.T) {
9+
now := time.Now()
10+
tt := map[string]struct {
11+
input func() *time.Time
12+
expectedTime int64
13+
}{
14+
"it should return the current time": {
15+
input: func() *time.Time {
16+
return nil
17+
},
18+
expectedTime: now.Unix(),
19+
},
20+
"it should return the provided time": {
21+
input: func() *time.Time {
22+
parsed, err := time.Parse(time.RFC3339, "2024-10-15T09:32:23Z")
23+
if err != nil {
24+
t.Errorf("timeParse unexpected error: %v", err)
25+
}
26+
return &parsed
27+
},
28+
expectedTime: 1728984743,
29+
},
30+
}
31+
32+
for name, tc := range tt {
33+
t.Run(name, func(t *testing.T) {
34+
result, _, err := getTime(tc.input())
35+
if err != nil {
36+
t.Errorf("getTime unexpected error: %v", err)
37+
}
38+
sec, _ := result.UnixTime()
39+
if sec != tc.expectedTime {
40+
t.Errorf("expected %v, got %v", tc.expectedTime, result)
41+
}
42+
})
43+
}
44+
}

version6.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
package uuid
66

7-
import "encoding/binary"
7+
import (
8+
"encoding/binary"
9+
"time"
10+
)
811

912
// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality.
1013
// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs.
@@ -19,12 +22,32 @@ import "encoding/binary"
1922
// SetClockSequence then it will be set automatically. If GetTime fails to
2023
// return the current NewV6 returns Nil and an error.
2124
func NewV6() (UUID, error) {
22-
var uuid UUID
2325
now, seq, err := GetTime()
2426
if err != nil {
25-
return uuid, err
27+
return Nil, err
28+
}
29+
return generateV6(now, seq), nil
30+
}
31+
32+
// NewV6WithTime returns a Version 6 UUID based on the current NodeID, clock
33+
// sequence, and a specified time. It is similar to the NewV6 function, but allows
34+
// you to specify the time. If time is passed as nil, then the current time is used.
35+
//
36+
// There is a limit on how many UUIDs can be generated for the same time, so if you
37+
// are generating multiple UUIDs, it is recommended to increment the time.
38+
// If getTime fails to return the current NewV6WithTime returns Nil and an error.
39+
func NewV6WithTime(customTime *time.Time) (UUID, error) {
40+
now, seq, err := getTime(customTime)
41+
if err != nil {
42+
return Nil, err
2643
}
2744

45+
return generateV6(now, seq), nil
46+
}
47+
48+
func generateV6(now Time, seq uint16) UUID {
49+
var uuid UUID
50+
2851
/*
2952
0 1 2 3
3053
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
@@ -56,5 +79,5 @@ func NewV6() (UUID, error) {
5679
copy(uuid[10:], nodeID[:])
5780
nodeMu.Unlock()
5881

59-
return uuid, nil
82+
return uuid
6083
}

version6_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package uuid
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestNewV6WithTime(t *testing.T) {
9+
testCases := map[string]string{
10+
"test with current date": time.Now().Format(time.RFC3339), // now
11+
"test with past date": time.Now().Add(-1 * time.Hour * 24 * 365).Format(time.RFC3339), // 1 year ago
12+
"test with future date": time.Now().Add(time.Hour * 24 * 365).Format(time.RFC3339), // 1 year from now
13+
"test with different timezone": "2021-09-01T12:00:00+04:00",
14+
"test with negative timezone": "2021-09-01T12:00:00-12:00",
15+
"test with future date in different timezone": "2124-09-23T12:43:30+09:00",
16+
}
17+
18+
for testName, inputTime := range testCases {
19+
t.Run(testName, func(t *testing.T) {
20+
customTime, err := time.Parse(time.RFC3339, inputTime)
21+
if err != nil {
22+
t.Errorf("time.Parse returned unexpected error %v", err)
23+
}
24+
id, err := NewV6WithTime(&customTime)
25+
if err != nil {
26+
t.Errorf("NewV6WithTime returned unexpected error %v", err)
27+
}
28+
29+
if id.Version() != 6 {
30+
t.Errorf("got %d, want version 6", id.Version())
31+
}
32+
unixTime := time.Unix(id.Time().UnixTime())
33+
// Compare the times in UTC format, since the input time might have different timezone,
34+
// and the result is always in system timezone
35+
if customTime.UTC().Format(time.RFC3339) != unixTime.UTC().Format(time.RFC3339) {
36+
t.Errorf("got %s, want %s", unixTime.Format(time.RFC3339), customTime.Format(time.RFC3339))
37+
}
38+
})
39+
}
40+
}
41+
42+
func TestNewV6FromTimeGeneratesUniqueUUIDs(t *testing.T) {
43+
now := time.Now()
44+
ids := make([]string, 0)
45+
runs := 26000
46+
47+
for i := 0; i < runs; i++ {
48+
now = now.Add(time.Nanosecond) // Without this line, we can generate only 16384 UUIDs for the same timestamp
49+
id, err := NewV6WithTime(&now)
50+
if err != nil {
51+
t.Errorf("NewV6WithTime returned unexpected error %v", err)
52+
}
53+
if id.Version() != 6 {
54+
t.Errorf("got %d, want version 6", id.Version())
55+
}
56+
57+
// Make sure we add only unique values
58+
if !contains(t, ids, id.String()) {
59+
ids = append(ids, id.String())
60+
}
61+
}
62+
63+
// Check we added all the UIDs
64+
if len(ids) != runs {
65+
t.Errorf("got %d UUIDs, want %d", len(ids), runs)
66+
}
67+
}
68+
69+
func BenchmarkNewV6WithTime(b *testing.B) {
70+
b.RunParallel(func(pb *testing.PB) {
71+
for pb.Next() {
72+
now := time.Now()
73+
_, err := NewV6WithTime(&now)
74+
if err != nil {
75+
b.Fatal(err)
76+
}
77+
}
78+
})
79+
}
80+
81+
func contains(t *testing.T, arr []string, str string) bool {
82+
t.Helper()
83+
84+
for _, a := range arr {
85+
if a == str {
86+
return true
87+
}
88+
}
89+
90+
return false
91+
}

0 commit comments

Comments
 (0)