namespace Mbproxy.Bcd; /// /// Pure, allocation-free codec for DirectLOGIC BCD register encoding/decoding. /// /// 16-bit BCD: one register holds 4 BCD digits (0–9999). /// Wire value 0x1234 decodes to decimal 1234. /// /// 32-bit BCD (CDAB word order, low-word-first): /// Register at Address = low 4 BCD digits (least-significant). /// Register at Address+1 = high 4 BCD digits (most-significant). /// Decoded decimal = Decode16(high) * 10_000 + Decode16(low). /// Example: 12_345_678 → low=0x5678, high=0x1234. /// /// Bad-nibble policy: Decode16/Decode32 throw /// (not a sentinel). The Phase 04 rewrite pipeline catches and surfaces the /// exception as an mbproxy.rewrite.invalid_bcd warning event. /// internal static class BcdCodec { private const int Max16 = 9_999; private const int Max32 = 99_999_999; // ── Encode ────────────────────────────────────────────────────────────── /// /// Encodes a non-negative integer in [0, 9999] to a 16-bit BCD register. /// E.g. 1234 → 0x1234. /// /// value < 0 or value > 9999. public static ushort Encode16(int value) { if ((uint)value > Max16) throw new ArgumentOutOfRangeException(nameof(value), value, $"BCD-16 value must be in [0, {Max16}]; got {value}."); // Pack four decimal digits into four BCD nibbles. int d3 = value / 1000; int d2 = (value / 100) % 10; int d1 = (value / 10) % 10; int d0 = value % 10; return (ushort)((d3 << 12) | (d2 << 8) | (d1 << 4) | d0); } /// /// Encodes a non-negative integer in [0, 99_999_999] to a CDAB BCD register pair. /// Returns (low, high) where low holds the 4 least-significant BCD digits and /// high holds the 4 most-significant BCD digits. /// E.g. 12_345_678 → (low: 0x5678, high: 0x1234). /// /// value < 0 or value > 99_999_999. public static (ushort low, ushort high) Encode32(int value) { if ((uint)value > Max32) throw new ArgumentOutOfRangeException(nameof(value), value, $"BCD-32 value must be in [0, {Max32}]; got {value}."); int lo = value % 10_000; // low 4 decimal digits int hi = value / 10_000; // high 4 decimal digits return (Encode16(lo), Encode16(hi)); } // ── Decode ────────────────────────────────────────────────────────────── /// /// Decodes a 16-bit BCD register to a non-negative integer. /// E.g. 0x1234 → 1234. /// /// Any nibble is >= 0xA (not a valid BCD digit). public static int Decode16(ushort raw) { // Validate all four nibbles first (fail fast with the raw value in the message). if (HasBadNibble(raw)) throw new FormatException( $"Register value 0x{raw:X4} is not valid BCD: one or more nibbles are >= 0xA."); int d3 = (raw >> 12) & 0xF; int d2 = (raw >> 8) & 0xF; int d1 = (raw >> 4) & 0xF; int d0 = raw & 0xF; return d3 * 1000 + d2 * 100 + d1 * 10 + d0; } /// /// Decodes a CDAB BCD register pair to a non-negative integer. /// = low 4 BCD digits; = high 4 BCD digits. /// E.g. (low: 0x5678, high: 0x1234) → 12_345_678. /// /// Either word has a bad nibble. public static int Decode32(ushort low, ushort high) { // Decode high first: if it throws, we skip decoding low unnecessarily. // But the spec says "throws once with the raw value" per word, so we decode // in natural order. Decode16 throws on the first bad word it encounters. int hiVal = Decode16(high); int loVal = Decode16(low); return hiVal * 10_000 + loVal; } // ── Private helpers ───────────────────────────────────────────────────── /// Returns true if any nibble in is >= 0xA. private static bool HasBadNibble(ushort raw) { // Check each nibble independently. return ((raw >> 12) & 0xF) >= 0xA || ((raw >> 8) & 0xF) >= 0xA || ((raw >> 4) & 0xF) >= 0xA || (raw & 0xF) >= 0xA; } }