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)
Hint 1: Extracting nibbles
To get the high nibble (first digit): (byte >> 4) & 0x0F
To get the low nibble (second digit): byte & 0x0F
Hint 2: Combining nibbles
To pack two digits into one byte: (highDigit << 4) | lowDigit
Hint 3: Digit to nibble
If you have a character '5' (ASCII 0x35), the numeric value is '5' - '0' = 5.