Skip to content
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
34 changes: 34 additions & 0 deletions shortuuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,40 @@ func NewWithAlphabet(abc string) string {
return enc.Encode(uuid.New())
}

// NewV7 returns a new UUIDv7, encoded with base57.
// UUIDv7 is time-based and provides monotonicity, making it ideal for certain database indexing scenarios.
func NewV7() string {
return DefaultEncoder.Encode(uuid.Must(uuid.NewV7()))
}

// NewV7WithEncoder returns a new UUIDv7, encoded with enc.
func NewV7WithEncoder(enc Encoder) string {
return enc.Encode(uuid.Must(uuid.NewV7()))
}

// NewV7WithNamespace returns a new UUIDv5 (or v7 if name is empty), encoded with base57.
func NewV7WithNamespace(name string) string {
Comment on lines +67 to +68
Copy link
Owner

@lithammer lithammer Jan 31, 2026

Choose a reason for hiding this comment

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

I think the naming for this function becomes a bit confusing when it says NewV7 but typically you actually get a V5.

The NewWithNamespace function gets away with it because it's not explicitly mentioned. Hmm 🤔

Copy link
Owner

Choose a reason for hiding this comment

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

I don't have any great suggestions though.

Copy link
Owner

@lithammer lithammer Feb 2, 2026

Choose a reason for hiding this comment

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

I'd say drop it for now. I think I want to move away from NewWithNamespace in favour of an explicit NewV5(ns, name) function, see:

The risk with trying to be smart is that people want to add new schemes to the name→namespace resolver, e.g:

Better to just help them create their own "namespace matcher" function. They can tailor it to their own use-case as well (for example don't bother with case-insensitive matching etc)

var u uuid.UUID

switch {
case name == "":
u = uuid.Must(uuid.NewV7())
case hasPrefixCaseInsensitive(name, "https://"), hasPrefixCaseInsensitive(name, "http://"):
u = hashedUUID(uuid.NameSpaceURL, name)
default:
u = hashedUUID(uuid.NameSpaceDNS, name)
}

return DefaultEncoder.Encode(u)
}

// NewV7WithAlphabet returns a new UUIDv7, encoded with base57 using the
// alternative alphabet abc.
func NewV7WithAlphabet(abc string) string {
enc := encoder{newAlphabet(abc)}
return enc.Encode(uuid.Must(uuid.NewV7()))
}

func hasPrefixCaseInsensitive(s, prefix string) bool {
return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix)
}
Expand Down
91 changes: 91 additions & 0 deletions shortuuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,79 @@ func TestAlphabet_MB(t *testing.T) {
}
}

func TestNewV7(t *testing.T) {
shortUUID := NewV7()

decodedUUID, err := DefaultEncoder.Decode(shortUUID)
if err != nil {
t.Fatalf("failed to decode short UUIDv7: %v", err)
}

if decodedUUID.Version() != 7 {
t.Errorf("expected UUID version 7, got %d", decodedUUID.Version())
}

if decodedUUID.Variant() != uuid.RFC4122 {
t.Errorf("expected RFC4122 variant, got %d", decodedUUID.Variant())
}
}

func TestNewV7WithEncoder(t *testing.T) {
customAlphabet := "abcdef0123456789"
enc := encoder{newAlphabet(customAlphabet)}

shortUUID := NewV7WithEncoder(enc)

decodedUUID, err := enc.Decode(shortUUID)
if err != nil {
t.Fatalf("failed to decode short UUIDv7 with custom encoder: %v", err)
}

if decodedUUID.Version() != 7 {
t.Errorf("expected UUID version 7, got %d", decodedUUID.Version())
}
}

func TestNewV7WithNamespace(t *testing.T) {
var tests = []struct {
name string
}{
{"http://www.example.com/"},
{"example.com/"},
{"https://shortuuid.example/"},
{""}, // Empty name for UUIDv7
}
for _, test := range tests {
shortUUID := NewV7WithNamespace(test.name)

decodedUUID, err := DefaultEncoder.Decode(shortUUID)
if err != nil {
t.Fatalf("failed to decode short UUIDv7 with namespace %q: %v", test.name, err)
}

if test.name == "" && decodedUUID.Version() != 7 {
t.Errorf("expected UUID version 7, got %d for empty namespace", decodedUUID.Version())
} else if test.name != "" && decodedUUID.Version() != 5 {
t.Errorf("expected UUID version 5, got %d for namespace %q", decodedUUID.Version(), test.name)
}
}
}

func TestNewV7WithAlphabet(t *testing.T) {
customAlphabet := "abcdefghij12345"
shortUUID := NewV7WithAlphabet(customAlphabet)

enc := encoder{newAlphabet(customAlphabet)}
decodedUUID, err := enc.Decode(shortUUID)
if err != nil {
t.Fatalf("failed to decode short UUIDv7 with custom alphabet: %v", err)
}

if decodedUUID.Version() != 7 {
t.Errorf("expected UUID version 7, got %d", decodedUUID.Version())
}
}

func BenchmarkUUID(b *testing.B) {
for i := 0; i < b.N; i++ {
New()
Expand Down Expand Up @@ -375,3 +448,21 @@ func BenchmarkNewWithNamespaceHttps(b *testing.B) {
_ = NewWithNamespace("https://someaveragelengthurl.test")
}
}

func BenchmarkNewV7(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NewV7()
}
}

func BenchmarkNewV7WithAlphabet(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NewV7WithAlphabet("abcdefghij12345")
}
}

func BenchmarkNewV7WithNamespace(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NewV7WithNamespace("http://example.com/")
}
}
Loading