///|
/// Validate a string containing decimal digits only.
pub fn validate_digits(field : Int, text : String) -> Result[Unit, IsoError] {
for i = 0; i < text.length(); i = i + 1 {
if !ascii_digit(text[i]) {
return Err(InvalidCharacter(field, i, "decimal"))
}
}
Ok(())
}
///|
/// Pack decimal digits into BCD. Odd values are padded on the requested side.
pub fn bcd_pack_digits(
field : Int,
digits : String,
pad_left : Bool,
pad_nibble : Int,
) -> Result[Bytes, IsoError] {
match validate_digits(field, digits) {
Err(error) => return Err(error)
Ok(_) => ()
}
if pad_nibble < 0 || pad_nibble > 15 {
return Err(InvalidBcd(0, pad_nibble))
}
let odd = digits.length() % 2 == 1
let byte_count = (digits.length() + 1) / 2
let output : Array[Byte] = []
for byte_index = 0; byte_index < byte_count; byte_index = byte_index + 1 {
let digit_index = byte_index * 2
let high = if odd && pad_left && byte_index == 0 {
pad_nibble
} else {
let index = if odd && pad_left { digit_index - 1 } else { digit_index }
digits[index].to_int() - 48
}
let low = if odd && !pad_left && byte_index == byte_count - 1 {
pad_nibble
} else {
let index = if odd && pad_left { digit_index } else { digit_index + 1 }
digits[index].to_int() - 48
}
output.push(((high << 4) | low).to_byte())
}
Ok(Bytes::from_array(output))
}
///|
/// Unpack BCD bytes into decimal digits.
pub fn bcd_unpack_digits(
field : Int,
data : Bytes,
digit_count : Int,
pad_left : Bool,
pad_nibble : Int,
) -> Result[String, IsoError] {
if digit_count < 0 || data.length() != (digit_count + 1) / 2 {
return Err(InvalidLength(field, (digit_count + 1) / 2, data.length()))
}
let odd = digit_count % 2 == 1
let out = StringBuilder()
for i = 0; i < data.length(); i = i + 1 {
let value = data[i].to_int()
let high = (value >> 4) & 15
let low = value & 15
if odd && pad_left && i == 0 {
if high != pad_nibble {
return Err(InvalidBcd(i * 2, high))
}
} else {
if high > 9 {
return Err(InvalidBcd(i * 2, high))
}
out.write_char((48 + high).unsafe_to_char())
}
if odd && !pad_left && i == data.length() - 1 {
if low != pad_nibble {
return Err(InvalidBcd(i * 2 + 1, low))
}
} else {
if low > 9 {
return Err(InvalidBcd(i * 2 + 1, low))
}
out.write_char((48 + low).unsafe_to_char())
}
}
Ok(out.to_string())
}
///|
/// Pack an LLVAR or LLLVAR length into BCD bytes.
pub fn bcd_pack_length(value : Int, digits : Int) -> Result[Bytes, IsoError] {
if digits != 2 && digits != 3 {
return Err(LengthPrefixOverflow(digits, value))
}
let text = match decimal_width(value, digits) {
Ok(text) => text
Err(error) => return Err(error)
}
bcd_pack_digits(0, text, true, 0)
}
///|
/// Decode a BCD length prefix and return value plus consumed bytes.
pub fn bcd_unpack_length(
data : Bytes,
start : Int,
digits : Int,
) -> Result[(Int, Int), IsoError] {
if digits != 2 && digits != 3 {
return Err(LengthPrefixOverflow(digits, 0))
}
let bytes_needed = (digits + 1) / 2
if start < 0 || start + bytes_needed > data.length() {
return Err(
Truncated(
"BCD length prefix",
bytes_needed,
Int::max(0, data.length() - start),
),
)
}
let raw = bytes_slice(data, start, start + bytes_needed)
let text = match bcd_unpack_digits(0, raw, digits, true, 0) {
Ok(text) => text
Err(error) => return Err(error)
}
let mut value = 0
for i = 0; i < text.length(); i = i + 1 {
value = value * 10 + text[i].to_int() - 48
}
Ok((value, bytes_needed))
}
///|
/// Pack signed decimal text using a trailing C/D sign nibble.
pub fn bcd_pack_signed(field : Int, text : String) -> Result[Bytes, IsoError] {
if text.length() < 2 {
return Err(InvalidNumeric(field, text))
}
let negative = text[0] == '-'
let positive = text[0] == '+'
if !negative && !positive {
return Err(InvalidNumeric(field, text))
}
let digits = text[1:].to_owned()
match validate_digits(field, digits) {
Err(error) => return Err(error)
Ok(_) => ()
}
let nibbles : Array[Int] = []
if digits.length() % 2 == 0 {
nibbles.push(0)
}
for i = 0; i < digits.length(); i = i + 1 {
nibbles.push(digits[i].to_int() - 48)
}
nibbles.push(if negative { 13 } else { 12 })
let out : Array[Byte] = []
let mut index = 0
while index < nibbles.length() {
out.push(((nibbles[index] << 4) | nibbles[index + 1]).to_byte())
index += 2
}
Ok(Bytes::from_array(out))
}
///|
/// Decode signed BCD text using a trailing C/D sign nibble.
pub fn bcd_unpack_signed(
field : Int,
data : Bytes,
digits : Int,
) -> Result[String, IsoError] {
let expected = (digits + 2) / 2
if digits <= 0 || data.length() != expected {
return Err(InvalidLength(field, expected, data.length()))
}
let nibbles : Array[Int] = []
for byte in data {
nibbles.push((byte.to_int() >> 4) & 15)
nibbles.push(byte.to_int() & 15)
}
let digit_start = if digits % 2 == 0 { 1 } else { 0 }
if digit_start == 1 && nibbles[0] != 0 {
return Err(InvalidBcd(0, nibbles[0]))
}
let sign = nibbles[nibbles.length() - 1]
let out = StringBuilder()
if sign == 13 {
out.write_char('-')
} else if sign == 12 || sign == 15 {
out.write_char('+')
} else {
return Err(InvalidBcd(nibbles.length() - 1, sign))
}
for i = digit_start; i < nibbles.length() - 1; i = i + 1 {
let nibble = nibbles[i]
if nibble > 9 {
return Err(InvalidBcd(i, nibble))
}
out.write_char((48 + nibble).unsafe_to_char())
}
Ok(out.to_string())
}