From 72a11e8f28b2aea8d76e2830952e7ccd76629e9b Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:23:58 +0200 Subject: [PATCH 1/4] Add NewV6WithTime --- time.go | 11 ++++-- time_test.go | 46 ++++++++++++++++++++++++ version6.go | 33 ++++++++++++++++-- version6_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 time_test.go create mode 100644 version6_test.go diff --git a/time.go b/time.go index aa1df76..efa4d42 100644 --- a/time.go +++ b/time.go @@ -45,11 +45,16 @@ func (t Time) UnixTime() (sec, nsec int64) { func GetTime() (Time, uint16, error) { defer timeMu.Unlock() timeMu.Lock() - return getTime() + return getTime(nil) } -func getTime() (Time, uint16, error) { - t := timeNow() +func getTime(customTime *time.Time) (Time, uint16, error) { + var t time.Time + if customTime == nil { // When not provided, use the current time + t = timeNow() + } else { + t = *customTime + } // If we don't have a clock sequence already, set one. if clockSeq == 0 { diff --git a/time_test.go b/time_test.go new file mode 100644 index 0000000..7442fa2 --- /dev/null +++ b/time_test.go @@ -0,0 +1,46 @@ +package uuid + +import ( + "fmt" + "testing" + "time" +) + +func TestGetTime(t *testing.T) { + now := time.Now() + tt := map[string]struct { + input func() *time.Time + expectedTime int64 + }{ + "it should return the current time": { + input: func() *time.Time { + return nil + }, + expectedTime: now.Unix(), + }, + "it should return the provided time": { + input: func() *time.Time { + parsed, err := time.Parse(time.RFC3339, "2024-10-15T09:32:23Z") + if err != nil { + t.Errorf("timeParse unexpected error: %v", err) + } + fmt.Println(parsed.Unix()) + return &parsed + }, + expectedTime: 1728984743, + }, + } + + for name, tc := range tt { + t.Run(name, func(t *testing.T) { + result, _, err := getTime(tc.input()) + if err != nil { + t.Errorf("getTime unexpected error: %v", err) + } + sec, _ := result.UnixTime() + if sec != tc.expectedTime { + t.Errorf("expected %v, got %v", tc.expectedTime, result) + } + }) + } +} diff --git a/version6.go b/version6.go index 3b6d011..99f097a 100644 --- a/version6.go +++ b/version6.go @@ -4,7 +4,10 @@ package uuid -import "encoding/binary" +import ( + "encoding/binary" + "time" +) // UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality. // It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs. @@ -19,8 +22,32 @@ import "encoding/binary" // SetClockSequence then it will be set automatically. If GetTime fails to // return the current NewV6 returns Nil and an error. func NewV6() (UUID, error) { - var uuid UUID - now, seq, err := GetTime() + return generateV6(nil) +} + +// NewV6WithTime returns a Version 6 UUID based on the current NodeID and clock +// sequence, and a specified time. It is similar to the NewV6 function, but allows +// you to specify the time. If time is passed as nil, then the current time is used. +// There is a limit on how many UUIDs can be generated for the same time, so if you +// are generating multiple UUIDs, it is recommended to increment the time +func NewV6WithTime(now *time.Time) (UUID, error) { + return generateV6(now) +} + +func generateV6(customTime *time.Time) (UUID, error) { + var ( + uuid UUID + now Time + seq uint16 + err error + ) + + if customTime != nil { // Get the time from the customTime + now, seq, err = getTime(customTime) + } else { // Use GetTime to get the current time + now, seq, err = GetTime() + } + // Return an error if unable to get time if err != nil { return uuid, err } diff --git a/version6_test.go b/version6_test.go new file mode 100644 index 0000000..690c09d --- /dev/null +++ b/version6_test.go @@ -0,0 +1,91 @@ +package uuid + +import ( + "testing" + "time" +) + +func TestNewV6WithTime(t *testing.T) { + testCases := map[string]string{ + "test with current date": time.Now().Format(time.RFC3339), // now + "test with past date": time.Now().Add(-1 * time.Hour * 24 * 365).Format(time.RFC3339), // 1 year ago + "test with future date": time.Now().Add(time.Hour * 24 * 365).Format(time.RFC3339), // 1 year from now + "test with different timezone": "2021-09-01T12:00:00+04:00", + "test with negative timezone": "2021-09-01T12:00:00-12:00", + "test with future date in different timezone": "2124-09-23T12:43:30+09:00", + } + + for testName, inputTime := range testCases { + t.Run(testName, func(t *testing.T) { + customTime, err := time.Parse(time.RFC3339, inputTime) + if err != nil { + t.Errorf("time.Parse returned unexpected error %v", err) + } + id, err := NewV6WithTime(&customTime) + if err != nil { + t.Errorf("NewV6WithTime returned unexpected error %v", err) + } + + if id.Version() != 6 { + t.Errorf("got %d, want version 6", id.Version()) + } + unixTime := time.Unix(id.Time().UnixTime()) + // Compare the times in UTC format, since the input time might have different timezone, + // and the result is always in system timezone + if customTime.UTC().Format(time.RFC3339) != unixTime.UTC().Format(time.RFC3339) { + t.Errorf("got %s, want %s", unixTime.Format(time.RFC3339), customTime.Format(time.RFC3339)) + } + }) + } +} + +func TestNewV6FromTimeGeneratesUniqueUUIDs(t *testing.T) { + now := time.Now() + ids := make([]string, 0) + runs := 26000 + + for i := 0; i < runs; i++ { + now = now.Add(time.Nanosecond) // Without this line, we can generate only 16384 UUIDs for the same timestamp + id, err := NewV6WithTime(&now) + if err != nil { + t.Errorf("NewV6WithTime returned unexpected error %v", err) + } + if id.Version() != 6 { + t.Errorf("got %d, want version 6", id.Version()) + } + + // Make sure we add only unique values + if !contains(t, ids, id.String()) { + ids = append(ids, id.String()) + } + } + + // Check we added all the UIDs + if len(ids) != runs { + t.Errorf("got %d UUIDs, want %d", len(ids), runs) + } +} + +func BenchmarkNewV6WithTime(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + now := time.Now() + _, err := NewV6WithTime(&now) + if err != nil { + b.Fatal(err) + } + } + }) +} + +func contains(t *testing.T, arr []string, str string) bool { + t.Helper() + + for _, a := range arr { + if a == str { + return true + } + } + + return false +} From a469771a4d87778c207e33b6505e1359935721e2 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 16 Oct 2024 06:45:20 +0200 Subject: [PATCH 2/4] Refactor generateV6 --- version6.go | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/version6.go b/version6.go index 99f097a..9be491c 100644 --- a/version6.go +++ b/version6.go @@ -22,36 +22,31 @@ import ( // SetClockSequence then it will be set automatically. If GetTime fails to // return the current NewV6 returns Nil and an error. func NewV6() (UUID, error) { - return generateV6(nil) + now, seq, err := GetTime() + if err != nil { + return Nil, err + } + return generateV6(now, seq), nil } -// NewV6WithTime returns a Version 6 UUID based on the current NodeID and clock +// NewV6WithTime returns a Version 6 UUID based on the current NodeID, clock // sequence, and a specified time. It is similar to the NewV6 function, but allows // you to specify the time. If time is passed as nil, then the current time is used. // There is a limit on how many UUIDs can be generated for the same time, so if you // are generating multiple UUIDs, it is recommended to increment the time -func NewV6WithTime(now *time.Time) (UUID, error) { - return generateV6(now) -} - -func generateV6(customTime *time.Time) (UUID, error) { - var ( - uuid UUID - now Time - seq uint16 - err error - ) - - if customTime != nil { // Get the time from the customTime - now, seq, err = getTime(customTime) - } else { // Use GetTime to get the current time - now, seq, err = GetTime() - } - // Return an error if unable to get time +// If getTime fails to return the current NewV6 returns Nil and an error. +func NewV6WithTime(customTime *time.Time) (UUID, error) { + now, seq, err := getTime(customTime) if err != nil { - return uuid, err + return Nil, err } + return generateV6(now, seq), nil +} + +func generateV6(now Time, seq uint16) UUID { + var uuid UUID + /* 0 1 2 3 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 @@ -83,5 +78,5 @@ func generateV6(customTime *time.Time) (UUID, error) { copy(uuid[10:], nodeID[:]) nodeMu.Unlock() - return uuid, nil + return uuid } From 11c34712e8d27d3f9d766b9ee57cd3f839dcc2a1 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Sat, 19 Oct 2024 07:52:09 +0200 Subject: [PATCH 3/4] fix NewV6WithTime doc comment --- version6.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/version6.go b/version6.go index 9be491c..17bbafe 100644 --- a/version6.go +++ b/version6.go @@ -32,9 +32,10 @@ func NewV6() (UUID, error) { // NewV6WithTime returns a Version 6 UUID based on the current NodeID, clock // sequence, and a specified time. It is similar to the NewV6 function, but allows // you to specify the time. If time is passed as nil, then the current time is used. +// // There is a limit on how many UUIDs can be generated for the same time, so if you -// are generating multiple UUIDs, it is recommended to increment the time -// If getTime fails to return the current NewV6 returns Nil and an error. +// are generating multiple UUIDs, it is recommended to increment the time. +// If getTime fails to return the current NewV6WithTime returns Nil and an error. func NewV6WithTime(customTime *time.Time) (UUID, error) { now, seq, err := getTime(customTime) if err != nil { From 7ab986a672ec5a1dd0680b1f2b664f2a4f764990 Mon Sep 17 00:00:00 2001 From: nicumicle <20170987+nicumicleI@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:07:27 +0200 Subject: [PATCH 4/4] fix: remove fmt.Println from test --- time_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/time_test.go b/time_test.go index 7442fa2..46354a4 100644 --- a/time_test.go +++ b/time_test.go @@ -1,7 +1,6 @@ package uuid import ( - "fmt" "testing" "time" ) @@ -24,7 +23,6 @@ func TestGetTime(t *testing.T) { if err != nil { t.Errorf("timeParse unexpected error: %v", err) } - fmt.Println(parsed.Unix()) return &parsed }, expectedTime: 1728984743,