Various improvements and changed encoding/decoding format to LLL-NNN-LLC with reduced character set.
All checks were successful
CI / test (push) Successful in 16s
All checks were successful
CI / test (push) Successful in 16s
This commit is contained in:
247
serial/serial.go
247
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 {
|
||||
func IsForbiddenTriplet(l1, l2, l3 byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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 isUpperAlphaASCII(b byte) bool {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user