Skip to content

Commit

Permalink
algorithm change
Browse files Browse the repository at this point in the history
  • Loading branch information
4kimov committed Sep 10, 2023
1 parent 3a169fa commit 030e037
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 234 deletions.
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,32 +52,31 @@ Simple encode & decode:
[embedmd]:# (examples/sqids-encode-decode/sqids-encode-decode.go /.+sqids.New/ /\[1, 2, 3\]/)
```go
s, _ := sqids.New()
id, _ := s.Encode([]uint64{1, 2, 3}) // "8QRLaD"
id, _ := s.Encode([]uint64{1, 2, 3}) // "86Rf07"
numbers := s.Decode(id) // [1, 2, 3]
```

> **Note**
> 🚧 Because of the algorithm's design, **multiple IDs can decode back into the same sequence of numbers**. If it's important to your design that IDs are canonical, you have to manually re-encode decoded numbers and check that the generated ID matches.
Randomize IDs by providing a custom alphabet:
Enforce a *minimum* length for IDs:

[embedmd]:# (examples/sqids-custom-alphabet/sqids-custom-alphabet.go /.+sqids.New/ /\[1, 2, 3\]/)
[embedmd]:# (examples/sqids-minimum-length/sqids-minimum-length.go /.+sqids.New/ /\[1, 2, 3\]/)
```go
s, _ := sqids.New(sqids.Options{
Alphabet: "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE",
MinLength: 10,
})
id, _ := s.Encode([]uint64{1, 2, 3}) // "B5aMa3"
id, _ := s.Encode([]uint64{1, 2, 3}) // "86Rf07xd4z"
numbers := s.Decode(id) // [1, 2, 3]
```
Randomize IDs by providing a custom alphabet:

Enforce a *minimum* length for IDs:

[embedmd]:# (examples/sqids-minimum-length/sqids-minimum-length.go /.+sqids.New/ /\[1, 2, 3\]/)
[embedmd]:# (examples/sqids-custom-alphabet/sqids-custom-alphabet.go /.+sqids.New/ /\[1, 2, 3\]/)
```go
s, _ := sqids.New(sqids.Options{
MinLength: 10,
Alphabet: "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE",
})
id, _ := s.Encode([]uint64{1, 2, 3}) // "75JT1cd0dL"
id, _ := s.Encode([]uint64{1, 2, 3}) // "B4aajs"
numbers := s.Decode(id) // [1, 2, 3]
```

Expand All @@ -86,9 +85,9 @@ Prevent specific words from appearing anywhere in the auto-generated IDs:
[embedmd]:# (examples/sqids-blocklist/sqids-blocklist.go /.+sqids.New/ /\[1, 2, 3\]/)
```go
s, _ := sqids.New(sqids.Options{
Blocklist: []string{"word1", "word2"},
Blocklist: []string{"86Rf07"},
})
id, _ := s.Encode([]uint64{1, 2, 3}) // "8QRLaD"
id, _ := s.Encode([]uint64{1, 2, 3}) // "se8ojk"
numbers := s.Decode(id) // [1, 2, 3]
```

