BCD Codec
BCD Codec
You're building a parser for a legacy payment network that uses BCD (Binary-Coded Decimal) encoding. Your team lead hands you a hex dump and says: "The amount field is 6 bytes starting at offset 20, but it's BCD-encoded. Decode it."
You stare at the bytes: 00 00 01 25 50 00
That's not ASCII. Each byte contains two decimal digits packed together. Welcome to BCD.
What is BCD?
BCD packs two decimal digits into a single byte. Each digit (0-9) uses 4 bits (a "nibble"):
Digit Binary | Digit Binary
0 0000 | 5 0101
1 0001 | 6 0110
2 0010 | 7 0111
3 0011 | 8 1000
4 0100 | 9 1001
Two nibbles per byte:
Byte 0x47:
Binary: 0100 0111
↓ ↓
4 7
Decoded: "47"
Byte 0x12:
Binary: 0001 0010
↓ ↓
1 2
Decoded: "12"
So 00 00 01 25 50 00 decodes to "000001255000".
The Challenge
Implement two functions:
- EncodeBCD: Convert a numeric string to BCD bytes
- DecodeBCD: Convert BCD bytes back to a numeric string
What You're Building
// EncodeBCD converts a numeric string to packed BCD bytes.
//
// For odd-length strings, left-pad with '0' before encoding.
// For example: "123" becomes "0123" → 0x01, 0x23
//
// Returns an error if the input contains non-digit characters.
func EncodeBCD(s string) ([]byte, error)
// DecodeBCD converts packed BCD bytes to a numeric string.
//
// Each byte produces exactly 2 digits. Leading zeros are preserved.
// For example: 0x01, 0x23 → "0123"
//
// Returns an error if any nibble is > 9 (invalid BCD).
func DecodeBCD(data []byte) (string, error)
Behavior
EncodeBCD
- Input: String of digits ("0"-"9" only)
- Output: BCD-encoded bytes
- Empty string → empty slice (no error)
- Odd-length strings: Left-pad with '0' first
- "5" → "05" →
0x05 - "123" → "0123" →
0x01, 0x23
- "5" → "05" →
- Non-digit character → error
DecodeBCD
- Input: BCD-encoded bytes
- Output: Numeric string (always even length)
- Empty slice → empty string (no error)
- Each byte produces exactly 2 digits
- Invalid nibble (> 9) → error
Examples
// Basic encoding
EncodeBCD("1234")
// → []byte{0x12, 0x34}, nil
// Odd length - left-padded
EncodeBCD("123")
// → []byte{0x01, 0x23}, nil
// Single digit
EncodeBCD("5")
// → []byte{0x05}, nil
// Amount encoding (common use case)
EncodeBCD("000000010000") // $100.00 in cents
// → []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00}, nil
// Empty string
EncodeBCD("")
// → []byte{}, nil
// Invalid input
EncodeBCD("12A4")
// → nil, error("invalid character 'A' at position 2")
// Basic decoding
DecodeBCD([]byte{0x12, 0x34})
// → "1234", nil
// Preserves leading zeros
DecodeBCD([]byte{0x00, 0x12})
// → "0012", nil
// Invalid BCD (nibble > 9)
DecodeBCD([]byte{0xAB})
// → "", error("invalid BCD nibble 10 in byte 0")
// Empty slice
DecodeBCD([]byte{})
// → "", nil
Edge Cases
- All zeros:
EncodeBCD("0000")→0x00, 0x00 - All nines:
EncodeBCD("9999")→0x99, 0x99 - Leading zeros preserved:
DecodeBCD(0x00, 0x01)→"0001"(not "1") - Single byte: Both
0x00and0x99are valid
Why This Matters
BCD is everywhere in financial systems:
- Amount fields: Many networks transmit amounts in BCD to save bandwidth
- Length prefixes: Some LLVAR implementations use BCD lengths
- Track data: Portions of track 2 data use BCD
- PIN blocks: Part of the PIN block format includes BCD-encoded data
- Date/time fields: Legacy systems often BCD-encode dates
When you see a 6-byte field that should contain a 12-digit amount, but the bytes look like 0x00, 0x00, 0x01, 0x00, 0x00, 0x00 instead of ASCII "000001000000" (which would be 12 bytes), you're looking at BCD.
Hints (only if stuck)
<details> <summary>Hint 1: Extracting nibbles</summary>
To get the high nibble (first digit): (byte >> 4) & 0x0F To get the low nibble (second digit): byte & 0x0F
</details>
<details> <summary>Hint 2: Combining nibbles</summary>
To pack two digits into one byte: (highDigit << 4) | lowDigit
</details>
<details> <summary>Hint 3: Digit to nibble</summary>
If you have a character '5' (ASCII 0x35), the numeric value is '5' - '0' = 5.
</details>