Added CompleteCode function.
All checks were successful
CI / test (push) Successful in 17s

This commit is contained in:
2026-02-21 04:19:19 +01:00
parent 50e9036400
commit 22ca29b4af
5 changed files with 110 additions and 10 deletions

View File

@@ -31,3 +31,11 @@ jobs:
- name: Test
run: go test -v ./...
- name: Build
run: go build -o ./bin/triplex-example ./cmd/triplex-example
- name: Run example
run: ./bin/triplex-example && ./bin/triplex-example --complete ABC-1234-D && echo "Example ran successfully"

View File

@@ -9,7 +9,7 @@ Copyright (c) 2026 Micha Hoiting
high-integrity identifiers. It maps a compact 32-bit integer space to a
human-readable serial format:
```
```text
LLL-NNN-LC
```
@@ -18,6 +18,12 @@ Where:
- `N` = number (`100``999`)
- `C` = checksum letter (`A``Z`)
For instance
```text
ABC-123-DT
```
## What it provides
- 4 data letters
@@ -77,6 +83,8 @@ func main() {
- 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.
- If provided, the callback is used to skip indices already in use by the client.
@@ -108,6 +116,7 @@ make test
Exported errors:
- `serial.ErrInvalidFormat`
- `serial.ErrInvalidFormatNoChecksum`
- `serial.ErrInvalidLetters`
- `serial.ErrForbiddenLetterTriplet`
- `serial.ErrInvalidNumber`

View File

@@ -6,6 +6,7 @@ import (
"crypto/rand"
"encoding/binary"
"errors"
"flag"
"fmt"
"git.hoiting.org/micha/triplex/serial"
@@ -29,6 +30,21 @@ func myRandomIndex(max uint32) (uint32, error) {
}
func main() {
complete := flag.String("complete", "", "complete code without checksum in format LLL-NNN-L")
flag.Parse()
if *complete != "" {
full, idx, err := serial.CompleteCode(*complete)
if err != nil {
fmt.Println("complete error:", err)
return
}
fmt.Println("code:", full)
fmt.Println("idx:", idx)
return
}
opts := serial.RandomCodeOptions{
MaxAttempts: 500,
IsInUse: func(idx uint32) bool {

View File

@@ -19,15 +19,16 @@ const (
)
var (
ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LC")
ErrInvalidLetters = errors.New("invalid letters")
ErrForbiddenLetterTriplet = errors.New("forbidden letter combination")
ErrInvalidNumber = errors.New("invalid number")
ErrNumberOutOfRange = errors.New("number out of range")
ErrInvalidChecksum = errors.New("invalid checksum")
ErrIndexOutOfRange = errors.New("index out of range")
ErrRandomSourceFailed = errors.New("random source failed")
ErrNoAvailableIndex = errors.New("no available index found")
ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LC")
ErrInvalidFormatNoChecksum = errors.New("invalid format, expected LLL-NNN-L")
ErrInvalidLetters = errors.New("invalid letters")
ErrForbiddenLetterTriplet = errors.New("forbidden letter combination")
ErrInvalidNumber = errors.New("invalid number")
ErrNumberOutOfRange = errors.New("number out of range")
ErrInvalidChecksum = errors.New("invalid checksum")
ErrIndexOutOfRange = errors.New("index out of range")
ErrRandomSourceFailed = errors.New("random source failed")
ErrNoAvailableIndex = errors.New("no available index found")
)
type RandomCodeOptions struct {
@@ -112,6 +113,40 @@ func Decode(idx uint32) (string, error) {
return code, nil
}
func CompleteCode(codeWithoutChecksum string) (string, uint32, error) {
if len(codeWithoutChecksum) != 9 || codeWithoutChecksum[3] != '-' || codeWithoutChecksum[7] != '-' {
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
}
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) {
options := RandomCodeOptions{}
if len(isInUse) > 0 {

View File

@@ -192,6 +192,38 @@ func TestEncodeRejectsNonDigitInNumber(t *testing.T) {
}
}
func TestCompleteCodeRoundtrip(t *testing.T) {
original, err := serial.Decode(123456)
if err != nil {
t.Fatalf("Decode failed: %v", err)
}
withoutChecksum := original[:len(original)-1]
full, idx, err := serial.CompleteCode(withoutChecksum)
if err != nil {
t.Fatalf("CompleteCode failed: %v", err)
}
if full != original {
t.Fatalf("completed code mismatch: got %q, want %q", full, original)
}
idx2, err := serial.Encode(full)
if err != nil {
t.Fatalf("Encode failed for completed code %q: %v", full, err)
}
if idx2 != idx {
t.Fatalf("idx mismatch: got %d, want %d", idx, idx2)
}
}
func TestCompleteCodeRejectsInvalidFormat(t *testing.T) {
_, _, err := serial.CompleteCode("AAA-10-A")
if !errors.Is(err, serial.ErrInvalidFormatNoChecksum) {
t.Fatalf("expected invalid no-checksum format error, got: %v", err)
}
}
func TestRandomCodeRoundtrip(t *testing.T) {
for i := 0; i < 100; i++ {
code, idx, err := serial.RandomCode()