Skip to main content

dvb_common/
bcd.rs

1//! Binary-coded decimal (BCD) codec for DVB wire fields.
2//!
3//! DVB packs many numeric fields as BCD — each 4-bit nibble holds one decimal
4//! digit (0–9). Examples: the HHMMSS of a UTC time, the frequency / symbol-rate
5//! of the delivery-system descriptors, the HHMM of a local-time offset. These
6//! helpers convert between the packed BCD representation and plain integers in
7//! both directions so callers never hand-decode nibbles.
8//!
9//! Every decode has a symmetric encode; both reject out-of-range input by
10//! returning `None` rather than producing garbage.
11
12/// Largest number of BCD nibbles representable in the [`bcd_to_decimal`] /
13/// [`decimal_to_bcd`] `u64` carrier.
14pub const MAX_NIBBLES: u8 = 16;
15
16/// Decode a packed-BCD byte (two nibbles) to `0..=99`.
17///
18/// Returns `None` if either nibble is greater than 9.
19#[must_use]
20pub fn from_bcd_byte(byte: u8) -> Option<u8> {
21    bcd_to_decimal(u64::from(byte), 2).map(|v| v as u8)
22}
23
24/// Encode `0..=99` to a packed-BCD byte.
25///
26/// Returns `None` if `value > 99`.
27#[must_use]
28pub fn to_bcd_byte(value: u8) -> Option<u8> {
29    decimal_to_bcd(u64::from(value), 2).map(|v| v as u8)
30}
31
32/// Decode the low `nibbles` BCD digits of `raw` to a decimal value.
33///
34/// Each nibble (most-significant first) contributes one decimal digit. Returns
35/// `None` if any of those nibbles is greater than 9, or if `nibbles` exceeds
36/// [`MAX_NIBBLES`].
37#[must_use]
38pub fn bcd_to_decimal(raw: u64, nibbles: u8) -> Option<u64> {
39    if nibbles > MAX_NIBBLES {
40        return None;
41    }
42    let mut acc = 0u64;
43    for i in (0..nibbles).rev() {
44        let digit = (raw >> (i * 4)) & 0x0F;
45        if digit > 9 {
46            return None;
47        }
48        acc = acc * 10 + digit;
49    }
50    Some(acc)
51}
52
53/// Encode `value` as `nibbles` packed-BCD digits in the low bits of a `u64`.
54///
55/// Returns `None` if `value` needs more than `nibbles` decimal digits, or if
56/// `nibbles` exceeds [`MAX_NIBBLES`].
57#[must_use]
58pub fn decimal_to_bcd(value: u64, nibbles: u8) -> Option<u64> {
59    if nibbles > MAX_NIBBLES {
60        return None;
61    }
62    let mut packed = 0u64;
63    let mut remaining = value;
64    for i in 0..nibbles {
65        let digit = remaining % 10;
66        packed |= digit << (i * 4);
67        remaining /= 10;
68    }
69    if remaining != 0 {
70        // value had more digits than `nibbles` could hold.
71        return None;
72    }
73    Some(packed)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn byte_round_trips_across_full_range() {
82        for v in 0..=99u8 {
83            let bcd = to_bcd_byte(v).expect("0..=99 encodes");
84            assert_eq!(from_bcd_byte(bcd), Some(v), "round-trip {v}");
85        }
86    }
87
88    #[test]
89    fn from_bcd_byte_rejects_non_decimal_nibbles() {
90        assert_eq!(from_bcd_byte(0x1A), None);
91        assert_eq!(from_bcd_byte(0xA1), None);
92        assert_eq!(from_bcd_byte(0x99), Some(99));
93    }
94
95    #[test]
96    fn to_bcd_byte_rejects_over_99() {
97        assert_eq!(to_bcd_byte(100), None);
98        assert_eq!(to_bcd_byte(99), Some(0x99));
99    }
100
101    #[test]
102    fn multi_nibble_round_trips() {
103        // 8 BCD nibbles in a u32-shaped value (satellite frequency: 11725000).
104        let raw = 0x1172_5000u64;
105        assert_eq!(bcd_to_decimal(raw, 8), Some(11_725_000));
106        assert_eq!(decimal_to_bcd(11_725_000, 8), Some(raw));
107    }
108
109    #[test]
110    fn bcd_to_decimal_rejects_bad_nibble() {
111        assert_eq!(bcd_to_decimal(0x000A_0000, 8), None);
112    }
113
114    #[test]
115    fn decimal_to_bcd_rejects_overflow() {
116        // 9 digits won't fit in 8 nibbles.
117        assert_eq!(decimal_to_bcd(100_000_000, 8), None);
118        assert_eq!(decimal_to_bcd(99_999_999, 8), Some(0x9999_9999));
119    }
120}