将金额 (int) 转换为 BCD

Convert amount (int) to BCD

我需要将 Int 左填充 6 个字节(数量)转换为 Python 中的 BCD。

int = 145
expect = "\x00\x00\x00\x00\x01\x45"

我最接近的是这段代码(但它需要以字节对循环):

def TO_BCD(value):
    return chr((((value / 10) << 4) & 0xF0) + ((value % 10) & 0x0F))

int = 145
TO_BCD(int) # => "\x00\x00\x00\x00\x01\x45" (expected)

这是一个例子。

script0.py:

#!/usr/bin/env python3

import sys


def bcd(value, length=0, pad='\x00'):
    ret = ""
    while value:
        value, ls4b = divmod(value, 10)
        value, ms4b = divmod(value, 10)
        ret = chr((ms4b << 4) + ls4b) + ret
    return pad * (length - len(ret)) + ret


def bcd_str(value, length=0, pad='\x00'):
    value_str = str(value)
    value_str = ("0" if len(value_str) % 2 else "") + value_str
    ret = ""
    for i in range(0, len(value_str), 2):
        ms4b = ord(value_str[i]) - 0x30
        ls4b = ord(value_str[i + 1]) - 0x30
        ret += chr((ms4b << 4) + ls4b)
    return pad * (length - len(ret)) + ret


def main():
    values = [
        145,
        5,
        123456,
    ]
    for value in values:
        print("{0:d} - [{1:s}] - [{2:s}]".format(value, repr(bcd(value, length=6)), repr(bcd_str(value, length=6))))

    # Bonus
    speed_test = 1
    if speed_test:
        import timeit  # Anti pattern: only import at the beginning of the file
        print("\nTesting speed:")
        stmt = "bcd({0:d})".format(1234567890 ** 32)
        count = 100000
        for func_name in ["bcd", "bcd_str"]:
            print("    {0:s}: {1:.03f} secs".format(func_name, timeit.timeit(stmt, setup="from __main__ import {0:s} as bcd".format(func_name), number=count)))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main()
    print("\nDone.")

输出:

[cfati@CFATI-5510-0:e:\Work\Dev\Whosebug\q057476837]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" script0.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32

145 - ['\x00\x00\x00\x00\x01E'] - ['\x00\x00\x00\x00\x01E']
5 - ['\x00\x00\x00\x00\x00\x05'] - ['\x00\x00\x00\x00\x00\x05']
123456 - ['\x00\x00\x00\x124V'] - ['\x00\x00\x00\x124V']

Testing speed:
    bcd: 17.107 secs
    bcd_str: 8.021 secs

Done.

备注:

  • 由于您使用的是 packed BCD,每个数字将以 4 位存储,因此 2 位数字将占用一个字节
  • 算法很简单:将数字分成2个数字组,在每个组中第1st (Most Significant)位会左移4位,然后是2nd(Least Significant) 将添加一个 - 这将是 charASCII
  • 输出看起来可能与您预期的有点不同,但这只是显示格式的原因:例如大写字母 'E' char 具有 ASCII 代码 0x45 ( 69),也可以写成'\x45',所以输出是正确的
  • 有2个实现:

    • bcd - 使用算术运算
    • bcd_str - 使用字符串操作

    速度测试(在 main 的末尾)产生了令人惊讶的结果:2nd(字符串)变体更快 ~2 倍)。一个简短的解释是(在 Python 中)modulo 操作在大量情况下是昂贵的(缓慢的)。

这看起来相当简单,并且得到了您正在寻找的答案。只需隔离每对数字并转换为 ASCII。

如果我大量执行此操作,那么我可能会为每个字节构建所有可能的 100 个值的 table(可能在 numpy 中),并使用输入中的每对数字对其进行索引。

m = 145
print(''.join(f"\x{m // 10**i % 10}{m // 10**(i-1) % 10}" for i in range(11, -1, -2)))

输出,虽然它只是一个字符串,而不是任何内部 BCD 表示

\x00\x00\x00\x00\x01\x45

同理,可以将BCD打包成字节串。打印时,Python 会将 BCD 45 解释为大写 E

import struct
m = 145

packed = struct.pack('6B', *[(m // 10**i % 10 << 4) + (m // 10**(i-1) % 10) for i in range(11, -1, -2)])
print(packed)
print(''.join(f"\{p:02x}" for p in packed))

输出

b'\x00\x00\x00\x00\x01E'
[=13=][=13=][=13=][=13=]