Skip to content

Commit 142fcab

Browse files
authored
Merge pull request #199 from kohenkatz/fixes_195
Proper timestamp calculation for UUID v7
2 parents 7d51018 + 240f296 commit 142fcab

File tree

3 files changed

+160
-5
lines changed

3 files changed

+160
-5
lines changed

generator_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,16 @@ func BenchmarkGenerator(b *testing.B) {
11211121
NewV5(NamespaceDNS, "www.example.com")
11221122
}
11231123
})
1124+
b.Run("NewV6", func(b *testing.B) {
1125+
for i := 0; i < b.N; i++ {
1126+
NewV6()
1127+
}
1128+
})
1129+
b.Run("NewV7", func(b *testing.B) {
1130+
for i := 0; i < b.N; i++ {
1131+
NewV7()
1132+
}
1133+
})
11241134
}
11251135

11261136
type faultyReader struct {

uuid.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@ const (
8282
)
8383

8484
// Timestamp is the count of 100-nanosecond intervals since 00:00:00.00,
85-
// 15 October 1582 within a V1 UUID. This type has no meaning for other
86-
// UUID versions since they don't have an embedded timestamp.
85+
// 15 October 1582 within a V1 or V6 UUID, or as a common intermediate
86+
// representation of the (Unix Millisecond) timestamp within a V7 UUID.
87+
// This type has no meaning for other UUID versions since they don't
88+
// have an embedded timestamp.
8789
type Timestamp uint64
8890

8991
const _100nsPerSecond = 10000000
@@ -144,8 +146,11 @@ func TimestampFromV7(u UUID) (Timestamp, error) {
144146
(int64(u[4]) << 8) |
145147
int64(u[5])
146148

147-
// convert to format expected by Timestamp
148-
tsNanos := epochStart + time.UnixMilli(t).UTC().UnixNano()/100
149+
// UUIDv7 stores MS since 1970-01-01 00:00:00, but the Timestamp
150+
// type stores 100-nanosecond increments since 1582-10-15 00:00:00.
151+
// This conversion multiplies ms by 10,000 to get 100-ns chunks and adds
152+
// the difference between October 1582 and January 1970.
153+
tsNanos := epochStart + (t * 10000)
149154
return Timestamp(tsNanos), nil
150155
}
151156

uuid_test.go

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,14 @@ func TestTimestampFromV7(t *testing.T) {
262262
want Timestamp
263263
wanterr bool
264264
}{
265+
// These non-V7 versions should not be able to be provided to TimestampFromV7
265266
{u: Must(NewV1()), wanterr: true},
267+
{u: NewV3(NamespaceDNS, "a.example.com"), wanterr: true},
266268
// v7 is unix_ts_ms, so zero value time is unix epoch
267269
{u: Must(FromString("00000000-0000-7000-0000-000000000000")), want: 122192928000000000},
268270
{u: Must(FromString("018a8fec-3ced-7164-995f-93c80cbdc575")), want: 139139245386050000},
269-
{u: Must(FromString("ffffffff-ffff-7fff-ffff-ffffffffffff")), want: Timestamp(epochStart + time.UnixMilli((1<<48)-1).UTC().UnixNano()/100)},
271+
// Calculated as `(1<<48)-1` milliseconds, times 100 ns per ms, plus epoch offset from 1970 to 1582.
272+
{u: Must(FromString("ffffffff-ffff-7fff-bfff-ffffffffffff")), want: 2936942695106550000},
270273
}
271274
for _, tt := range tests {
272275
got, err := TimestampFromV7(tt.u)
@@ -281,6 +284,56 @@ func TestTimestampFromV7(t *testing.T) {
281284
}
282285
}
283286

287+
func TestMinMaxTimestamps(t *testing.T) {
288+
tests := []struct {
289+
u UUID
290+
want time.Time
291+
}{
292+
293+
// v1 min and max
294+
{u: Must(FromString("00000000-0000-1000-8000-000000000000")), want: time.Date(1582, 10, 15, 0, 0, 0, 0, time.UTC)}, //1582-10-15 0:00:00 (UTC)
295+
{u: Must(FromString("ffffffff-ffff-1fff-bfff-ffffffffffff")), want: time.Date(5236, 3, 31, 21, 21, 00, 684697500, time.UTC)}, //5236-03-31 21:21:00 (UTC)
296+
297+
// v6 min and max
298+
{u: Must(FromString("00000000-0000-6000-8000-000000000000")), want: time.Date(1582, 10, 15, 0, 0, 0, 0, time.UTC)}, //1582-10-15 0:00:00 (UTC)
299+
{u: Must(FromString("ffffffff-ffff-6fff-bfff-ffffffffffff")), want: time.Date(5236, 3, 31, 21, 21, 00, 684697500, time.UTC)}, //5236-03-31 21:21:00 (UTC)
300+
301+
// v7 min and max
302+
{u: Must(FromString("00000000-0000-7000-8000-000000000000")), want: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, //1970-01-01 0:00:00 (UTC)
303+
{u: Must(FromString("ffffffff-ffff-7fff-bfff-ffffffffffff")), want: time.Date(10889, 8, 2, 5, 31, 50, 655000000, time.UTC)}, //10889-08-02 5:31:50.655 (UTC)
304+
}
305+
for _, tt := range tests {
306+
var got Timestamp
307+
var err error
308+
var functionName string
309+
310+
switch tt.u.Version() {
311+
case V1:
312+
functionName = "TimestampFromV1"
313+
got, err = TimestampFromV1(tt.u)
314+
case V6:
315+
functionName = "TimestampFromV6"
316+
got, err = TimestampFromV6(tt.u)
317+
case V7:
318+
functionName = "TimestampFromV7"
319+
got, err = TimestampFromV7(tt.u)
320+
}
321+
322+
if err != nil {
323+
t.Errorf(functionName+"(%v) got error %v, want %v", tt.u, err, tt.want)
324+
}
325+
326+
tm, err := got.Time()
327+
if err != nil {
328+
t.Errorf(functionName+"(%v) got error %v, want %v", tt.u, err, tt.want)
329+
}
330+
331+
if !tt.want.Equal(tm) {
332+
t.Errorf(functionName+"(%v) got %v, want %v", tt.u, tm.UTC(), tt.want)
333+
}
334+
}
335+
}
336+
284337
func BenchmarkFormat(b *testing.B) {
285338
var tests = []string{
286339
"%s",
@@ -300,3 +353,90 @@ func BenchmarkFormat(b *testing.B) {
300353
})
301354
}
302355
}
356+
357+
var uuidBenchmarkSink UUID
358+
var timestampBenchmarkSink Timestamp
359+
var timeBenchmarkSink time.Time
360+
361+
func BenchmarkTimestampFrom(b *testing.B) {
362+
var err error
363+
numbUUIDs := 1000
364+
if testing.Short() {
365+
numbUUIDs = 10
366+
}
367+
368+
funcs := []struct {
369+
name string
370+
create func() (UUID, error)
371+
timestamp func(UUID) (Timestamp, error)
372+
}{
373+
{"v1", NewV1, TimestampFromV1},
374+
{"v6", NewV6, TimestampFromV6},
375+
{"v7", NewV7, TimestampFromV7},
376+
}
377+
378+
for _, fns := range funcs {
379+
b.Run(fns.name, func(b *testing.B) {
380+
// Make sure we don't just encode the same string over and over again as that will hit memory caches unrealistically
381+
uuids := make([]UUID, numbUUIDs)
382+
for i := 0; i < numbUUIDs; i++ {
383+
uuids[i] = Must(fns.create())
384+
if !testing.Short() {
385+
time.Sleep(1 * time.Millisecond)
386+
}
387+
}
388+
b.ResetTimer()
389+
for i := 0; i < b.N; i++ {
390+
timestampBenchmarkSink, err = fns.timestamp(uuids[i%numbUUIDs])
391+
392+
if err != nil {
393+
b.Fatal(err)
394+
}
395+
}
396+
})
397+
}
398+
}
399+
400+
func BenchmarkTimestampTime(b *testing.B) {
401+
var err error
402+
numbUUIDs := 1000
403+
if testing.Short() {
404+
numbUUIDs = 10
405+
}
406+
407+
funcs := []struct {
408+
name string
409+
create func() (UUID, error)
410+
timestamp func(UUID) (Timestamp, error)
411+
}{
412+
{"v1", NewV1, TimestampFromV1},
413+
{"v6", NewV6, TimestampFromV6},
414+
{"v7", NewV7, TimestampFromV7},
415+
}
416+
417+
for _, fns := range funcs {
418+
b.Run(fns.name, func(b *testing.B) {
419+
// Make sure we don't just encode the same string over and over again as that will hit memory caches unrealistically
420+
uuids := make([]UUID, numbUUIDs)
421+
timestamps := make([]Timestamp, numbUUIDs)
422+
for i := 0; i < numbUUIDs; i++ {
423+
uuids[i] = Must(fns.create())
424+
timestamps[i], err = fns.timestamp(uuids[i])
425+
if err != nil {
426+
b.Fatal(err)
427+
}
428+
if !testing.Short() {
429+
time.Sleep(1 * time.Millisecond)
430+
}
431+
}
432+
b.ResetTimer()
433+
for i := 0; i < b.N; i++ {
434+
timeBenchmarkSink, err = timestamps[i%numbUUIDs].Time()
435+
if err != nil {
436+
b.Fatal(err)
437+
}
438+
}
439+
})
440+
}
441+
442+
}

0 commit comments

Comments
 (0)