pub const MAX_NIBBLES: u8 = 16;
#[must_use]
pub fn from_bcd_byte(byte: u8) -> Option<u8> {
bcd_to_decimal(u64::from(byte), 2).map(|v| v as u8)
}
#[must_use]
pub fn to_bcd_byte(value: u8) -> Option<u8> {
decimal_to_bcd(u64::from(value), 2).map(|v| v as u8)
}
#[must_use]
pub fn bcd_to_decimal(raw: u64, nibbles: u8) -> Option<u64> {
if nibbles > MAX_NIBBLES {
return None;
}
let mut acc = 0u64;
for i in (0..nibbles).rev() {
let digit = (raw >> (i * 4)) & 0x0F;
if digit > 9 {
return None;
}
acc = acc * 10 + digit;
}
Some(acc)
}
#[must_use]
pub fn decimal_to_bcd(value: u64, nibbles: u8) -> Option<u64> {
if nibbles > MAX_NIBBLES {
return None;
}
let mut packed = 0u64;
let mut remaining = value;
for i in 0..nibbles {
let digit = remaining % 10;
packed |= digit << (i * 4);
remaining /= 10;
}
if remaining != 0 {
return None;
}
Some(packed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn byte_round_trips_across_full_range() {
for v in 0..=99u8 {
let bcd = to_bcd_byte(v).expect("0..=99 encodes");
assert_eq!(from_bcd_byte(bcd), Some(v), "round-trip {v}");
}
}
#[test]
fn from_bcd_byte_rejects_non_decimal_nibbles() {
assert_eq!(from_bcd_byte(0x1A), None);
assert_eq!(from_bcd_byte(0xA1), None);
assert_eq!(from_bcd_byte(0x99), Some(99));
}
#[test]
fn to_bcd_byte_rejects_over_99() {
assert_eq!(to_bcd_byte(100), None);
assert_eq!(to_bcd_byte(99), Some(0x99));
}
#[test]
fn multi_nibble_round_trips() {
let raw = 0x1172_5000u64;
assert_eq!(bcd_to_decimal(raw, 8), Some(11_725_000));
assert_eq!(decimal_to_bcd(11_725_000, 8), Some(raw));
}
#[test]
fn bcd_to_decimal_rejects_bad_nibble() {
assert_eq!(bcd_to_decimal(0x000A_0000, 8), None);
}
#[test]
fn decimal_to_bcd_rejects_overflow() {
assert_eq!(decimal_to_bcd(100_000_000, 8), None);
assert_eq!(decimal_to_bcd(99_999_999, 8), Some(0x9999_9999));
}
}