10進数浮動小数点 Decimal 型 ⟺ DPD (固定ビット長バイナリ)変換

# 目的

バイナリ データに「固定ビット長の 10 進数浮動小数点データ」を用いたいとき、IEEE 754 にある decimal **N** 形式（以下、DPD）は一つの選択肢になります。そこで、decimal モジュールの Decimal 型を DPD 形式に変換／逆変換するものが欲しくなります。


# プログラム

変換は Decimal 型と int 型の間で行います。

```Python:Python3
from decimal import Decimal


class DPD:
    COMBINATION_DECODE_TABLE = tuple(
        # 0=NORMAL, 1=INFINITY, 2=NaN
        ((0, ((g >> 3) & 3), (g & 7)),
         (0, ((g >> 1) & 3), ((g & 1) | 8)),
         (1, 0, 0),
         (2, 0, 0))
        [(2 + (g & 1)) if g >= 30 else (1 if g >= 24 else 0)]
        for g in range(32))

    COMBINATION_ENCODE_TABLE = tuple(
        ([((e << 3) | d) for d in range(8)] +
         [((e << 1) | d) for d in (0x18, 0x19)])
        for e in range(3))

    DPD_DECODE_TABLE = tuple(
        '%03x' % (lambda x: ((
            x,
            (x | 0x0ee) - 0x06e + ((x >> 4) & 0x06),
            (x | 0xe0e) - 0x60e + ((x >> 8) & 0x06),
            (x | 0xeee) - 0x66e + ((x >> 8) & 0x06),
            (x | 0xeee) - 0x6e6 + ((x >> 4) & 0x60),
            (x | 0x0ee) - 0x066,
            (x | 0xeee) - 0x666,
        )[max(0, ((p >> 1) & 7) - 4) + (0 if (~p & 14) else ((p >> 5) & 3))]))
        (p + (p & 0x380))
        for p in range(1024))

    DPD_ENCODE_TABLE = tuple(
        (lambda x: (
            0x000 | (x & 0x077) | ((x & 0x700) >> 1),
            0x008 | (x & 0x071) | ((x & 0x700) >> 1),
            0x00a | (x & 0x011) | ((x & 0x700) >> 1) | ((x & 0x006) << 4),
            0x00c | (x & 0x071) | ((x & 0x100) >> 1) | ((x & 0x006) << 7),
            0x00e | (x & 0x011) | ((x & 0x100) >> 1) | ((x & 0x006) << 7),
            0x02e | (x & 0x011) | ((x & 0x100) >> 1) | ((x & 0x060) << 3),
            0x04e | (x & 0x011) | ((x & 0x700) >> 1),
            0x06e | (x & 0x011) | ((x & 0x100) >> 1),
        )[(0, 6, 2, 8, 5, 3, 7, 4, 1)[(x & 0x888) % 9]])
        (int('%03d' % d, 16))
        for d in range(1000))

    def __init__(self, bits=64):
        if (bits & 31):
            raise NotImplementedError

        S = DPD
        self.S = S
        self.decomb = S.COMBINATION_DECODE_TABLE
        self.encomb = S.COMBINATION_ENCODE_TABLE
        self.dedpd = S.DPD_DECODE_TABLE
        self.endpd = S.DPD_ENCODE_TABLE

        self.k = bits
        self.w = (self.k >> 4) + 4
        self.t = self.k - (1 + 5 + self.w)
        self.p = (self.k >> 5) * 9 - 2
        self.emax = 3 << ((self.k >> 4) + 3)
        self.bias = self.emax + self.p - 2

        self.p_sign = self.k - 1
        self.p_mode = self.k - (1 + 5)
        self.p_nan = self.p_mode - 1
        self.p_exp = self.t
        self.p_trailings = tuple(reversed([p for p in range(0, self.t, 10)]))

        self.b_emax = self.emax << 1
        self.n_expt = self.p_mode - self.p_exp
        self.m_expt = (1 << self.n_expt) - 1

        self.n_dpd = int(self.t / 10)
        self.n_dec = self.n_dpd * 3

        self.sign = 1 << self.p_sign
        self.combination = self.sign - (1 << self.p_exp)
        self.trailing = (1 << self.p_exp) - 1

        self.infinite = 0x1E << self.p_mode
        self.qnan = 0x3E << self.p_nan
        self.snan = 0x3F << self.p_nan

    def decode(self, binary, context=None):
        s = ('', '-')[(binary >> self.p_sign) & 1]
        (mode, eh, d1) = self.decomb[(binary >> self.p_mode) & 0x1f]

        if mode == 1:
            return Decimal(s + 'Infinity', context)
        if mode == 2:
            return Decimal(s + ('', 's')[(binary >> self.p_nan) & 1] + 'NaN', context)
        if d1 == 0 and (binary & self.trailing) == 0:
            return Decimal(s + '0', context)

        e = ((eh << self.n_expt) | ((binary >> self.p_exp) & self.m_expt)) - self.bias
        d = ('%d' % d1)
        for p in self.p_trailings:
            d += self.dedpd[(binary >> p) & 0x3ff]
        while len(d) and d[0] == '0':
            d = d[1:]
        while len(d) and d[-1] == '0' and e < 0:
            d = d[:-1]
            e += 1
        return Decimal('%s%se%d' % (s, d, e), context)

    def encode(self, decimal):
        if type(decimal) != Decimal:
            raise NotImplementedError

        (s, d, e) = decimal.as_tuple()
        rs = s << self.p_sign

        if decimal.is_zero():
            return rs
        if decimal.is_infinite():
            return (rs | self.infinite)
        if decimal.is_qnan():
            return (rs | self.qnan)
        if decimal.is_snan():
            return (rs | self.snan)

        dp = len(d) - self.p
        e += dp
        if dp < 0:
            d = d + ((0,) * (-dp))
        d = d[:self.p]

        e += self.bias
        if e >= self.b_emax:
            return (rs | self.infinite)
        if e <= -self.p:
            return rs
        if e < 0:
            d = (((0,) * (-e)) + d)[:self.p]
            e = 0
        m = self.encomb[e >> self.n_expt][d[0]]
        re = (m << self.p_mode) | ((e & self.m_expt) << self.p_exp)
        rd = 0
        for i in range(1, self.p, 3):
            rd = (rd << 10) | self.endpd[d[i] * 100 + d[i + 1] * 10 + d[i + 2]]
        return (rs | re | rd)
```

```Python:テスト
converter = DPD(32)                     # 32 = バイナリのビット長(32の倍数のみ)
enc = converter.encode(Decimal('0.1'))  # = 0x25e00000
dec = converter.decode(enc)             # = Decimal('0.1')
print(hex(enc), dec.__repr__())         # 0x25e00000 Decimal('0.1')
```

---

**参考**

- [備忘録: 浮動小数点データ](https://qiita.com/ikiuo/items/6c02524a35f676ec753b)