Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate uuid V7 base on provided time #179

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,32 @@ func TestVersion7FromReader(t *testing.T) {
}
}

func TestVersion7FromReaderWithTime(t *testing.T) {
myString := "8059ddhdle77cb52"
// other tests affects `lastV7time`, so we need to reset it
lastV7time = 0
timeFunc := func() time.Time {
return time.Date(1992, time.November, 5, 8, 8, 8, 8, time.UTC)
}

id, err := NewV7FromReaderWithTime(bytes.NewReader([]byte(myString)), timeFunc)
if err != nil {
t.Fatal("failed generating UUID from a reader with time")
}
if id.String() != "00a7dc05-8640-7000-ac65-373763623532" {
t.Errorf("expected deterministic result 00a7dc05-8640-7000-ac65-373763623532 got %s", id.String())
}

id, err = NewV7FromReaderWithTime(bytes.NewReader([]byte(myString)), timeFunc)
if err != nil {
t.Fatal("failed generating UUID from a reader with time")
}

if id.String() != "00a7dc05-8640-7001-ac65-373763623532" {
t.Errorf("expected deterministic result on sequential call 00a7dc05-8640-7001-ac65-373763623532 got %s", id.String())
}
}

func TestVersion7Monotonicity(t *testing.T) {
length := 10000
u1 := Must(NewV7()).String()
Expand Down
26 changes: 20 additions & 6 deletions version7.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package uuid

import (
"io"
"time"
)

// UUID version 7 features a time-ordered value field derived from the widely
Expand All @@ -25,7 +26,7 @@ func NewV7() (UUID, error) {
if err != nil {
return uuid, err
}
makeV7(uuid[:])
makeV7(uuid[:], timeNow)
return uuid, nil
}

Expand All @@ -38,14 +39,27 @@ func NewV7FromReader(r io.Reader) (UUID, error) {
return uuid, err
}

makeV7(uuid[:])
makeV7(uuid[:], timeNow)
return uuid, nil
}

// NewV7FromReaderWithTime returns a Version 7 UUID based on the provided time function.
// it uses NewRandomFromReader fill random bits and the provided time function to get the time.
// On error, NewV7FromReader returns Nil and an error.
func NewV7FromReaderWithTime(r io.Reader, timeFunc func() time.Time) (UUID, error) {
uuid, err := NewRandomFromReader(r)
if err != nil {
return uuid, err
}

makeV7(uuid[:], timeFunc)
return uuid, nil
}

// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6])
// uuid[8] already has the right version number (Variant is 10)
// see function NewV7 and NewV7FromReader
func makeV7(uuid []byte) {
func makeV7(uuid []byte, timeFunc func() time.Time) {
/*
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
Expand All @@ -61,7 +75,7 @@ func makeV7(uuid []byte) {
*/
_ = uuid[15] // bounds check

t, s := getV7Time()
t, s := getV7Time(timeFunc)

uuid[0] = byte(t >> 40)
uuid[1] = byte(t >> 32)
Expand All @@ -85,11 +99,11 @@ const nanoPerMilli = 1000000
// getV7Time returns the time in milliseconds and nanoseconds / 256.
// The returned (milli << 12 + seq) is guaranteed to be greater than
// (milli << 12 + seq) returned by any previous call to getV7Time.
func getV7Time() (milli, seq int64) {
func getV7Time(timeFunc func() time.Time) (milli, seq int64) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the behavior later in this function with lastV7time I don't think we want that when using a custom timestamp. Basically if you pass in a future time, then a past time, the past time will get reset to be in the future. I opened #182 that doesn't have this issue.

timeMu.Lock()
defer timeMu.Unlock()

nano := timeNow().UnixNano()
nano := timeFunc().UnixNano()
milli = nano / nanoPerMilli
// Sequence number is between 0 and 3906 (nanoPerMilli>>8)
seq = (nano - milli*nanoPerMilli) >> 8
Expand Down