From cdf30c1bae6ec0f069934f2166a2228477ef3284 Mon Sep 17 00:00:00 2001 From: micha Date: Mon, 2 Mar 2026 04:05:11 +0100 Subject: [PATCH] Various improvements and changed encoding/decoding format to LLL-NNN-LLC with reduced character set. --- README.md | 46 ++--- cmd/triplex-example/main.go | 10 +- serial/serial.go | 249 +++++++++++++++------------ serial/serial_test.go | 334 ++++++++++++++++++++++++++++++------ 4 files changed, 453 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index a6eda42..800294e 100644 --- a/README.md +++ b/README.md @@ -10,36 +10,36 @@ high-integrity identifiers. It maps a compact 32-bit integer space to a human-readable serial format: ```text -LLL-NNN-LC +LLL-NNN-LLC ``` Where: -- `L` = data letter (`A`–`Z`) +- `L` = data letter (`A` – `Z` excluding `F`, `I`, `O`, `Q`, `U`) - `N` = number (`100`–`999`) -- `C` = checksum letter (`A`–`Z`) +- `C` = checksum letter (`A` – `Z`) For instance ```text -ABC-123-DT +ABC-123-DEO ``` ## What it provides -- 4 data letters +- 5 data letters - 3 numeric digits (`100`–`999`) - 1 checksum letter -- Optional forbidden-triplet filtering on the first three letters +- Optional forbidden-triplet letter combination filtering on the first three letters - Reversible integer encoding/decoding - Deterministic checksum generation -Total index capacity: **386,942,400** (`26^4 * 900`) +Total index capacity: **3,675,690,900** (`21^5 * 900`) ## Features -- Reversible mapping: `Encode(code) ↔ Decode(index)` +- Reversible mapping: `Decode(code) ↔ Encode(index)` - Deterministic checksum: `C = 'A' + (index % 26)` -- Compact index space: fits in `uint32` +- Compact index space: fits in `uint` - Human-readable serial format - Useful for manufacturing, logistics, SaaS identifiers, and audit-safe systems @@ -55,14 +55,14 @@ import ( ) func main() { - idx := uint32(123456) + idx := uint(123456) - code, err := serial.Decode(idx) + code, err := serial.Encode(idx) if err != nil { panic(err) } - idx2, err := serial.Encode(code) + idx2, err := serial.Decode(code) if err != nil { panic(err) } @@ -74,21 +74,21 @@ func main() { ## Notes -- The current serial format is `LLL-NNN-LC`. +- The current serial format is `LLL-NNN-LLC`. - Forbidden-triplet logic is centralized in `serial.IsForbiddenTriplet` and can be tailored to project rules. ## API -- `serial.Encode(code string) (uint32, error)` - - Parses and validates `LLL-NNN-LC`, checks checksum, and returns the deterministic index. -- `serial.Decode(idx uint32) (string, error)` - - Converts a valid index back to `LLL-NNN-LC`. -- `serial.CompleteCode(codeWithoutChecksum string) (string, uint32, error)` - - Takes `LLL-NNN-L` and returns the completed `LLL-NNN-LC` plus index. -- `serial.RandomCode(isInUse ...func(uint32) bool) (string, uint32, error)` - - Generates a random valid `LLL-NNN-LC` code and its corresponding index. +- `serial.Decode(code string) (uint, error)` + - Parses and validates `LLL-NNN-LLC`, checks checksum, and returns the deterministic index. +- `serial.Encode(idx uint) (string, error)` + - Converts a valid index back to `LLL-NNN-LLC`. +- `serial.CompleteCode(codeWithoutChecksum string) (string, uint, error)` + - Takes `LLL-NNN-L` and returns the completed `LLL-NNN-LLC` plus index. +- `serial.RandomCode(isInUse ...func(uint) bool) (string, uint, error)` + - Generates a random valid `LLL-NNN-LLC` code and its corresponding index. - If provided, the callback is used to skip indices already in use by the client. -- `serial.RandomCodeWithOptions(options serial.RandomCodeOptions) (string, uint32, error)` +- `serial.RandomCodeWithOptions(options serial.RandomCodeOptions) (string, uint, error)` - Configurable variant with `MaxAttempts`, `IsInUse`, and custom `RandomIndex` source. Example with options: @@ -117,7 +117,7 @@ Exported errors: - `serial.ErrInvalidFormat` - `serial.ErrInvalidFormatNoChecksum` -- `serial.ErrInvalidLetters` +- `serial.ErrInvalidLetter` - `serial.ErrForbiddenLetterTriplet` - `serial.ErrInvalidNumber` - `serial.ErrNumberOutOfRange` diff --git a/cmd/triplex-example/main.go b/cmd/triplex-example/main.go index 3d85271..8e338d6 100644 --- a/cmd/triplex-example/main.go +++ b/cmd/triplex-example/main.go @@ -13,11 +13,11 @@ import ( "git.hoiting.org/micha/triplex/serial" ) -func alreadyUsedByClient(idx uint32) bool { +func alreadyUsedByClient(idx uint) bool { return idx%2 == 0 } -func myRandomIndex(max uint32) (uint32, error) { +func myRandomIndex(max uint) (uint, error) { if max == 0 { return 0, fmt.Errorf("max must be > 0") } @@ -27,7 +27,7 @@ func myRandomIndex(max uint32) (uint32, error) { return 0, err } - return binary.LittleEndian.Uint32(buf[:]) % max, nil + return uint(binary.LittleEndian.Uint32(buf[:])) % max, nil } func main() { @@ -48,10 +48,10 @@ func main() { opts := serial.RandomCodeOptions{ MaxAttempts: 500, - IsInUse: func(idx uint32) bool { + IsInUse: func(idx uint) bool { return alreadyUsedByClient(idx) }, - RandomIndex: func(max uint32) (uint32, error) { + RandomIndex: func(max uint) (uint, error) { return myRandomIndex(max) }, } diff --git a/serial/serial.go b/serial/serial.go index 5f0cef5..600cd7a 100644 --- a/serial/serial.go +++ b/serial/serial.go @@ -13,15 +13,15 @@ const ( MaxIndex = block MaxRandomAttempts = 1024 - letters = 26 - numbers = 900 // 100–999 - block = letters * letters * letters * letters * numbers // 26^4 * 900 + letters = 21 // A-Z excluding F, I, O, Q, U + numbers = 900 // 100–999 + block = letters * letters * letters * numbers * letters * letters // 26^5 * 900 ) var ( - ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LC") - ErrInvalidFormatNoChecksum = errors.New("invalid format, expected LLL-NNN-L") - ErrInvalidLetters = errors.New("invalid letters") + ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LLL") + ErrInvalidFormatNoChecksum = errors.New("invalid format, expected LLL-NNN-LL") + ErrInvalidLetter = errors.New("invalid letters") ErrForbiddenLetterTriplet = errors.New("forbidden letter combination") ErrInvalidNumber = errors.New("invalid number") ErrNumberOutOfRange = errors.New("number out of range") @@ -33,188 +33,225 @@ var ( type RandomCodeOptions struct { MaxAttempts int - IsInUse func(uint32) bool - RandomIndex func(max uint32) (uint32, error) + IsInUse func(uint) bool + RandomIndex func(max uint) (uint, error) } -func Encode(code string) (uint32, error) { - // Expected format: L1L2L3-NNN-L4C => length 10, positions: 0,1,2,4,5,6,8,9 - if len(code) != 10 || code[3] != '-' || code[7] != '-' { +func Decode(code string) (uint, error) { + // Expected format: L1L2L3-NNN-L4L5C => length 10, positions: 0,1,2,4,5,6,8,9,10 + if len(code) != 11 || + !isUpperASCIIAlpha(code[10]) { return 0, ErrInvalidFormat } - - l1, l2, l3, l4 := code[0], code[1], code[2], code[8] - n1, n2, n3 := code[4], code[5], code[6] - - // Validate letters - // Forbidden rule applies to l1,l2,l3 - // Parse number from code[4:7] - if !isUpperAlphaASCII(l1) || !isUpperAlphaASCII(l2) || !isUpperAlphaASCII(l3) || !isUpperAlphaASCII(l4) { - return 0, ErrInvalidLetters + idx, err := decodeWithoutChecksum(code[0:10]) + if err != nil { + return 0, err } - if IsForbiddenTriplet(l1, l2, l3) { - return 0, ErrForbiddenLetterTriplet - } - if !isNumber(n1) || !isNumber(n2) || !isNumber(n3) { - return 0, ErrInvalidNumber - } - - L1 := uint32(l1 - 'A') - L2 := uint32(l2 - 'A') - L3 := uint32(l3 - 'A') - L4 := uint32(l4 - 'A') - num := uint32(n1-'0')*100 + uint32(n2-'0')*10 + uint32(n3-'0') - if num < 100 || num > 999 { - return 0, ErrNumberOutOfRange - } - N := num - 100 - - idx := uint32((((((L1*letters+L2)*letters+L3)*letters + L4) * numbers) + N)) - - if code[9] != checksumLetter(idx) { + checksum := checksumLetter(idx) + if code[10] != checksum { return 0, ErrInvalidChecksum } - return idx, nil } -func Decode(idx uint32) (string, error) { +func Encode(idx uint) (string, error) { if idx >= MaxIndex { return "", ErrIndexOutOfRange } - x := int(idx) - L1 := x / (letters * letters * letters * numbers) - r1 := x % (letters * letters * letters * numbers) + // L1 = // xLL-NNN-LL + L1 := x / (letters * letters * numbers * letters * letters) + r1 := x % (letters * letters * numbers * letters * letters) - L2 := r1 / (letters * letters * numbers) - r2 := r1 % (letters * letters * numbers) + // L2 = // xL-NNN-LL + L2 := r1 / (letters * numbers * letters * letters) + r2 := r1 % (letters * numbers * letters * letters) - L3 := r2 / (letters * numbers) - r3 := r2 % (letters * numbers) + // L3 = // x-NNN-LL + L3 := r2 / (numbers * letters * letters) + r3 := r2 % (numbers * letters * letters) - L4 := r3 / numbers - N := r3 % numbers + // N = // x-LL + N := r3 / (letters * letters) + r4 := r3 % (letters * letters) - l1 := byte('A' + L1) - l2 := byte('A' + L2) - l3 := byte('A' + L3) - l4 := byte('A' + L4) + // L4 = // xL + L4 := r4 / letters + r5 := r4 % letters + + // L5 = // x + L5 := r5 + + l1 := encodeLetter(L1) + l2 := encodeLetter(L2) + l3 := encodeLetter(L3) + l4 := encodeLetter(L4) + l5 := encodeLetter(L5) if IsForbiddenTriplet(l1, l2, l3) { return "", ErrForbiddenLetterTriplet } c := checksumLetter(idx) - - code := fmt.Sprintf("%c%c%c-%03d-%c%c", l1, l2, l3, N+100, l4, c) - + code := fmt.Sprintf("%c%c%c-%03d-%c%c%c", l1, l2, l3, N+100, l4, l5, c) return code, nil } -func CompleteCode(codeWithoutChecksum string) (string, uint32, error) { - if len(codeWithoutChecksum) != 9 || codeWithoutChecksum[3] != '-' || codeWithoutChecksum[7] != '-' { +func CompleteCode(codeWithoutChecksum string) (string, uint, error) { + if len(codeWithoutChecksum) != 10 { return "", 0, ErrInvalidFormatNoChecksum } - - l1, l2, l3, l4 := codeWithoutChecksum[0], codeWithoutChecksum[1], codeWithoutChecksum[2], codeWithoutChecksum[8] - n1, n2, n3 := codeWithoutChecksum[4], codeWithoutChecksum[5], codeWithoutChecksum[6] - - if !isUpperAlphaASCII(l1) || !isUpperAlphaASCII(l2) || !isUpperAlphaASCII(l3) || !isUpperAlphaASCII(l4) { - return "", 0, ErrInvalidLetters + idx, err := decodeWithoutChecksum(codeWithoutChecksum) + if err != nil { + return "", 0, err } - if IsForbiddenTriplet(l1, l2, l3) { - return "", 0, ErrForbiddenLetterTriplet - } - if !isNumber(n1) || !isNumber(n2) || !isNumber(n3) { - return "", 0, ErrInvalidNumber - } - - L1 := uint32(l1 - 'A') - L2 := uint32(l2 - 'A') - L3 := uint32(l3 - 'A') - L4 := uint32(l4 - 'A') - num := uint32(n1-'0')*100 + uint32(n2-'0')*10 + uint32(n3-'0') - if num < 100 || num > 999 { - return "", 0, ErrNumberOutOfRange - } - N := num - 100 - - idx := uint32((((((L1*letters+L2)*letters+L3)*letters + L4) * numbers) + N)) checksum := checksumLetter(idx) - return codeWithoutChecksum + string(checksum), idx, nil } -func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) { +func RandomCode(isInUse ...func(uint) bool) (string, uint, error) { options := RandomCodeOptions{} if len(isInUse) > 0 { options.IsInUse = isInUse[0] } - return RandomCodeWithOptions(options) } -func RandomCodeWithOptions(options RandomCodeOptions) (string, uint32, error) { +func RandomCodeWithOptions(options RandomCodeOptions) (string, uint, error) { maxAttempts := options.MaxAttempts if maxAttempts <= 0 { maxAttempts = MaxRandomAttempts } - inUse := options.IsInUse randomIndex := options.RandomIndex if randomIndex == nil { - randomIndex = randomUint32n + randomIndex = randomuintn } - for attempts := 0; attempts < maxAttempts; attempts++ { idx, err := randomIndex(MaxIndex) if err != nil { return "", 0, fmt.Errorf("%w: %v", ErrRandomSourceFailed, err) } - if idx >= MaxIndex { return "", 0, fmt.Errorf("%w: index out of range", ErrRandomSourceFailed) } - if inUse != nil && inUse(idx) { continue } - - code, err := Decode(idx) + code, err := Encode(idx) if err == nil { return code, idx, nil } if errors.Is(err, ErrForbiddenLetterTriplet) { continue } - return "", 0, err } - return "", 0, ErrNoAvailableIndex } -func checksumLetter(idx uint32) byte { - return byte('A' + idx%26) +func IsForbiddenTriplet(l1, l2, l3 byte) bool { + return false } -func isUpperAlphaASCII(b byte) bool { +func decodeWithoutChecksum(code string) (uint, error) { + if code[3] != '-' || + code[7] != '-' { + return 0, ErrInvalidFormat + } + l1, l2, l3 := code[0], code[1], code[2] + n1, n2, n3 := code[4], code[5], code[6] + l4, l5 := code[8], code[9] + + // Validate letters + // Parse number from code[4:7] + if !isTriplexAlpha(l1) || + !isTriplexAlpha(l2) || + !isTriplexAlpha(l3) || + !isTriplexAlpha(l4) || + !isTriplexAlpha(l5) { + return 0, ErrInvalidLetter + } + if !isNumber(n1) || + !isNumber(n2) || + !isNumber(n3) { + return 0, ErrInvalidNumber + } + N, err := iton(n1, n2, n3) + if err != nil { + return 0, err + } + L1 := decodeLetter(l1) + L2 := decodeLetter(l2) + L3 := decodeLetter(l3) + L4 := decodeLetter(l4) + L5 := decodeLetter(l5) + idx := uint((((((L1*letters+L2)*letters+L3)*numbers+N)*letters+L4)*letters + L5)) + return idx, nil +} + +func checksumLetter(idx uint) byte { + return byte('A' + idx % 26) +} + +func isUpperASCIIAlpha(b byte) bool { return (b >= 'A' && b <= 'Z') } +func isTriplexAlpha(b byte) bool { + s := [...]bool{ + 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': false, + 'G': true, 'H': true, 'I': false, 'J': true, 'K': true, 'L': true, + 'M': true, 'N': true, 'O': false, 'P': true, 'Q': false, 'R': true, + 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, + 'Y': false, 'Z': true} + return (b >= 'A' && b <= 'Z' && s[b]) +} + func isNumber(b byte) bool { return b >= '0' && b <= '9' } -func randomUint32n(n uint32) (uint32, error) { +func encodeLetter(index int) byte { + s := [...]byte{ + 0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'G', + 6: 'H', 7: 'J', 8: 'K', 9: 'L', 10: 'M', + 11: 'N', 12: 'P', 13: 'R', 14: 'S', 15: 'T', + 16: 'U', 17: 'V', 18: 'W', 19: 'X', 20: 'Z'} + if index < 0 || index >= len(s) { + panic("index out of range") + } + return s[index] +} + +func decodeLetter(letter byte) int { + s := [...]int{ + 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': -1, 'G': 5, + 'H': 6, 'I': -1, 'J': 7, 'K': 8, 'L': 9, 'M': 10, + 'N': 11, 'O': -1, 'P': 12, 'Q': -1, 'R': 13, 'S': 14, 'T': 15, + 'U': 16, 'V': 17, 'W': 18, 'X': 19, 'Y': -1, 'Z': 20} + if letter < 'A' || letter > 'Z' || s[letter] == -1 { + panic("invalid letter") + } + return s[letter] +} + +func iton(n1, n2, n3 byte) (int, error) { + num := int(n1-'0')*100 + int(n2-'0')*10 + int(n3-'0') + if num < 100 || num > 999 { + return 0, ErrNumberOutOfRange + } + return num - 100, nil +} + +func randomuintn(n uint) (uint, error) { if n == 0 { return 0, ErrIndexOutOfRange } - maxUint32 := ^uint32(0) - limit := maxUint32 - (maxUint32 % n) + maxuint := ^uint(0) + limit := maxuint - (maxuint % n) for { var buf [4]byte @@ -222,13 +259,9 @@ func randomUint32n(n uint32) (uint32, error) { return 0, err } - v := binary.LittleEndian.Uint32(buf[:]) + v := uint(binary.LittleEndian.Uint32(buf[:])) if v < limit { return v % n, nil } } } - -func IsForbiddenTriplet(l1, l2, l3 byte) bool { - return false -} diff --git a/serial/serial_test.go b/serial/serial_test.go index 8231ac3..613e170 100644 --- a/serial/serial_test.go +++ b/serial/serial_test.go @@ -11,19 +11,19 @@ import ( "git.hoiting.org/micha/triplex/serial" ) -// Property: Decode(Encode(code)) == code (for valid codes) +// Property: Encode(Decode(code)) == code (for valid codes) func TestEncodeDecodeRoundtrip(t *testing.T) { - f := func(idx uint32) bool { + f := func(idx uint) bool { if idx >= serial.MaxIndex { return true } - code, err := serial.Decode(idx) + code, err := serial.Encode(idx) if err != nil { return false } - idx2, err := serial.Encode(code) + idx2, err := serial.Decode(code) if err != nil { return false } @@ -32,28 +32,28 @@ func TestEncodeDecodeRoundtrip(t *testing.T) { } if err := quick.Check(f, nil); err != nil { - t.Fatalf("Encode/Decode roundtrip failed: %v", err) + t.Fatalf("Decode/Encode roundtrip failed: %v", err) } } -// Property: Decode(Encode(Decode(i))) == Decode(i) (idempotent on valid indices) +// Property: Encode(Decode(Encode(i))) == Encode(i) (idempotent on valid indices) func TestDecodeEncodeDecodeIdempotent(t *testing.T) { - f := func(idx uint32) bool { + f := func(idx uint) bool { if idx >= serial.MaxIndex { return true } - code1, err := serial.Decode(idx) + code1, err := serial.Encode(idx) if err != nil { return true // decode may reject indices (e.g. forbidden pair) } - idx2, err := serial.Encode(code1) + idx2, err := serial.Decode(code1) if err != nil { return false } - code2, err := serial.Decode(idx2) + code2, err := serial.Encode(idx2) if err != nil { return false } @@ -62,18 +62,18 @@ func TestDecodeEncodeDecodeIdempotent(t *testing.T) { } if err := quick.Check(f, nil); err != nil { - t.Fatalf("Decode/Encode/Decode idempotence failed: %v", err) + t.Fatalf("Encode/Decode/Encode idempotence failed: %v", err) } } -// Property: forbidden first three letters are never returned by Decode +// Property: forbidden first three letters are never returned by Encode func TestForbiddenTripletsNeverDecoded(t *testing.T) { - f := func(idx uint32) bool { + f := func(idx uint) bool { if idx >= serial.MaxIndex { return true } - code, err := serial.Decode(idx) + code, err := serial.Encode(idx) if err != nil { return true // decode may reject indices } @@ -86,31 +86,31 @@ func TestForbiddenTripletsNeverDecoded(t *testing.T) { } if err := quick.Check(f, nil); err != nil { - t.Fatalf("Forbidden triplets appeared in Decode: %v", err) + t.Fatalf("Forbidden triplets appeared in Encode: %v", err) } } -// Optional: basic check for format LLL-NNN-LC +// Optional: basic check for format LLL-NNN-LLC func TestDecodedFormatLooksCorrect(t *testing.T) { - f := func(idx uint32) bool { + f := func(idx uint) bool { if idx >= serial.MaxIndex { return true } - code, err := serial.Decode(idx) + code, err := serial.Encode(idx) if err != nil { return true } - if len(code) != 10 { + if len(code) != 11 { return false } if code[3] != '-' || code[7] != '-' { return false } - // letters at positions 0,1,2,8,9 - for _, p := range []int{0, 1, 2, 8, 9} { + // letters at positions 0,1,2,8,9,10 + for _, p := range []int{0, 1, 2, 8, 9, 10} { if code[p] < 'A' || code[p] > 'Z' { return false } @@ -121,6 +121,9 @@ func TestDecodedFormatLooksCorrect(t *testing.T) { if code[p] < '0' || code[p] > '9' { return false } + if code[0] == '0' { + return false + } } return true @@ -131,18 +134,249 @@ func TestDecodedFormatLooksCorrect(t *testing.T) { } } -func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) { - baseIdx := uint32(0x11223344) - idx := baseIdx % serial.MaxIndex +func TestEncodeSpecificIndex0(t *testing.T) { + code := "AAA-100-AA" + idx := uint(0) - code, err := serial.Decode(idx) + code2, err := serial.Encode(idx) if err != nil { - t.Fatalf("Decode failed for idx %d (derived from 0x11223344): %v", idx, err) + t.Fatalf("Encode failed for idx %d: %v", idx, err) } - idx2, err := serial.Encode(code) + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestEncodeSpecificIndex1(t *testing.T) { + code := "AAA-100-AB" + idx := uint(1) + + code2, err := serial.Encode(idx) if err != nil { - t.Fatalf("Encode failed for code %q: %v", code, err) + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} +func TestEncodeSpecificIndex20(t *testing.T) { + code := "AAA-100-AZ" + idx := uint(20) + + code2, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestEncodeSpecificIndex21(t *testing.T) { + code := "AAA-100-BA" + idx := uint(21) + + code2, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestEncodeSpecificIndex42(t *testing.T) { + code := "AAA-100-CA" + idx := uint(42) + + code2, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestEncodeSpecificIndex441(t *testing.T) { + code := "AAA-101-AA" + idx := uint(441) + + code2, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestEncodeSpecificIndex396900(t *testing.T) { + code := "AAB-100-AA" + idx := uint(396900) + + code2, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestEncodeSpecificIndex8334900(t *testing.T) { + code := "ABA-100-AA" + idx := uint(8334900) + + code2, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestEncodeSpecificIndex175032900(t *testing.T) { + code := "BAA-100-AA" + idx := uint(175032900) + + code2, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d: %v", idx, err) + } + + // Check excluding the checksum letter + if code != code2[0:10] { + t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2) + } +} + +func TestDencodeSpecificIndex0(t *testing.T) { + code := "AAA-100-AAA" + idx := uint(0) + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) + } + + if idx != idx2 { + t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx) + } +} + +func TestDencodeSpecificIndex21(t *testing.T) { + code := "AAA-100-BAV" + idx := uint(21) + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) + } + + if idx != idx2 { + t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx) + } +} + +func TestDencodeSpecificIndex1(t *testing.T) { + code := "AAA-100-ABB" + idx := uint(1) + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) + } + + if idx != idx2 { + t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx) + } +} + +func TestDencodeSpecificIndex441(t *testing.T) { + code := "AAA-101-AAZ" + idx := uint(441) + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) + } + + if idx != idx2 { + t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx) + } +} +func TestDencodeSpecificIndex396900(t *testing.T) { + code := "AAB-100-AAK" + idx := uint(396900) + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) + } + + if idx != idx2 { + t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx) + } +} + +func TestDencodeSpecificIndex8334900(t *testing.T) { + code := "ABA-100-AAC" + idx := uint(8334900) + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) + } + + if idx != idx2 { + t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx) + } +} + +func TestDencodeSpecificIndex175032900(t *testing.T) { + code := "BAA-100-AAQ" + idx := uint(175032900) + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) + } + + if idx != idx2 { + t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx) + } +} + +func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) { + baseIdx := uint(0x11223344) + idx := baseIdx % serial.MaxIndex + + code, err := serial.Encode(idx) + if err != nil { + t.Fatalf("Encode failed for idx %d (derived from 0x11223344): %v", idx, err) + } + + idx2, err := serial.Decode(code) + if err != nil { + t.Fatalf("Decode failed for code %q: %v", code, err) } if idx2 != idx { @@ -151,51 +385,51 @@ func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) { } func TestDecodeOutOfRangeAtMaxIndex(t *testing.T) { - _, err := serial.Decode(serial.MaxIndex) + _, err := serial.Encode(serial.MaxIndex) if !errors.Is(err, serial.ErrIndexOutOfRange) { t.Fatalf("expected index out of range error, got: %v", err) } } func TestDecodeOutOfRangeAtMaxIndexPlusHundred(t *testing.T) { - _, err := serial.Decode(serial.MaxIndex + 100) + _, err := serial.Encode(serial.MaxIndex + 100) if !errors.Is(err, serial.ErrIndexOutOfRange) { t.Fatalf("expected index out of range error, got: %v", err) } } func TestEncodeRejectsOldFormatWithThirdDash(t *testing.T) { - _, err := serial.Encode("AA-100-AA") + _, err := serial.Decode("AA-100-AAA") if !errors.Is(err, serial.ErrInvalidFormat) { t.Fatalf("expected invalid format error, got: %v", err) } } func TestEncodeRejectsLowercaseLetters(t *testing.T) { - _, err := serial.Encode("AAa-100-AA") - if !errors.Is(err, serial.ErrInvalidLetters) { + _, err := serial.Decode("AAa-100-AAA") + if !errors.Is(err, serial.ErrInvalidLetter) { t.Fatalf("expected invalid letters error, got: %v", err) } } func TestEncodeRejectsInvalidChecksum(t *testing.T) { - _, err := serial.Encode("AAA-100-AB") + _, err := serial.Decode("AAA-100-AAB") if !errors.Is(err, serial.ErrInvalidChecksum) { t.Fatalf("expected invalid checksum error, got: %v", err) } } func TestEncodeRejectsNonDigitInNumber(t *testing.T) { - _, err := serial.Encode("AAA-1A0-AA") + _, err := serial.Decode("AAA-1A0-AAA") if !errors.Is(err, serial.ErrInvalidNumber) { t.Fatalf("expected invalid number error, got: %v", err) } } func TestCompleteCodeRoundtrip(t *testing.T) { - original, err := serial.Decode(123456) + original, err := serial.Encode(123456) if err != nil { - t.Fatalf("Decode failed: %v", err) + t.Fatalf("Encode failed: %v", err) } withoutChecksum := original[:len(original)-1] @@ -208,9 +442,9 @@ func TestCompleteCodeRoundtrip(t *testing.T) { t.Fatalf("completed code mismatch: got %q, want %q", full, original) } - idx2, err := serial.Encode(full) + idx2, err := serial.Decode(full) if err != nil { - t.Fatalf("Encode failed for completed code %q: %v", full, err) + t.Fatalf("Decode failed for completed code %q: %v", full, err) } if idx2 != idx { t.Fatalf("idx mismatch: got %d, want %d", idx, idx2) @@ -218,7 +452,7 @@ func TestCompleteCodeRoundtrip(t *testing.T) { } func TestCompleteCodeRejectsInvalidFormat(t *testing.T) { - _, _, err := serial.CompleteCode("AAA-10-A") + _, _, err := serial.CompleteCode("AAA-10-AA") if !errors.Is(err, serial.ErrInvalidFormatNoChecksum) { t.Fatalf("expected invalid no-checksum format error, got: %v", err) } @@ -231,18 +465,18 @@ func TestRandomCodeRoundtrip(t *testing.T) { t.Fatalf("RandomCode failed: %v", err) } - idx2, err := serial.Encode(code) + idx2, err := serial.Decode(code) if err != nil { - t.Fatalf("Encode failed for random code %q: %v", code, err) + t.Fatalf("Decode failed for random code %q: %v", code, err) } if idx2 != idx { t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2) } - decoded, err := serial.Decode(idx) + decoded, err := serial.Encode(idx) if err != nil { - t.Fatalf("Decode failed for idx %d: %v", idx, err) + t.Fatalf("Encode failed for idx %d: %v", idx, err) } if decoded != code { @@ -254,7 +488,7 @@ func TestRandomCodeRoundtrip(t *testing.T) { func TestRandomCodeInvokesInUseCallback(t *testing.T) { called := false - code, idx, err := serial.RandomCode(func(candidate uint32) bool { + code, idx, err := serial.RandomCode(func(candidate uint) bool { called = true return false }) @@ -265,9 +499,9 @@ func TestRandomCodeInvokesInUseCallback(t *testing.T) { t.Fatalf("expected in-use callback to be called") } - idx2, err := serial.Encode(code) + idx2, err := serial.Decode(code) if err != nil { - t.Fatalf("Encode failed for random code %q: %v", code, err) + t.Fatalf("Decode failed for random code %q: %v", code, err) } if idx2 != idx { t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2) @@ -275,7 +509,7 @@ func TestRandomCodeInvokesInUseCallback(t *testing.T) { } func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) { - _, _, err := serial.RandomCode(func(candidate uint32) bool { + _, _, err := serial.RandomCode(func(candidate uint) bool { return true }) if !errors.Is(err, serial.ErrNoAvailableIndex) { @@ -286,7 +520,7 @@ func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) { func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) { _, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{ MaxAttempts: 1, - IsInUse: func(candidate uint32) bool { + IsInUse: func(candidate uint) bool { return true }, }) @@ -297,7 +531,7 @@ func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) { func TestRandomCodeWithOptionsRandomSourceFailure(t *testing.T) { _, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{ - RandomIndex: func(max uint32) (uint32, error) { + RandomIndex: func(max uint) (uint, error) { return 0, fmt.Errorf("rng down") }, })