// Numeric Compaction (ISO/IEC 15438 4.4.4).
//
// The numeric string is split into chunks of at most 44 digits. Each chunk
// is prefixed with a literal "1" and then converted to base 900 by repeated
// division; that prefix is what removes the ambiguity caused by leading
// zeros, so "0001234" and "1234" get different codeword sequences.
//
// The conversion uses plain decimal long division, so no big integer library
// is needed.
///|
/// Number of decimal digits converted per numeric codeword block.
pub let numeric_chunk_size : Int = 44
///|
/// Divide a base 10 digit array by 900, returning the quotient digits and the
/// remainder. Input and output are normal order (most significant first).
fn divide_by_900(digits : Array[Int]) -> (Array[Int], Int) {
let quotient : Array[Int] = []
let mut remainder = 0
for digit in digits {
let value = remainder * 10 + digit
let next = value / 900
remainder = value % 900
if !quotient.is_empty() || next != 0 {
quotient.push(next)
}
}
(quotient, remainder)
}
///|
/// Convert one digit chunk to base 900 digits: the chunk gets a leading "1"
/// and is then repeatedly divided by 900.
pub fn decimal_chunk_to_base900(digits : Array[Int]) -> Array[Int] {
let mut decimal : Array[Int] = [1]
for digit in digits {
decimal.push(digit)
}
let reverse : Array[Int] = []
while !decimal.is_empty() {
let (quotient, remainder) = divide_by_900(decimal)
reverse.push(remainder)
decimal = quotient
}
let result : Array[Int] = []
let mut index = reverse.length()
while index > 0 {
index -= 1
result.push(reverse[index])
}
result
}
///|
/// Convert base 900 digits back to decimal digits. `base900` is most
/// significant first; the result has no leading zeros except for the value 0.
pub fn base900_to_decimal(base900 : Array[Int]) -> Array[Int] {
let mut decimal : Array[Int] = [0]
for word in base900 {
// decimal = decimal * 900 + word, computed least significant first.
let next : Array[Int] = []
let mut add = word
let mut index = decimal.length()
while index > 0 {
index -= 1
let value = decimal[index] * 900 + add
next.push(value % 10)
add = value / 10
}
while add > 0 {
next.push(add % 10)
add = add / 10
}
let ordered : Array[Int] = []
let mut position = next.length()
while position > 0 {
position -= 1
ordered.push(next[position])
}
decimal = ordered
}
let trimmed : Array[Int] = []
let mut started = false
for digit in decimal {
if digit != 0 || started {
started = true
trimmed.push(digit)
}
}
if trimmed.is_empty() {
trimmed.push(0)
}
trimmed
}
///|
/// Encode decimal digits with Numeric Compaction. The latch codeword is not
/// included; `encode_high_level` adds it.
///
/// `input` must be non-empty and contain only ASCII digits.
pub fn encode_numeric(input : Array[Int]) -> Array[Int] raise EncodeError {
encode_numeric_segment(input, 0, input.length())
}
///|
/// Encode `input[start..start+count)` with Numeric Compaction.
pub fn encode_numeric_segment(
input : Array[Int],
start : Int,
count : Int,
) -> Array[Int] raise EncodeError {
if count <= 0 {
raise EmptyInput
}
let digits : Array[Int] = []
let mut index = 0
while index < count {
let code = input[start + index]
if !is_digit(code) {
raise UnrepresentableChar(code)
}
digits.push(code - 48)
index += 1
}
let result : Array[Int] = []
let mut offset = 0
while offset < digits.length() {
let end = if offset + numeric_chunk_size < digits.length() {
offset + numeric_chunk_size
} else {
digits.length()
}
let chunk : Array[Int] = []
while offset < end {
chunk.push(digits[offset])
offset += 1
}
for word in decimal_chunk_to_base900(chunk) {
result.push(word)
}
}
result
}
///|
/// Decode Numeric Compaction codewords back to decimal digits.
///
/// The caller passes exactly the codewords that belong to the numeric
/// segment, without the latch codeword.
pub fn decode_numeric(words : Array[Int]) -> Array[Int] raise EncodeError {
if words.is_empty() {
raise EmptyInput
}
let digits : Array[Int] = []
let mut offset = 0
while offset < words.length() {
let end = if offset + numeric_chunk_size < words.length() {
offset + numeric_chunk_size
} else {
words.length()
}
let chunk : Array[Int] = []
while offset < end {
chunk.push(words[offset])
offset += 1
}
let decimal = base900_to_decimal(chunk)
// The leading "1" prefix is not part of the original digits.
let mut index = 0
let mut skipped = false
while index < decimal.length() {
if !skipped && decimal[index] == 1 {
skipped = true
} else {
digits.push(decimal[index] + 48)
}
index += 1
}
if !skipped {
raise UnrepresentableChar(-1)
}
}
digits
}
///|
/// Numeric encoding without error handling: returns `None` on failure.
pub fn encode_numeric_opt(input : Array[Int]) -> Array[Int]? {
try encode_numeric(input) catch {
_ => None
} noraise {
words => Some(words)
}
}