Expand Down
6 changes: 3 additions & 3 deletions alphabet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestMultibyteAlphabet(t *testing.T) {

func TestAlphabetSimple(t *testing.T) {
numbers := []uint64{1, 2, 3}
id := "4d9fd2"
id := "489158"

alphabet := "0123456789abcdef"
s, err := New(Options{
Expand All @@ -44,7 +44,7 @@ func TestAlphabetSimple(t *testing.T) {

func TestAlphabetShort(t *testing.T) {
s, err := New(Options{
Alphabet: "abcde",
Alphabet: "abc",
})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -94,7 +94,7 @@ func TestRepeatingAlphabetCharacters(t *testing.T) {

func TestTooShortOfAnAlphabet(t *testing.T) {
if _, err := New(Options{
Alphabet: "abcd",
Alphabet: "ab",
}); err == nil {
t.Errorf("Should not accept too short of an alphabet")
}
Expand Down
71 changes: 47 additions & 24 deletions blocklist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ func TestBlocklist(t *testing.T) {
}

func TestBlocklistDefault(t *testing.T) {
numbers := []uint64{200044}
blockedID := "sexy"
unblockedID := "d171vI"
numbers := []uint64{4572721}
blockedID := "aho1e"
unblockedID := "JExTR"

s, err := New()
if err != nil {
Expand All @@ -45,8 +45,8 @@ func TestBlocklistDefault(t *testing.T) {
}

func TestBlocklistEmpty(t *testing.T) {
numbers := []uint64{200044}
id := "sexy"
numbers := []uint64{4572721}
id := "aho1e"

s, err := New(Options{
Blocklist: []string{},
Expand All @@ -70,12 +70,12 @@ func TestBlocklistEmpty(t *testing.T) {
}

func TestBlocklistNonEmpty(t *testing.T) {
numbers := []uint64{200044}
id := "sexy"
numbers := []uint64{4572721}
id := "aho1e"

s, err := New(Options{
Blocklist: []string{
"AvTg", // originally encoded [100000]
"ArUO", // originally encoded [100000]
},
})
if err != nil {
Expand All @@ -98,7 +98,7 @@ func TestBlocklistNonEmpty(t *testing.T) {
}

// make sure we are using the passed blocklist
decodedNumbers = s.Decode("AvTg")
decodedNumbers = s.Decode("ArUO")
if !reflect.DeepEqual([]uint64{100_000}, decodedNumbers) {
t.Errorf("Decoding `%v` should produce `%v`, but instead produced `%v`", id, []uint64{100_000}, decodedNumbers)
}
Expand All @@ -108,27 +108,27 @@ func TestBlocklistNonEmpty(t *testing.T) {
t.Fatal(err)
}

if generatedID != "7T1X8k" {
t.Errorf("Encoding `%v` should produce `%v`, but instead produced `%v`", []uint64{100_000}, "7T1X8k", generatedID)
if generatedID != "QyG4" {
t.Errorf("Encoding `%v` should produce `%v`, but instead produced `%v`", []uint64{100_000}, "QyG4", generatedID)
}

decodedNumbers = s.Decode("7T1X8k")
decodedNumbers = s.Decode("QyG4")
if !reflect.DeepEqual([]uint64{100_000}, decodedNumbers) {
t.Errorf("Decoding `%v` should produce `%v`, but instead produced `%v`", id, []uint64{100_000}, decodedNumbers)
}
}

func TestNewBlocklist(t *testing.T) {
numbers := []uint64{1, 2, 3}
id := "TM0x1Mxz"
numbers := []uint64{1000000, 2000000}
id := "1aYeB7bRUt"

s, err := New(Options{
Blocklist: []string{
"8QRLaD", // normal result of 1st encoding, let's block that word on purpose
"7T1cd0dL", // result of 2nd encoding
"UeIe", // result of 3rd encoding is `RA8UeIe7`, let's block a substring
"imhw", // result of 4th encoding is `WM3Limhw`, let's block the postfix
"LfUQ", // result of 4th encoding is `LfUQh4HN`, let's block the prefix
"JSwXFaosAN", // normal result of 1st encoding, let's block that word on purpose
"OCjV9JK64o", // result of 2nd encoding
"rBHf", // result of 3rd encoding is `4rBHfOiqd3`, let's block a substring
"79SM", // result of 4th encoding is `dyhgw479SM`, let's block the postfix
"7tE6", // result of 4th encoding is `7tE6jdAHLe`, let's block the prefix
},
})
if err != nil {
Expand All @@ -152,7 +152,7 @@ func TestNewBlocklist(t *testing.T) {

func TestDecodingBlocklistedIDs(t *testing.T) {
numbers := []uint64{1, 2, 3}
blocklist := []string{"8QRLaD", "7T1cd0dL", "RA8UeIe7", "WM3Limhw", "LfUQh4HN"}
blocklist := []string{"86Rf07", "se8ojk", "ARsz1p", "Q8AI49", "5sQRZO"}

s, err := New(Options{
Blocklist: blocklist,
Expand All @@ -173,7 +173,7 @@ func TestShortBlocklistMatch(t *testing.T) {
numbers := []uint64{1_000}

s, err := New(Options{
Blocklist: []string{"pPQ"},
Blocklist: []string{"pnd"},
})

if err != nil {
Expand All @@ -193,11 +193,11 @@ func TestShortBlocklistMatch(t *testing.T) {

func TestUpperCaseAlphabetBlocklistFiltering(t *testing.T) {
numbers := []uint64{1, 2, 3}
id := "ULPBZGBM"
id := "IBSHOZ"

s, err := New(Options{
Alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
Blocklist: []string{"sqnmpn"}, // lowercase blocklist in only-uppercase alphabet
Blocklist: []string{"sxnzkl"}, // lowercase blocklist in only-uppercase alphabet
})
if err != nil {
t.Fatal(err)
Expand All @@ -210,7 +210,7 @@ func TestUpperCaseAlphabetBlocklistFiltering(t *testing.T) {

decodedNumbers := s.Decode(id)

// without blocklist, would've been "SQNMPN"
// without blocklist, would've been "SXNZKL"
if id != generatedID {
t.Errorf("Encoding `%v` should produce `%v`, but instead produced `%v`", numbers, id, generatedID)
}
Expand All @@ -220,6 +220,29 @@ func TestUpperCaseAlphabetBlocklistFiltering(t *testing.T) {
}
}

func TestMaxEncodingAttempts(t *testing.T) {
alphabet := "abc"
minLength := uint8(3)
blocklist := []string{"cab", "abc", "bca"}

s, err := New(Options{
Alphabet: alphabet,
MinLength: minLength,
Blocklist: blocklist,
})
if err != nil {
t.Fatal(err)
}

if len(alphabet) != int(minLength) || len(blocklist) != int(minLength) {
t.Errorf("`TestMaxEncodingAttempts` is not setup properly")
}

if _, err := s.Encode([]uint64{0}); err == nil {
t.Errorf("Should throw error about max regeneration attempts")
}
}

func TestFilterBlocklist(t *testing.T) {
t.Run("no words less than 3 chars", func(t *testing.T) {
filtered := filterBlocklist("YESNO", []string{"yes", "no"})
Expand Down
80 changes: 37 additions & 43 deletions encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,24 @@ func BenchmarkEncodeDecode(b *testing.B) {
}

func TestEncodingSimple(t *testing.T) {
id := "86Rf07"
numbers := []uint64{1, 2, 3}

s, err := New()
if err != nil {
t.Fatal(err)
}

id, err := s.Encode(numbers)
generatedID, err := s.Encode(numbers)
if err != nil {
t.Fatal(err)
}

decodedNumbers := s.Decode(id)
if id != generatedID {
t.Errorf("Encoding `%v` should produce `%v`, but instead produced `%v`", numbers, id, generatedID)
}

decodedNumbers := s.Decode(generatedID)
if !reflect.DeepEqual(numbers, decodedNumbers) {
t.Errorf("Could not encode/decode `%v`", numbers)
}
Expand Down Expand Up @@ -71,16 +76,16 @@ func TestEncodingIncrementalNumbers(t *testing.T) {
}

ids := map[string][]uint64{
"bV": {0},
"U9": {1},
"g8": {2},
"Ez": {3},
"V8": {4},
"ul": {5},
"O3": {6},
"AF": {7},
"ph": {8},
"n8": {9},
"bM": {0},
"Uk": {1},
"gb": {2},
"Ef": {3},
"Vq": {4},
"uw": {5},
"OI": {6},
"AX": {7},
"p6": {8},
"nJ": {9},
}

for id, numbers := range ids {
Expand All @@ -107,16 +112,16 @@ func TestEncodingIncrementalNumbersSameIndex0(t *testing.T) {
}

ids := map[string][]uint64{
"SrIu": {0, 0},
"nZqE": {0, 1},
"tJyf": {0, 2},
"e86S": {0, 3},
"rtC7": {0, 4},
"sQ8R": {0, 5},
"uz2n": {0, 6},
"7Td9": {0, 7},
"3nWE": {0, 8},
"mIxM": {0, 9},
"SvIz": {0, 0},
"n3qa": {0, 1},
"tryF": {0, 2},
"eg6q": {0, 3},
"rSCF": {0, 4},
"sR8x": {0, 5},
"uY2M": {0, 6},
"74dI": {0, 7},
"30WX": {0, 8},
"moxr": {0, 9},
}

for id, numbers := range ids {
Expand All @@ -143,16 +148,16 @@ func TestEncodingIncrementalNumbersSameIndex1(t *testing.T) {
}

ids := map[string][]uint64{
"SrIu": {0, 0},
"nbqh": {1, 0},
"t4yj": {2, 0},
"eQ6L": {3, 0},
"r4Cc": {4, 0},
"sL82": {5, 0},
"uo2f": {6, 0},
"7Zdq": {7, 0},
"36Wf": {8, 0},
"m4xT": {9, 0},
"SvIz": {0, 0},
"nWqP": {1, 0},
"tSyw": {2, 0},
"eX68": {3, 0},
"rxCY": {4, 0},
"sV8a": {5, 0},
"uf2K": {6, 0},
"7Cdk": {7, 0},
"3aWP": {8, 0},
"m2xn": {9, 0},
}

for id, numbers := range ids {
Expand Down Expand Up @@ -234,16 +239,5 @@ func TestEncodingInvalidCharacter(t *testing.T) {
}
}

func TestDecodeInvalidIDWithRepeatingReservedCharacter(t *testing.T) {
s, err := New()
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(s.Decode("fff"), []uint64{}) {
t.Errorf("Could not decode invalid ID with repeating reserved character")
}
}

// TestEncodingOutOfRange - no need since `[]uint64` handles ranges
// func TestEncodingOutOfRange(t *testing.T) {}
4 changes: 2 additions & 2 deletions examples/sqids-blocklist/sqids-blocklist.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (

func main() {
s, _ := sqids.New(sqids.Options{
Blocklist: []string{"word1", "word2"},
Blocklist: []string{"86Rf07"},
})
id, _ := s.Encode([]uint64{1, 2, 3}) // "8QRLaD"
id, _ := s.Encode([]uint64{1, 2, 3}) // "se8ojk"
numbers := s.Decode(id) // [1, 2, 3]

fmt.Println(id, numbers)
Expand Down
2 changes: 1 addition & 1 deletion examples/sqids-custom-alphabet/sqids-custom-alphabet.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func main() {
s, _ := sqids.New(sqids.Options{
Alphabet: "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE",
})
id, _ := s.Encode([]uint64{1, 2, 3}) // "B5aMa3"
id, _ := s.Encode([]uint64{1, 2, 3}) // "B4aajs"
numbers := s.Decode(id) // [1, 2, 3]

fmt.Println(id, numbers)
Expand Down
2 changes: 1 addition & 1 deletion examples/sqids-encode-decode/sqids-encode-decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func main() {
s, _ := sqids.New()
id, _ := s.Encode([]uint64{1, 2, 3}) // "8QRLaD"
id, _ := s.Encode([]uint64{1, 2, 3}) // "86Rf07"
numbers := s.Decode(id) // [1, 2, 3]

fmt.Println(id, numbers)
Expand Down

0 comments on commit 030e037

Please sign in to comment.