BCD (Binary Coded Decimals): Python parsing library

BCD (Binary Coded Decimals) is a common way to encode integer numbers in a way that makes human-readable output somewhat simpler. In this encoding scheme, every decimal digit is encoded as either a single byte (8 bits), or a nibble (half of a byte, 4 bits). This obviously wastes a lot of bits, but it makes translation into human-readable string much easier than traditional binary-to-decimal conversion process, which includes lots of divisions by 10.

For example, encoding integer 31337 in 8-digit, 8 bits per digit, big endian order of digits BCD format yields

00 00 00 03 01 03 03 07

Encoding the same integer as 8-digit, 4 bits per digit, little endian order BCD format would yield:

73 31 30 00

Using this type of encoding in Kaitai Struct is pretty straightforward: one calls for this type, specifying desired encoding parameters, and gets result using either as_int or as_str attributes.

KS implementation details

License: CC0-1.0
Minimal Kaitai Struct required: 0.8

References

This page hosts a formal specification of BCD (Binary Coded Decimals) using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Runtime library

All parsing code for Python generated by Kaitai Struct depends on the Python runtime library. You have to install it before you can parse data.

The Python runtime library can be installed from PyPI:

python3 -m pip install kaitaistruct

Code

Parse a local file and get structure in memory:

data = Bcd.from_file("path/to/local/file.bin")

Or parse structure from a bytes:

from kaitaistruct import KaitaiStream, BytesIO

raw = b"\x00\x01\x02..."
data = Bcd(KaitaiStream(BytesIO(raw)))

After that, one can get various attributes from the structure by invoking getter methods like:

data.as_int # => Value of this BCD number as integer. Endianness would be selected based on `is_le` parameter given.

Python source code to parse BCD (Binary Coded Decimals)

bcd.py

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO


if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9):
    raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__))

class Bcd(KaitaiStruct):
    """BCD (Binary Coded Decimals) is a common way to encode integer
    numbers in a way that makes human-readable output somewhat
    simpler. In this encoding scheme, every decimal digit is encoded as
    either a single byte (8 bits), or a nibble (half of a byte, 4
    bits). This obviously wastes a lot of bits, but it makes translation
    into human-readable string much easier than traditional
    binary-to-decimal conversion process, which includes lots of
    divisions by 10.
    
    For example, encoding integer 31337 in 8-digit, 8 bits per digit,
    big endian order of digits BCD format yields
    
    ```
    00 00 00 03 01 03 03 07
    ```
    
    Encoding the same integer as 8-digit, 4 bits per digit, little
    endian order BCD format would yield:
    
    ```
    73 31 30 00
    ```
    
    Using this type of encoding in Kaitai Struct is pretty
    straightforward: one calls for this type, specifying desired
    encoding parameters, and gets result using either `as_int` or
    `as_str` attributes.
    """
    def __init__(self, num_digits, bits_per_digit, is_le, _io, _parent=None, _root=None):
        self._io = _io
        self._parent = _parent
        self._root = _root if _root else self
        self.num_digits = num_digits
        self.bits_per_digit = bits_per_digit
        self.is_le = is_le
        self._read()

    def _read(self):
        self.digits = []
        for i in range(self.num_digits):
            _on = self.bits_per_digit
            if _on == 4:
                self.digits.append(self._io.read_bits_int_be(4))
            elif _on == 8:
                self.digits.append(self._io.read_u1())


    @property
    def as_int(self):
        """Value of this BCD number as integer. Endianness would be selected based on `is_le` parameter given."""
        if hasattr(self, '_m_as_int'):
            return self._m_as_int

        self._m_as_int = (self.as_int_le if self.is_le else self.as_int_be)
        return getattr(self, '_m_as_int', None)

    @property
    def as_int_le(self):
        """Value of this BCD number as integer (treating digit order as little-endian)."""
        if hasattr(self, '_m_as_int_le'):
            return self._m_as_int_le

        self._m_as_int_le = (self.digits[0] + (0 if self.num_digits < 2 else ((self.digits[1] * 10) + (0 if self.num_digits < 3 else ((self.digits[2] * 100) + (0 if self.num_digits < 4 else ((self.digits[3] * 1000) + (0 if self.num_digits < 5 else ((self.digits[4] * 10000) + (0 if self.num_digits < 6 else ((self.digits[5] * 100000) + (0 if self.num_digits < 7 else ((self.digits[6] * 1000000) + (0 if self.num_digits < 8 else (self.digits[7] * 10000000)))))))))))))))
        return getattr(self, '_m_as_int_le', None)

    @property
    def last_idx(self):
        """Index of last digit (0-based)."""
        if hasattr(self, '_m_last_idx'):
            return self._m_last_idx

        self._m_last_idx = (self.num_digits - 1)
        return getattr(self, '_m_last_idx', None)

    @property
    def as_int_be(self):
        """Value of this BCD number as integer (treating digit order as big-endian)."""
        if hasattr(self, '_m_as_int_be'):
            return self._m_as_int_be

        self._m_as_int_be = (self.digits[self.last_idx] + (0 if self.num_digits < 2 else ((self.digits[(self.last_idx - 1)] * 10) + (0 if self.num_digits < 3 else ((self.digits[(self.last_idx - 2)] * 100) + (0 if self.num_digits < 4 else ((self.digits[(self.last_idx - 3)] * 1000) + (0 if self.num_digits < 5 else ((self.digits[(self.last_idx - 4)] * 10000) + (0 if self.num_digits < 6 else ((self.digits[(self.last_idx - 5)] * 100000) + (0 if self.num_digits < 7 else ((self.digits[(self.last_idx - 6)] * 1000000) + (0 if self.num_digits < 8 else (self.digits[(self.last_idx - 7)] * 10000000)))))))))))))))
        return getattr(self, '_m_as_int_be', None)