Skip to content

Commit e826b84

Browse files
Add "AtTime" generators for V1, V6, and V7 (#142)
* add "AtTime" generators for V1, V6, and V7 * doc: update doc strings * fix: convenience methods * test: add tests for AtTime methods --------- Co-authored-by: Cameron Ackerman <[email protected]>
1 parent 190948b commit e826b84

File tree

2 files changed

+202
-18
lines changed

2 files changed

+202
-18
lines changed

generator.go

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ func NewV1() (UUID, error) {
5151
return DefaultGenerator.NewV1()
5252
}
5353

54+
// NewV1 returns a UUID based on the provided timestamp and MAC address.
55+
func NewV1AtTime(atTime time.Time) (UUID, error) {
56+
return DefaultGenerator.NewV1AtTime(atTime)
57+
}
58+
5459
// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name.
5560
func NewV3(ns UUID, name string) UUID {
5661
return DefaultGenerator.NewV3(ns, name)
@@ -66,27 +71,45 @@ func NewV5(ns UUID, name string) UUID {
6671
return DefaultGenerator.NewV5(ns, name)
6772
}
6873

69-
// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
74+
// NewV6 returns a k-sortable UUID based on the current timestamp and 48 bits of
7075
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
7176
// order being adjusted to allow the UUID to be k-sortable.
7277
func NewV6() (UUID, error) {
7378
return DefaultGenerator.NewV6()
7479
}
7580

76-
// NewV7 returns a k-sortable UUID based on the current millisecond precision
77-
// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter.
81+
// NewV6 returns a k-sortable UUID based on the provided timestamp and 48 bits of
82+
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
83+
// order being adjusted to allow the UUID to be k-sortable.
84+
func NewV6AtTime(atTime time.Time) (UUID, error) {
85+
return DefaultGenerator.NewV6AtTime(atTime)
86+
}
87+
88+
// NewV7 returns a k-sortable UUID based on the current millisecond-precision
89+
// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch
90+
// generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter.
7891
func NewV7() (UUID, error) {
7992
return DefaultGenerator.NewV7()
8093
}
8194

95+
// NewV7 returns a k-sortable UUID based on the provided millisecond-precision
96+
// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch
97+
// generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter.
98+
func NewV7AtTime(atTime time.Time) (UUID, error) {
99+
return DefaultGenerator.NewV7AtTime(atTime)
100+
}
101+
82102
// Generator provides an interface for generating UUIDs.
83103
type Generator interface {
84104
NewV1() (UUID, error)
105+
NewV1AtTime(time.Time) (UUID, error)
85106
NewV3(ns UUID, name string) UUID
86107
NewV4() (UUID, error)
87108
NewV5(ns UUID, name string) UUID
88109
NewV6() (UUID, error)
110+
NewV6AtTime(time.Time) (UUID, error)
89111
NewV7() (UUID, error)
112+
NewV7AtTime(time.Time) (UUID, error)
90113
}
91114

92115
// Gen is a reference UUID generator based on the specifications laid out in
@@ -211,9 +234,14 @@ func WithRandomReader(reader io.Reader) GenOption {
211234

212235
// NewV1 returns a UUID based on the current timestamp and MAC address.
213236
func (g *Gen) NewV1() (UUID, error) {
237+
return g.NewV1AtTime(g.epochFunc())
238+
}
239+
240+
// NewV1AtTime returns a UUID based on the provided timestamp and current MAC address.
241+
func (g *Gen) NewV1AtTime(atTime time.Time) (UUID, error) {
214242
u := UUID{}
215243

216-
timeNow, clockSeq, err := g.getClockSequence(false)
244+
timeNow, clockSeq, err := g.getClockSequence(false, atTime)
217245
if err != nil {
218246
return Nil, err
219247
}
@@ -264,10 +292,17 @@ func (g *Gen) NewV5(ns UUID, name string) UUID {
264292
return u
265293
}
266294

267-
// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
295+
// NewV6 returns a k-sortable UUID based on the current timestamp and 48 bits of
268296
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
269297
// order being adjusted to allow the UUID to be k-sortable.
270298
func (g *Gen) NewV6() (UUID, error) {
299+
return g.NewV6AtTime(g.epochFunc())
300+
}
301+
302+
// NewV6 returns a k-sortable UUID based on the provided timestamp and 48 bits of
303+
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
304+
// order being adjusted to allow the UUID to be k-sortable.
305+
func (g *Gen) NewV6AtTime(atTime time.Time) (UUID, error) {
271306
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-6
272307
0 1 2 3
273308
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
@@ -282,7 +317,7 @@ func (g *Gen) NewV6() (UUID, error) {
282317
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
283318
var u UUID
284319

285-
timeNow, _, err := g.getClockSequence(false)
320+
timeNow, _, err := g.getClockSequence(false, atTime)
286321
if err != nil {
287322
return Nil, err
288323
}
@@ -306,9 +341,15 @@ func (g *Gen) NewV6() (UUID, error) {
306341
return u, nil
307342
}
308343

309-
// NewV7 returns a k-sortable UUID based on the current millisecond precision
344+
// NewV7 returns a k-sortable UUID based on the current millisecond-precision
310345
// UNIX epoch and 74 bits of pseudorandom data.
311346
func (g *Gen) NewV7() (UUID, error) {
347+
return g.NewV7AtTime(g.epochFunc())
348+
}
349+
350+
// NewV7 returns a k-sortable UUID based on the provided millisecond-precision
351+
// UNIX epoch and 74 bits of pseudorandom data.
352+
func (g *Gen) NewV7AtTime(atTime time.Time) (UUID, error) {
312353
var u UUID
313354
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7
314355
0 1 2 3
@@ -323,7 +364,7 @@ func (g *Gen) NewV7() (UUID, error) {
323364
| rand_b |
324365
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
325366

326-
ms, clockSeq, err := g.getClockSequence(true)
367+
ms, clockSeq, err := g.getClockSequence(true, atTime)
327368
if err != nil {
328369
return Nil, err
329370
}
@@ -355,12 +396,13 @@ func (g *Gen) NewV7() (UUID, error) {
355396
return u, nil
356397
}
357398

358-
// getClockSequence returns the epoch and clock sequence for V1,V6 and V7 UUIDs.
359-
//
360-
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of 100-
399+
// getClockSequence returns the epoch and clock sequence of the provided time,
400+
// used for generating V1,V6 and V7 UUIDs.
361401
//
362-
// nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian reform to the Christian calendar).
363-
func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {
402+
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of
403+
// 100-nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian
404+
// reform to the Christian calendar).
405+
func (g *Gen) getClockSequence(useUnixTSMs bool, atTime time.Time) (uint64, uint16, error) {
364406
var err error
365407
g.clockSequenceOnce.Do(func() {
366408
buf := make([]byte, 2)
@@ -378,9 +420,9 @@ func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {
378420

379421
var timeNow uint64
380422
if useUnixTSMs {
381-
timeNow = uint64(g.epochFunc().UnixMilli())
423+
timeNow = uint64(atTime.UnixMilli())
382424
} else {
383-
timeNow = g.getEpoch()
425+
timeNow = g.getEpoch(atTime)
384426
}
385427
// Clock didn't change since last UUID generation.
386428
// Should increase clock sequence.
@@ -417,9 +459,9 @@ func (g *Gen) getHardwareAddr() ([]byte, error) {
417459
}
418460

419461
// Returns the difference between UUID epoch (October 15, 1582)
420-
// and current time in 100-nanosecond intervals.
421-
func (g *Gen) getEpoch() uint64 {
422-
return epochStart + uint64(g.epochFunc().UnixNano()/100)
462+
// and the provided time in 100-nanosecond intervals.
463+
func (g *Gen) getEpoch(atTime time.Time) uint64 {
464+
return epochStart + uint64(atTime.UnixNano()/100)
423465
}
424466

425467
// Returns the UUID based on the hashing of the namespace UUID and name.

generator_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func testNewV1(t *testing.T) {
5353
t.Run("MissingNetworkWithOptions", testNewV1MissingNetworkWithOptions)
5454
t.Run("MissingNetworkFaultyRand", testNewV1MissingNetworkFaultyRand)
5555
t.Run("MissingNetworkFaultyRandWithOptions", testNewV1MissingNetworkFaultyRandWithOptions)
56+
t.Run("AtSpecificTime", testNewV1AtTime)
5657
}
5758

5859
func TestNewGenWithHWAF(t *testing.T) {
@@ -225,6 +226,53 @@ func testNewV1MissingNetworkFaultyRandWithOptions(t *testing.T) {
225226
}
226227
}
227228

229+
func testNewV1AtTime(t *testing.T) {
230+
atTime := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
231+
232+
u1, err := NewV1AtTime(atTime)
233+
if err != nil {
234+
t.Fatal(err)
235+
}
236+
237+
u2, err := NewV1AtTime(atTime)
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
242+
// Even with the same timestamp, there is still a monotonically increasing portion,
243+
// so they should not be 100% identical. Bytes 0-7 and 10-16 should be identical.
244+
u1Bytes := u1.Bytes()
245+
u2Bytes := u2.Bytes()
246+
binary.BigEndian.PutUint16(u1Bytes[8:], 0)
247+
binary.BigEndian.PutUint16(u2Bytes[8:], 0)
248+
if !bytes.Equal(u1Bytes, u2Bytes) {
249+
t.Errorf("generated different UUIDs across calls with same timestamp: %v / %v", u1, u2)
250+
}
251+
252+
ts1, err := TimestampFromV1(u1)
253+
if err != nil {
254+
t.Fatal(err)
255+
}
256+
time1, err := ts1.Time()
257+
if err != nil {
258+
t.Fatal(err)
259+
}
260+
if time1.Equal(atTime) {
261+
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
262+
}
263+
ts2, err := TimestampFromV1(u2)
264+
if err != nil {
265+
t.Fatal(err)
266+
}
267+
time2, err := ts2.Time()
268+
if err != nil {
269+
t.Fatal(err)
270+
}
271+
if time2.Equal(atTime) {
272+
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
273+
}
274+
}
275+
228276
func testNewV1FaultyRandWithOptions(t *testing.T) {
229277
g := NewGenWithOptions(WithRandomReader(&faultyReader{
230278
readToFail: 0, // fail immediately
@@ -423,6 +471,7 @@ func testNewV6(t *testing.T) {
423471
t.Run("ShortRandomRead", testNewV6ShortRandomRead)
424472
t.Run("ShortRandomReadWithOptions", testNewV6ShortRandomReadWithOptions)
425473
t.Run("KSortable", testNewV6KSortable)
474+
t.Run("AtSpecificTime", testNewV6AtTime)
426475
}
427476

428477
func testNewV6Basic(t *testing.T) {
@@ -601,6 +650,51 @@ func testNewV6KSortable(t *testing.T) {
601650
}
602651
}
603652

653+
func testNewV6AtTime(t *testing.T) {
654+
atTime := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
655+
656+
u1, err := NewV6AtTime(atTime)
657+
if err != nil {
658+
t.Fatal(err)
659+
}
660+
661+
u2, err := NewV6AtTime(atTime)
662+
if err != nil {
663+
t.Fatal(err)
664+
}
665+
666+
// Even with the same timestamp, there is still a random portion,
667+
// so they should not be 100% identical. Bytes 0-8 are the timestamp so they should be identical.
668+
u1Bytes := u1.Bytes()[:8]
669+
u2Bytes := u2.Bytes()[:8]
670+
if !bytes.Equal(u1Bytes, u2Bytes) {
671+
t.Errorf("generated different UUIDs across calls with same timestamp: %v / %v", u1, u2)
672+
}
673+
674+
ts1, err := TimestampFromV6(u1)
675+
if err != nil {
676+
t.Fatal(err)
677+
}
678+
time1, err := ts1.Time()
679+
if err != nil {
680+
t.Fatal(err)
681+
}
682+
if time1.Equal(atTime) {
683+
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
684+
}
685+
ts2, err := TimestampFromV6(u2)
686+
if err != nil {
687+
t.Fatal(err)
688+
}
689+
time2, err := ts2.Time()
690+
if err != nil {
691+
t.Fatal(err)
692+
}
693+
if time2.Equal(atTime) {
694+
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
695+
}
696+
}
697+
604698
func testNewV7(t *testing.T) {
605699
t.Run("Basic", makeTestNewV7Basic())
606700
t.Run("TestVector", makeTestNewV7TestVector())
@@ -614,6 +708,7 @@ func testNewV7(t *testing.T) {
614708
t.Run("ShortRandomReadWithOptions", makeTestNewV7ShortRandomReadWithOptions())
615709
t.Run("KSortable", makeTestNewV7KSortable())
616710
t.Run("ClockSequence", makeTestNewV7ClockSequence())
711+
t.Run("AtSpecificTime", makeTestNewV7AtTime())
617712
}
618713

619714
func makeTestNewV7Basic() func(t *testing.T) {
@@ -861,6 +956,53 @@ func makeTestNewV7ClockSequence() func(t *testing.T) {
861956
}
862957
}
863958

959+
func makeTestNewV7AtTime() func(t *testing.T) {
960+
return func(t *testing.T) {
961+
atTime := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
962+
963+
u1, err := NewV7AtTime(atTime)
964+
if err != nil {
965+
t.Fatal(err)
966+
}
967+
968+
u2, err := NewV7AtTime(atTime)
969+
if err != nil {
970+
t.Fatal(err)
971+
}
972+
973+
// Even with the same timestamp, there is still a random portion,
974+
// so they should not be 100% identical. Bytes 0-6 are the timestamp so they should be identical.
975+
u1Bytes := u1.Bytes()[:7]
976+
u2Bytes := u2.Bytes()[:7]
977+
if !bytes.Equal(u1Bytes, u2Bytes) {
978+
t.Errorf("generated different UUIDs across calls with same timestamp: %v / %v", u1, u2)
979+
}
980+
981+
ts1, err := TimestampFromV7(u1)
982+
if err != nil {
983+
t.Fatal(err)
984+
}
985+
time1, err := ts1.Time()
986+
if err != nil {
987+
t.Fatal(err)
988+
}
989+
if time1.Equal(atTime) {
990+
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
991+
}
992+
ts2, err := TimestampFromV7(u2)
993+
if err != nil {
994+
t.Fatal(err)
995+
}
996+
time2, err := ts2.Time()
997+
if err != nil {
998+
t.Fatal(err)
999+
}
1000+
if time2.Equal(atTime) {
1001+
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
1002+
}
1003+
}
1004+
}
1005+
8641006
func TestDefaultHWAddrFunc(t *testing.T) {
8651007
tests := []struct {
8661008
n string

0 commit comments

Comments
 (0)