From 6049c2e86450464d6bfdbbdb4fa2b4d64912ca01 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Mon, 6 Apr 2026 16:03:06 +0700 Subject: lib/uuidv7: remove the v7 suffix from type Adding suffix version to the type seems not right (and also mouthful to read) since the package already defines the version of UUID. --- lib/uuidv7/example_test.go | 162 ++++++++++++++++++++++++++ lib/uuidv7/uuid.go | 238 ++++++++++++++++++++++++++++++++++++++ lib/uuidv7/uuid_test.go | 115 ++++++++++++++++++ lib/uuidv7/uuidv7.go | 238 -------------------------------------- lib/uuidv7/uuidv7_example_test.go | 162 -------------------------- lib/uuidv7/uuidv7_test.go | 115 ------------------ 6 files changed, 515 insertions(+), 515 deletions(-) create mode 100644 lib/uuidv7/example_test.go create mode 100644 lib/uuidv7/uuid.go create mode 100644 lib/uuidv7/uuid_test.go delete mode 100644 lib/uuidv7/uuidv7.go delete mode 100644 lib/uuidv7/uuidv7_example_test.go delete mode 100644 lib/uuidv7/uuidv7_test.go diff --git a/lib/uuidv7/example_test.go b/lib/uuidv7/example_test.go new file mode 100644 index 00000000..86ae7aab --- /dev/null +++ b/lib/uuidv7/example_test.go @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2026 M. Shulhan + +package uuidv7_test + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "log" + "time" + + "git.sr.ht/~shulhan/pakakeh.go/lib/uuidv7" +) + +func ExampleGenerate() { + // Begin mocking Now and Rand, DO NOT USE in production code. + now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC) + uuidv7.Now = func() time.Time { + now = now.Add(time.Second) + return now + } + uuidv7.Rand = func() uint64 { + return 0xFFFF_FFFF_FFFF_FFFF + } + + id := uuidv7.Generate() + fmt.Printf("%s %s\n", id, id.Time().UTC()) + id = uuidv7.Generate() + fmt.Printf("%s %s\n", id, id.Time().UTC()) + + // Output: + // 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:01.123 +0000 UTC + // 019CD2F8-1ECB-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:02.123 +0000 UTC +} + +func ExampleParse() { + id := uuidv7.Parse(``) + fmt.Printf("%t %s %s\n", id.IsZero(), id, id.Time().UTC()) + + id = uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF`) + fmt.Printf("%t %s %s\n", id.IsZero(), id, id.Time().UTC()) + + // Output: + // true 00000000-0000-0000-0000-000000000000 1970-01-01 00:00:00 +0000 UTC + // false 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:01.123 +0000 UTC +} + +func ExampleUUID_Equal() { + id := uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF`) + id2 := uuidv7.Parse(`019CD2F8-2AE3-774E-BFFF-FFFFFFFFFFFF`) + + err := id.Equal(id) + fmt.Printf("%v\n", err) + + err = id.Equal(&id) + fmt.Printf("%v\n", err) + + err = id.Equal(&id2) + fmt.Printf("%v\n", err) + + // Output: + // + // + // uuidv7: not equal, want 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF, got 019CD2F8-2AE3-774E-BFFF-FFFFFFFFFFFF +} + +func ExampleUUID_IsEqual() { + id := uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF`) + other := uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFF0`) + + fmt.Println(id.IsEqual(id)) + fmt.Println(id.IsEqual(other)) + + // Output: + // true + // false +} + +func ExampleUUID_MarshalBinary() { + now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC) + uuidv7.Now = func() time.Time { + now = now.Add(time.Second) + return now + } + uuidv7.Rand = func() uint64 { + return 0xFFFF_FFFF_FFFF_FFFF + } + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + id := uuidv7.Generate() + err := enc.Encode(id) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Sent: %s\n", id) + + dec := gob.NewDecoder(&buf) + var gotID uuidv7.UUID + err = dec.Decode(&gotID) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Receive: %s\n", gotID) + + // Output: + // Sent: 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF + // Receive: 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF +} + +func ExampleUUID_MarshalText() { + now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC) + uuidv7.Now = func() time.Time { + now = now.Add(time.Second) + return now + } + uuidv7.Rand = func() uint64 { + return 0xFFFF_FFFF_FFFF_FFFF + } + + type T struct { + ID uuidv7.UUID + } + + t := T{ + ID: uuidv7.Generate(), + } + jsonb, err := json.Marshal(t) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", jsonb) + + var got T + err = json.Unmarshal(jsonb, &got) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%+v\n", got) + + // Output: + // {"ID":"019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF"} + // {ID:019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF} +} + +func ExampleUUID_Scan() { + var id uuidv7.UUID + + // Scan the value from the database: + // + // dbc.QueryRow(`SELECT id ...`, &id) + + err := id.Scan([]byte(`017F22E2-79B0-7CC3-98C4-DC0C0C07398F`)) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", id) + // Output: + // 017F22E2-79B0-7CC3-98C4-DC0C0C07398F +} diff --git a/lib/uuidv7/uuid.go b/lib/uuidv7/uuid.go new file mode 100644 index 00000000..203b41cf --- /dev/null +++ b/lib/uuidv7/uuid.go @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2026 M. Shulhan + +// Package uuidv7 implements UUID version 7 as defined in RFC 9562. +package uuidv7 + +import ( + "crypto/rand" + "database/sql/driver" + "encoding/binary" + "encoding/hex" + "fmt" + "time" +) + +const hexTable = `0123456789ABCDEF` +const variantMask uint64 = 0x8000_0000_0000_0000 + +// Now defines the function variable that return the current time in UTC. +// This identifier is exported to simplify testing. +var Now = func() time.Time { + return time.Now().UTC() +} + +// Rand defines the function variable that return a random uint64. +// This identifier is exported to simplify testing. +var Rand = func() (v uint64) { + b := make([]byte, 8) + rand.Read(b) + v, _ = binary.Uvarint(b) + return v +} + +// UUID defines the container for UUID version 7 that satisfy the +// [database/sql], [encoding/gob], and [encoding/json]. +type UUID struct { + // The high 8 octets store the unix timestamp in milliseconds (6 + // octets), the version (4 bits), and clock precision (12 bits). + high uint64 + + // The low 8 octets store the variant (2 bits) and random number (62 + // bits). + low uint64 +} + +// Generate generates new UUID version 7. +func Generate() (id UUID) { + now := Now() + generate(&now, &id) + return id +} + +func generate(t *time.Time, id *UUID) { + unixns := uint64(t.UnixNano()) // 1773066002123456700 + millis := uint64(unixns / 1e6) // 1773066002123 + id.high = millis << 16 // 0x019c_d2f8_1ecb_0000 + id.high |= 0x7000 // 0x019c_d2f8_1ecb_7000 + prec := unixns - (millis * 1e6) // 456700 + clockPrec := uint64((prec * 4096) / 1e6) // 1870 + id.high |= clockPrec // 0x019c_d2f8_1ecb_774e + rand := Rand() // 0xffff_ffff_ffff_ffff + rand >>= 2 // 0x3fff_ffff_ffff_ffff + id.low = variantMask | rand // 0xbfff_ffff_ffff_ffff +} + +// Parse parses the UUID formated string and return it as non-zero id. +// If the string s is invalid UUID it will return zero id. +func Parse(s string) (id UUID) { + id.UnmarshalText([]byte(s)) + return id +} + +// Bytes returns id as a stream of binary. +func (id UUID) Bytes() (data []byte) { + data = make([]byte, 0, binary.MaxVarintLen64*2) + data = binary.BigEndian.AppendUint64(data, id.high) + data = binary.BigEndian.AppendUint64(data, id.low) + return data +} + +// Equal returns nil if id and v are equals. +// This method implements [git.sr.ht/~shulhan/pakakeh.go/lib/reflect.Equaler] +// interface. +func (id *UUID) Equal(v any) (err error) { + ptr, ok := v.(*UUID) + if !ok { + other, ok := v.(UUID) + if !ok { + return fmt.Errorf(`uuidv7: Equal: want type %T, got %T`, &id, v) + } + ptr = &other + } + if id.high != ptr.high { + return fmt.Errorf(`uuidv7: not equal, want %s, got %s`, id.String(), ptr.String()) + } + if id.low != ptr.low { + return fmt.Errorf(`uuidv7: not equal, want %s, got %s`, id.String(), ptr.String()) + } + return nil +} + +// IsEqual returns true if id equal with other. +func (id UUID) IsEqual(other UUID) bool { + return id.high == other.high && id.low == other.low +} + +// IsZero returns true if all bits is zero. +func (id UUID) IsZero() bool { + return id.high == 0 && id.low == 0 +} + +// MarshalBinary encodes the id to binary for [encoding/gob]. +func (id UUID) MarshalBinary() (data []byte, err error) { + data = id.Bytes() + return data, nil +} + +// MarshalText encodes the id to JSON for [encoding/json]. +func (id UUID) MarshalText() (data []byte, err error) { + v := id.String() + return []byte(v), nil +} + +// UnmarshalBinary decodes the data into id for [encoding/gob]. +func (id *UUID) UnmarshalBinary(data []byte) (err error) { + if len(data) != 16 { + return fmt.Errorf(`uuidv7: invalid length %d`, len(data)) + } + ver := data[6] >> 4 + if ver != 7 { + return fmt.Errorf(`uuidv7: invalid version %d`, ver) + } + id.high = binary.BigEndian.Uint64(data[:8]) + id.low = binary.BigEndian.Uint64(data[8:]) + return nil +} + +// UnmarshalText decodes the JSON data into id for [encoding/json]. +func (id *UUID) UnmarshalText(data []byte) (err error) { + src := make([]byte, 0, 32) + for _, c := range data { + if c == '-' { + continue + } + src = append(src, c) + } + l := len(src) + if l != 32 { + return fmt.Errorf(`uuidv7: invalid length %d`, l) + } + if src[12] != '7' { + return fmt.Errorf(`uuidv7: invalid version %c`, data[12]) + } + + l = hex.DecodedLen(len(src[:16])) + dst := make([]byte, l) + _, err = hex.Decode(dst, src[:16]) + if err != nil { + return fmt.Errorf(`uuidv7: %w`, err) + } + high := binary.BigEndian.Uint64(dst) + + _, err = hex.Decode(dst, src[16:]) + if err != nil { + return fmt.Errorf(`uuidv7: %w`, err) + } + id.high = high + id.low = binary.BigEndian.Uint64(dst) + return nil +} + +// Scan scans the raw database value into id. +// This method implement [database/sql.Scanner] interface. +// Column with NULL value will returns no error set it to zero UUID. +func (id *UUID) Scan(src any) (err error) { + switch v := src.(type) { + case []byte: + err = id.UnmarshalText(v) + if err != nil { + return err + } + case nil: + id.high = 0 + id.low = 0 + return nil + default: + return fmt.Errorf(`uuidv7: Scan: invalid type %T`, src) + } + return nil +} + +// String returns the human readable format of UUID in the form of +// "0xxxxxxx-xxxx-7xxx-8xxx-xxxxxxxxxxxx". +func (id UUID) String() string { + buf := make([]byte, 36) + sw := 56 + x := 0 + for sw >= 0 { + b := byte(id.high >> sw) + sw -= 8 + buf[x] = hexTable[b>>4] + buf[x+1] = hexTable[b&0x0F] + x += 2 + if x == 8 || x == 13 { + buf[x] = '-' + x++ + } + } + buf[x] = '-' + x++ + sw = 56 + for sw >= 0 { + b := byte(id.low >> sw) + sw -= 8 + buf[x] = hexTable[b>>4] + buf[x+1] = hexTable[b&0x0F] + x += 2 + if x == 23 { + buf[x] = '-' + x++ + } + } + return string(buf) +} + +// Time returns the Unix epoch timestamp in milliseconds stored in the UUID. +func (id *UUID) Time() time.Time { + millis := id.high >> 16 + t := time.UnixMilli(int64(millis)) + return t +} + +// Value returns the value for sending it to the database. +// This method implements the [driver.Valuer] interface. +func (id UUID) Value() (v driver.Value, err error) { + v = id.String() + return v, nil +} diff --git a/lib/uuidv7/uuid_test.go b/lib/uuidv7/uuid_test.go new file mode 100644 index 00000000..126945d3 --- /dev/null +++ b/lib/uuidv7/uuid_test.go @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2026 M. Shulhan + +package uuidv7 + +import ( + "testing" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test" +) + +func TestUUID_Scan(t *testing.T) { + id := Parse(`019B76DA-A800-7000-8000-00000000001A`) + + listCase := []struct { + desc string + data any + expError string + exp string + }{{ + desc: `With empty data`, + data: `017F22E2-79B0-7CC3-98C4-DC0C0C07398F`, + expError: `uuidv7: Scan: invalid type string`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }, { + desc: `With invalid version`, + data: []byte(`5c146b14-3c52-4afd-938a-375d0df1fbf6`), + expError: `uuidv7: invalid version 2`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }, { + desc: `With nil`, + data: nil, + expError: ``, + exp: `00000000-0000-0000-0000-000000000000`, + }} + for _, tc := range listCase { + err := id.Scan(tc.data) + if err != nil { + test.Assert(t, tc.desc, tc.expError, err.Error()) + } + test.Assert(t, tc.desc, tc.exp, id.String()) + } +} + +func TestUUID_UnmarshalBinary(t *testing.T) { + id := Parse(`019B76DA-A800-7000-8000-00000000001A`) + + listCase := []struct { + desc string + expError string + exp string + data []byte + }{{ + desc: `With empty data`, + expError: `uuidv7: invalid length 0`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }, { + desc: `With non-version 7`, + data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 1, 2, 3, 4}, + expError: `uuidv7: invalid version 0`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }, { + desc: `OK`, + data: []byte{0xf, 0xf, 0xf, 0xf, + 0xf, 0xf, + 0x70, 0xf, + 0xf, 0xf, + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf}, + exp: `0F0F0F0F-0F0F-700F-0F0F-0F0F0F0F0F0F`, + }} + for _, tc := range listCase { + err := id.UnmarshalBinary(tc.data) + if err != nil { + test.Assert(t, tc.desc, tc.expError, err.Error()) + } + test.Assert(t, tc.desc, tc.exp, id.String()) + } +} + +func TestUUID_UnmarshalText(t *testing.T) { + id := Parse(`019B76DA-A800-7000-8000-00000000001A`) + + listCase := []struct { + desc string + expError string + exp string + data []byte + }{{ + desc: `With empty data`, + expError: `uuidv7: invalid length 0`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }, { + desc: `With non-version 7`, + data: []byte(`5c146b14-3c52-4afd-938a-375d0df1fbf6`), + expError: `uuidv7: invalid version 2`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }, { + desc: `With invalid hex (high)`, + data: []byte(`X17F22E2-79B0-7CC3-98C4-DC0C0C07398F`), + expError: `uuidv7: encoding/hex: invalid byte: U+0058 'X'`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }, { + desc: `With invalid hex (low)`, + data: []byte(`017F22E2-79B0-7CC3-98C4-DC0C0C07398X`), + expError: `uuidv7: encoding/hex: invalid byte: U+0058 'X'`, + exp: `019B76DA-A800-7000-8000-00000000001A`, + }} + for _, tc := range listCase { + err := id.UnmarshalText(tc.data) + if err != nil { + test.Assert(t, tc.desc, tc.expError, err.Error()) + } + test.Assert(t, tc.desc, tc.exp, id.String()) + } +} diff --git a/lib/uuidv7/uuidv7.go b/lib/uuidv7/uuidv7.go deleted file mode 100644 index 5aac01bb..00000000 --- a/lib/uuidv7/uuidv7.go +++ /dev/null @@ -1,238 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-FileCopyrightText: 2026 M. Shulhan - -// Package uuidv7 implements UUID version 7 as defined in RFC 9562. -package uuidv7 - -import ( - "crypto/rand" - "database/sql/driver" - "encoding/binary" - "encoding/hex" - "fmt" - "time" -) - -const hexTable = `0123456789ABCDEF` -const variantMask uint64 = 0x8000_0000_0000_0000 - -// Now defines the function variable that return the current time in UTC. -// This identifier is exported to simplify testing. -var Now = func() time.Time { - return time.Now().UTC() -} - -// Rand defines the function variable that return a random uint64. -// This identifier is exported to simplify testing. -var Rand = func() (v uint64) { - b := make([]byte, 8) - rand.Read(b) - v, _ = binary.Uvarint(b) - return v -} - -// UUIDv7 defines the container for UUID version 7 that satisfy the -// [database/sql], [encoding/gob], and [encoding/json]. -type UUIDv7 struct { - // The high 8 octets store the unix timestamp in milliseconds (6 - // octets), the version (4 bits), and clock precision (12 bits). - high uint64 - - // The low 8 octets store the variant (2 bits) and random number (62 - // bits). - low uint64 -} - -// Generate generates new UUID version 7. -func Generate() (id UUIDv7) { - now := Now() - generate(&now, &id) - return id -} - -func generate(t *time.Time, id *UUIDv7) { - unixns := uint64(t.UnixNano()) // 1773066002123456700 - millis := uint64(unixns / 1e6) // 1773066002123 - id.high = millis << 16 // 0x019c_d2f8_1ecb_0000 - id.high |= 0x7000 // 0x019c_d2f8_1ecb_7000 - prec := unixns - (millis * 1e6) // 456700 - clockPrec := uint64((prec * 4096) / 1e6) // 1870 - id.high |= clockPrec // 0x019c_d2f8_1ecb_774e - rand := Rand() // 0xffff_ffff_ffff_ffff - rand >>= 2 // 0x3fff_ffff_ffff_ffff - id.low = variantMask | rand // 0xbfff_ffff_ffff_ffff -} - -// Parse parses the UUIDv7 formated string and return it as non-zero id. -// If the string s is invalid UUIDv7 it will return zero id. -func Parse(s string) (id UUIDv7) { - id.UnmarshalText([]byte(s)) - return id -} - -// Bytes returns id as a stream of binary. -func (id UUIDv7) Bytes() (data []byte) { - data = make([]byte, 0, binary.MaxVarintLen64*2) - data = binary.BigEndian.AppendUint64(data, id.high) - data = binary.BigEndian.AppendUint64(data, id.low) - return data -} - -// Equal returns nil if id and v are equals. -// This method implements [git.sr.ht/~shulhan/pakakeh.go/lib/reflect.Equaler] -// interface. -func (id *UUIDv7) Equal(v any) (err error) { - ptr, ok := v.(*UUIDv7) - if !ok { - other, ok := v.(UUIDv7) - if !ok { - return fmt.Errorf(`uuidv7: Equal: want type %T, got %T`, &id, v) - } - ptr = &other - } - if id.high != ptr.high { - return fmt.Errorf(`uuidv7: not equal, want %s, got %s`, id.String(), ptr.String()) - } - if id.low != ptr.low { - return fmt.Errorf(`uuidv7: not equal, want %s, got %s`, id.String(), ptr.String()) - } - return nil -} - -// IsEqual returns true if id equal with other. -func (id UUIDv7) IsEqual(other UUIDv7) bool { - return id.high == other.high && id.low == other.low -} - -// IsZero returns true if all bits is zero. -func (id UUIDv7) IsZero() bool { - return id.high == 0 && id.low == 0 -} - -// MarshalBinary encodes the id to binary for [encoding/gob]. -func (id UUIDv7) MarshalBinary() (data []byte, err error) { - data = id.Bytes() - return data, nil -} - -// MarshalText encodes the id to JSON for [encoding/json]. -func (id UUIDv7) MarshalText() (data []byte, err error) { - v := id.String() - return []byte(v), nil -} - -// UnmarshalBinary decodes the data into id for [encoding/gob]. -func (id *UUIDv7) UnmarshalBinary(data []byte) (err error) { - if len(data) != 16 { - return fmt.Errorf(`uuidv7: invalid length %d`, len(data)) - } - ver := data[6] >> 4 - if ver != 7 { - return fmt.Errorf(`uuidv7: invalid version %d`, ver) - } - id.high = binary.BigEndian.Uint64(data[:8]) - id.low = binary.BigEndian.Uint64(data[8:]) - return nil -} - -// UnmarshalText decodes the JSON data into id for [encoding/json]. -func (id *UUIDv7) UnmarshalText(data []byte) (err error) { - src := make([]byte, 0, 32) - for _, c := range data { - if c == '-' { - continue - } - src = append(src, c) - } - l := len(src) - if l != 32 { - return fmt.Errorf(`uuidv7: invalid length %d`, l) - } - if src[12] != '7' { - return fmt.Errorf(`uuidv7: invalid version %c`, data[12]) - } - - l = hex.DecodedLen(len(src[:16])) - dst := make([]byte, l) - _, err = hex.Decode(dst, src[:16]) - if err != nil { - return fmt.Errorf(`uuidv7: %w`, err) - } - high := binary.BigEndian.Uint64(dst) - - _, err = hex.Decode(dst, src[16:]) - if err != nil { - return fmt.Errorf(`uuidv7: %w`, err) - } - id.high = high - id.low = binary.BigEndian.Uint64(dst) - return nil -} - -// Scan scans the raw database value into id. -// This method implement [database/sql.Scanner] interface. -// Column with NULL value will returns no error set it to zero UUID. -func (id *UUIDv7) Scan(src any) (err error) { - switch v := src.(type) { - case []byte: - err = id.UnmarshalText(v) - if err != nil { - return err - } - case nil: - id.high = 0 - id.low = 0 - return nil - default: - return fmt.Errorf(`uuidv7: Scan: invalid type %T`, src) - } - return nil -} - -// String returns the human readable format of UUIDv7 in the form of -// "0xxxxxxx-xxxx-7xxx-8xxx-xxxxxxxxxxxx". -func (id UUIDv7) String() string { - buf := make([]byte, 36) - sw := 56 - x := 0 - for sw >= 0 { - b := byte(id.high >> sw) - sw -= 8 - buf[x] = hexTable[b>>4] - buf[x+1] = hexTable[b&0x0F] - x += 2 - if x == 8 || x == 13 { - buf[x] = '-' - x++ - } - } - buf[x] = '-' - x++ - sw = 56 - for sw >= 0 { - b := byte(id.low >> sw) - sw -= 8 - buf[x] = hexTable[b>>4] - buf[x+1] = hexTable[b&0x0F] - x += 2 - if x == 23 { - buf[x] = '-' - x++ - } - } - return string(buf) -} - -// Time returns the Unix epoch timestamp in milliseconds stored in the UUIDv7. -func (id *UUIDv7) Time() time.Time { - millis := id.high >> 16 - t := time.UnixMilli(int64(millis)) - return t -} - -// Value returns the value for sending it to the database. -// This method implements the [driver.Valuer] interface. -func (id UUIDv7) Value() (v driver.Value, err error) { - v = id.String() - return v, nil -} diff --git a/lib/uuidv7/uuidv7_example_test.go b/lib/uuidv7/uuidv7_example_test.go deleted file mode 100644 index 5a71d69e..00000000 --- a/lib/uuidv7/uuidv7_example_test.go +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-FileCopyrightText: 2026 M. Shulhan - -package uuidv7_test - -import ( - "bytes" - "encoding/gob" - "encoding/json" - "fmt" - "log" - "time" - - "git.sr.ht/~shulhan/pakakeh.go/lib/uuidv7" -) - -func ExampleGenerate() { - // Begin mocking Now and Rand, DO NOT USE in production code. - now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC) - uuidv7.Now = func() time.Time { - now = now.Add(time.Second) - return now - } - uuidv7.Rand = func() uint64 { - return 0xFFFF_FFFF_FFFF_FFFF - } - - id := uuidv7.Generate() - fmt.Printf("%s %s\n", id, id.Time().UTC()) - id = uuidv7.Generate() - fmt.Printf("%s %s\n", id, id.Time().UTC()) - - // Output: - // 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:01.123 +0000 UTC - // 019CD2F8-1ECB-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:02.123 +0000 UTC -} - -func ExampleParse() { - id := uuidv7.Parse(``) - fmt.Printf("%t %s %s\n", id.IsZero(), id, id.Time().UTC()) - - id = uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF`) - fmt.Printf("%t %s %s\n", id.IsZero(), id, id.Time().UTC()) - - // Output: - // true 00000000-0000-0000-0000-000000000000 1970-01-01 00:00:00 +0000 UTC - // false 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF 2026-03-09 14:20:01.123 +0000 UTC -} - -func ExampleUUIDv7_Equal() { - id := uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF`) - id2 := uuidv7.Parse(`019CD2F8-2AE3-774E-BFFF-FFFFFFFFFFFF`) - - err := id.Equal(id) - fmt.Printf("%v\n", err) - - err = id.Equal(&id) - fmt.Printf("%v\n", err) - - err = id.Equal(&id2) - fmt.Printf("%v\n", err) - - // Output: - // - // - // uuidv7: not equal, want 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF, got 019CD2F8-2AE3-774E-BFFF-FFFFFFFFFFFF -} - -func ExampleUUIDv7_IsEqual() { - id := uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF`) - other := uuidv7.Parse(`019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFF0`) - - fmt.Println(id.IsEqual(id)) - fmt.Println(id.IsEqual(other)) - - // Output: - // true - // false -} - -func ExampleUUIDv7_MarshalBinary() { - now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC) - uuidv7.Now = func() time.Time { - now = now.Add(time.Second) - return now - } - uuidv7.Rand = func() uint64 { - return 0xFFFF_FFFF_FFFF_FFFF - } - - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - id := uuidv7.Generate() - err := enc.Encode(id) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Sent: %s\n", id) - - dec := gob.NewDecoder(&buf) - var gotID uuidv7.UUIDv7 - err = dec.Decode(&gotID) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Receive: %s\n", gotID) - - // Output: - // Sent: 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF - // Receive: 019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF -} - -func ExampleUUIDv7_MarshalText() { - now := time.Date(2026, 3, 9, 14, 20, 0, 123456700, time.UTC) - uuidv7.Now = func() time.Time { - now = now.Add(time.Second) - return now - } - uuidv7.Rand = func() uint64 { - return 0xFFFF_FFFF_FFFF_FFFF - } - - type T struct { - ID uuidv7.UUIDv7 - } - - t := T{ - ID: uuidv7.Generate(), - } - jsonb, err := json.Marshal(t) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%s\n", jsonb) - - var got T - err = json.Unmarshal(jsonb, &got) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%+v\n", got) - - // Output: - // {"ID":"019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF"} - // {ID:019CD2F8-1AE3-774E-BFFF-FFFFFFFFFFFF} -} - -func ExampleUUIDv7_Scan() { - var id uuidv7.UUIDv7 - - // Scan the value from the database: - // - // dbc.QueryRow(`SELECT id ...`, &id) - - err := id.Scan([]byte(`017F22E2-79B0-7CC3-98C4-DC0C0C07398F`)) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%s\n", id) - // Output: - // 017F22E2-79B0-7CC3-98C4-DC0C0C07398F -} diff --git a/lib/uuidv7/uuidv7_test.go b/lib/uuidv7/uuidv7_test.go deleted file mode 100644 index 2116b045..00000000 --- a/lib/uuidv7/uuidv7_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-FileCopyrightText: 2026 M. Shulhan - -package uuidv7 - -import ( - "testing" - - "git.sr.ht/~shulhan/pakakeh.go/lib/test" -) - -func TestUUIDv7_Scan(t *testing.T) { - id := Parse(`019B76DA-A800-7000-8000-00000000001A`) - - listCase := []struct { - desc string - data any - expError string - exp string - }{{ - desc: `With empty data`, - data: `017F22E2-79B0-7CC3-98C4-DC0C0C07398F`, - expError: `uuidv7: Scan: invalid type string`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }, { - desc: `With invalid version`, - data: []byte(`5c146b14-3c52-4afd-938a-375d0df1fbf6`), - expError: `uuidv7: invalid version 2`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }, { - desc: `With nil`, - data: nil, - expError: ``, - exp: `00000000-0000-0000-0000-000000000000`, - }} - for _, tc := range listCase { - err := id.Scan(tc.data) - if err != nil { - test.Assert(t, tc.desc, tc.expError, err.Error()) - } - test.Assert(t, tc.desc, tc.exp, id.String()) - } -} - -func TestUUIDv7_UnmarshalBinary(t *testing.T) { - id := Parse(`019B76DA-A800-7000-8000-00000000001A`) - - listCase := []struct { - desc string - expError string - exp string - data []byte - }{{ - desc: `With empty data`, - expError: `uuidv7: invalid length 0`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }, { - desc: `With non-version 7`, - data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 1, 2, 3, 4}, - expError: `uuidv7: invalid version 0`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }, { - desc: `OK`, - data: []byte{0xf, 0xf, 0xf, 0xf, - 0xf, 0xf, - 0x70, 0xf, - 0xf, 0xf, - 0xf, 0xf, 0xf, 0xf, 0xf, 0xf}, - exp: `0F0F0F0F-0F0F-700F-0F0F-0F0F0F0F0F0F`, - }} - for _, tc := range listCase { - err := id.UnmarshalBinary(tc.data) - if err != nil { - test.Assert(t, tc.desc, tc.expError, err.Error()) - } - test.Assert(t, tc.desc, tc.exp, id.String()) - } -} - -func TestUUIDv7_UnmarshalText(t *testing.T) { - id := Parse(`019B76DA-A800-7000-8000-00000000001A`) - - listCase := []struct { - desc string - expError string - exp string - data []byte - }{{ - desc: `With empty data`, - expError: `uuidv7: invalid length 0`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }, { - desc: `With non-version 7`, - data: []byte(`5c146b14-3c52-4afd-938a-375d0df1fbf6`), - expError: `uuidv7: invalid version 2`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }, { - desc: `With invalid hex (high)`, - data: []byte(`X17F22E2-79B0-7CC3-98C4-DC0C0C07398F`), - expError: `uuidv7: encoding/hex: invalid byte: U+0058 'X'`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }, { - desc: `With invalid hex (low)`, - data: []byte(`017F22E2-79B0-7CC3-98C4-DC0C0C07398X`), - expError: `uuidv7: encoding/hex: invalid byte: U+0058 'X'`, - exp: `019B76DA-A800-7000-8000-00000000001A`, - }} - for _, tc := range listCase { - err := id.UnmarshalText(tc.data) - if err != nil { - test.Assert(t, tc.desc, tc.expError, err.Error()) - } - test.Assert(t, tc.desc, tc.exp, id.String()) - } -} -- cgit v1.3