## Copyright (c) 2021-2024 Status Research & Development GmbH ## Licensed under either of ## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) ## * MIT license ([LICENSE-MIT](LICENSE-MIT)) ## at your option. ## This file may not be copied, modified, or distributed except according to ## those terms. ## This module implements BASE10 (decimal) encoding and decoding procedures. ## ## Encoding procedures are adopted versions of C functions described here: ## # https://www.facebook.com/notes/facebook-engineering/three-optimization-tips-for-c/10151361643253920 import pkg/results export results {.push raises: [].} type Base10* = object func maxLen*(T: typedesc[Base10], I: type): int8 = ## The maximum number of bytes needed to encode any value of type I when I is uint8: 3 elif I is uint16: 5 elif I is uint32: 10 elif I is uint64: 20 else: when sizeof(uint) == 4: 10 else: 20 type Base10Buf*[T: SomeUnsignedInt] = object data*: array[maxLen(Base10, T), byte] len*: int8 # >= 1 when holding valid unsigned integer func decode*[A: byte|char, T: SomeUnsignedInt]( B: typedesc[Base10], t: typedesc[T], src: openArray[A]): Result[T, cstring] = ## Convert base10 encoded string or array of bytes to unsigned integer. const MaxValue = T(high(T) div 10) MaxNumber = T(high(T) - MaxValue * 10) if len(src) == 0: return err("Missing decimal value") var v = T(0) for i in 0 ..< len(src): let ch = when A is char: byte(src[i]) else: src[i] let d = if (ch >= ord('0')) and (ch <= ord('9')): T(ch - ord('0')) else: return err("Non-decimal character encountered") if (v > MaxValue) or (v == MaxValue and T(d) > MaxNumber): return err("Integer overflow") v = (v shl 3) + (v shl 1) + T(d) ok(v) func encodedLength*(B: typedesc[Base10], value: SomeUnsignedInt): int8 = ## Procedure returns number of characters needed to encode integer ``value``. when type(value) is uint8: if value < 10'u8: return 1'i8 if value < 100'u8: return 2'i8 3'i8 elif type(value) is uint16: if value < 10'u16: return 1'i8 if value < 100'u16: return 2'i8 if value < 1000'u16: return 3'i8 if value < 10000'u16: return 4'i8 5'i8 elif (type(value) is uint32) or ((type(value) is uint) and (sizeof(uint) == 4)): const P04 = 1_0000'u32 P05 = 1_0000_0'u32 P06 = 1_0000_00'u32 P07 = 1_0000_000'u32 P08 = 1_0000_0000'u32 P09 = 1_0000_0000_0'u32 if value < 10'u32: return 1'i8 if value < 100'u32: return 2'i8 if value < 1000'u32: return 3'i8 if value < P08: if value < P06: if value < P04: return 4'i8 return 5'i8 + (if value >= P05: 1'i8 else: 0'i8) return 7'i8 + (if value >= P07: 1'i8 else: 0'i8) 9'i8 + (if value >= P09: 1'i8 else: 0'i8) elif (type(value) is uint64) or ((type(value) is uint) and (sizeof(uint) == 8)): const P04 = 1_0000'u64 P05 = 1_0000_0'u64 P06 = 1_0000_00'u64 P07 = 1_0000_000'u64 P08 = 1_0000_0000'u64 P09 = 1_0000_0000_0'u64 P10 = 1_0000_0000_00'u64 P11 = 1_0000_0000_000'u64 P12 = 1_0000_0000_0000'u64 if value < 10'u64: return 1'i8 if value < 100'u64: return 2'i8 if value < 1000'u64: return 3'i8 if value < P12: if value < P08: if value < P06: if value < P04: return 4'i8 return 5'i8 + (if value >= P05: 1'i8 else: 0) return 7'i8 + (if value >= P07: 1'i8 else: 0) if value < P10: return 9'i8 + (if value >= P09: 1'i8 else: 0) return 11'i8 + (if value >= P11: 1'i8 else: 0) return 12'i8 + B.encodedLength(value div P12) func encode[A: byte|char](B: typedesc[Base10], value: SomeUnsignedInt, output: var openArray[A], length: int8): Result[int8, cstring] = const Digits = cstring( "0001020304050607080910111213141516171819" & "2021222324252627282930313233343536373839" & "4041424344454647484950515253545556575859" & "6061626364656667686970717273747576777879" & "8081828384858687888990919293949596979899" ) if len(output) < length: return err("Not enough space to store decimal value") var v = value var next = length - 1 while v >= type(value)(100): let index = uint8((v mod type(value)(100)) shl 1) v = v div type(value)(100) when A is char: output[next] = Digits[index + 1] output[next - 1] = Digits[index] else: output[next] = byte(Digits[index + 1]) output[next - 1] = byte(Digits[index]) dec(next, 2) if v < type(value)(10): when A is char: output[next] = char(ord('0') + (v and type(value)(0x0F))) else: output[next] = byte('0') + byte(v and type(value)(0x0F)) else: let index = uint8(v) shl 1 when A is char: output[next] = Digits[index + 1] output[next - 1] = Digits[index] else: output[next] = byte(Digits[index + 1]) output[next - 1] = byte(Digits[index]) ok(length) func encode*[A: byte|char](B: typedesc[Base10], value: SomeUnsignedInt, output: var openArray[A]): Result[int8, cstring] = ## Encode integer value to array of characters or bytes. B.encode(value, output, B.encodedLength(value)) func toString*(B: typedesc[Base10], value: SomeUnsignedInt): string = ## Encode integer value ``value`` to string. var buf = newString(B.encodedLength(value)) # Buffer of proper size is allocated, so error is not possible discard B.encode(value, buf, int8(len(buf))) buf func toBytes*[I: SomeUnsignedInt](B: typedesc[Base10], v: I): Base10Buf[I] {. noinit.} = ## Encode integer value ``value`` to array of bytes. let res = B.encode(v, result.data, B.encodedLength(v)) result.len = int8(res.get()) func toBytes*[I: SomeUnsignedInt](v: I, B: typedesc[Base10]): Base10Buf[I] {. noinit.} = ## Encode integer value ``value`` to array of bytes. let res = B.encode(v, result.data, B.encodedLength(v)) result.len = int8(res.get())