From 027307383ea8f2488843c1fe4350f087c6f5dda5 Mon Sep 17 00:00:00 2001 From: micha Date: Fri, 20 Feb 2026 16:47:07 +0100 Subject: [PATCH] Initial version --- serial/serial.go | 61 ++++++++++++++++++++ serial/serial_test.go | 128 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 serial/serial.go create mode 100644 serial/serial_test.go diff --git a/serial/serial.go b/serial/serial.go new file mode 100644 index 0000000..cf4e7ea --- /dev/null +++ b/serial/serial.go @@ -0,0 +1,61 @@ +const ( + letters = 26 + numbers = 900 // 100–999 + block = letters * letters * letters * numbers // 26^3 * 900 +) + +func Encode(code string) (uint32, error) { + // Verwacht formaat: L1L2-NNN-L3-C => lengte 10, posities: 0,1,3,4,5,7,9 + if len(code) != 10 || code[2] != '-' || code[6] != '-' || code[8] != '-' { + return 0, errors.New("invalid format, expected LL-NNN-L-C") + } + + l1, l2, l3 := code[0], code[1], code[7] + + // letters checken + // forbidden alleen op l1,l2 + // nummer parsen uit code[3:6] + + L1 := int(l1 - 'A') + L2 := int(l2 - 'A') + L3 := int(l3 - 'A') + N := num - 100 + + idx := uint32((((L1*letters + L2)*letters + L3) * numbers) + N) + + if code[9] != checksumLetter(idx) { + return 0, errors.New("invalid checksum") + } + + return idx, nil +} + +func Decode(idx uint32) (string, error) { + if idx >= block { + return "", errors.New("index out of range") + } + + x := int(idx) + + L1 := x / (letters * letters * numbers) + r1 := x % (letters * letters * numbers) + + L2 := r1 / (letters * numbers) + r2 := r1 % (letters * numbers) + + L3 := r2 / numbers + N := r2 % numbers + + l1 := byte('A' + L1) + l2 := byte('A' + L2) + l3 := byte('A' + L3) + + if isForbiddenPair(l1, l2) { + return "", errors.New("forbidden letter combination") + } + + c := checksumLetter(idx) + + return fmt.Sprintf("%c%c-%03d-%c-%c", l1, l2, N+100, l3, c), nil +} + diff --git a/serial/serial_test.go b/serial/serial_test.go new file mode 100644 index 0000000..905198c --- /dev/null +++ b/serial/serial_test.go @@ -0,0 +1,128 @@ +package serial_test + +import ( + "testing" + "testing/quick" + + "yourmodule/serial" +) + +// Property: Decode(Encode(code)) == code (voor geldige codes) +func TestEncodeDecodeRoundtrip(t *testing.T) { + f := func(idx uint32) bool { + if idx >= serial.MaxIndex { + return true + } + + code, err := serial.Decode(idx) + if err != nil { + return false + } + + idx2, err := serial.Encode(code) + if err != nil { + return false + } + + return idx == idx2 + } + + if err := quick.Check(f, nil); err != nil { + t.Fatalf("Encode/Decode roundtrip failed: %v", err) + } +} + +// Property: Decode(Encode(Decode(i))) == Decode(i) (idempotent op geldige indices) +func TestDecodeEncodeDecodeIdempotent(t *testing.T) { + f := func(idx uint32) bool { + if idx >= serial.MaxIndex { + return true + } + + code1, err := serial.Decode(idx) + if err != nil { + return true // decode mag indices weigeren (bv. forbidden pair) + } + + idx2, err := serial.Encode(code1) + if err != nil { + return false + } + + code2, err := serial.Decode(idx2) + if err != nil { + return false + } + + return code1 == code2 + } + + if err := quick.Check(f, nil); err != nil { + t.Fatalf("Decode/Encode/Decode idempotence failed: %v", err) + } +} + +// Property: forbidden eerste twee letters komen nooit uit Decode +func TestForbiddenPairsNeverDecoded(t *testing.T) { + f := func(idx uint32) bool { + if idx >= serial.MaxIndex { + return true + } + + code, err := serial.Decode(idx) + if err != nil { + return true // decode mag indices weigeren + } + + l1 := code[0] + l2 := code[1] + + return !serial.IsForbiddenPair(l1, l2) + } + + if err := quick.Check(f, nil); err != nil { + t.Fatalf("Forbidden pairs appeared in Decode: %v", err) + } +} + +// Optional: basis-check op formaat LL-NNN-L-C +func TestDecodedFormatLooksCorrect(t *testing.T) { + f := func(idx uint32) bool { + if idx >= serial.MaxIndex { + return true + } + + code, err := serial.Decode(idx) + if err != nil { + return true + } + + if len(code) != 10 { + return false + } + if code[2] != '-' || code[6] != '-' || code[8] != '-' { + return false + } + + // letters op posities 0,1,7,9 + for _, p := range []int{0, 1, 7, 9} { + if code[p] < 'A' || code[p] > 'Z' { + return false + } + } + + // cijfers op posities 3,4,5 + for _, p := range []int{3, 4, 5} { + if code[p] < '0' || code[p] > '9' { + return false + } + } + + return true + } + + if err := quick.Check(f, nil); err != nil { + t.Fatalf("Decoded format property failed: %v", err) + } +} +