2

I need to convert an Int left padded 6 bytes (amount) to a BCD in Python.

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

The closest I come is with this code (but it needs to loop in byte pair):

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)

JayC
  • 135
  • 1
  • 2
  • 11
  • You're on the right track. Keep going. You don't need a loop, although that's one way of dong it. – user207421 Aug 13 '19 at 12:11
  • Which is the 12 bits pad? `"\x00\x00\x00\x00\x01\x45"` is 6 bytes (48 bits) long, out of which the number itself is represented by the last 16 bits. – CristiFati Aug 13 '19 at 12:26
  • No its my fault, its not a 12 bit padding. The total length of the message should be 6 bytes, the BCD should be right justified and left side padded with 0. As in my example above all the nibbles to the left of the int is 0 up to a total of 6 bytes. – JayC Aug 13 '19 at 15:59
  • Well @user207421 i'm locked, by thinking inside the box - so if you know anything I dont here, that would be appreciated. – JayC Aug 13 '19 at 16:02
  • @JayC Divide `value` by 100. Convert the result as above and you will get `"\x00\x01"`. Join the two strings and keep going. (It's the same procedure as for converting to a regular string.) – molbdnilo Aug 13 '19 at 16:24

2 Answers2

0

Here's an example.

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.")

Output:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\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.

Notes:

  • Since you're working with packed BCD, each digit will be stored in 4 bits, and thus 2 digits will take one byte
  • The algorithm is simple: split the number in 2 digit groups, in each group the 1st (Most Significant) digit will be shifted to the left by 4 bits, and then the 2nd (Least Significant) one will be added - this will be the char's ASCII code
  • The output might look a bit different than what you're expecting, but that's only due display formatting: for example capital letter 'E' char has the ASCII code 0x45 (69), and can also be written as '\x45', so the output is correct
  • There are 2 implementations:

    • bcd - uses arithmetical operations
    • bcd_str - uses operations on strings

    The speed test (at the end of main) yields surprising results: the 2nd (string) variant is faster (by a factor of ~2). A brief explanation would be that (in Python,) modulo operation is expensive (slow) on large numbers.

CristiFati
  • 38,250
  • 9
  • 50
  • 87
0

This seems fairly simple, and gets the answer you were looking for. Just isolate each pair of digits and convert to ASCII.

If I were doing this in high volume then I'd probably build a table (perhaps in numpy) of all the possible 100 values per byte and index it with each pair of digits in the input.

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

Output, although it's just a string, not any internal BCD representation

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

Along the same lines, you can pack the BCD into a byte string. When printed, Python will interpret BCD 45 as a capital 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))

Output

b'\x00\x00\x00\x00\x01E'
\00\00\00\00\01\45
Deepstop
  • 3,627
  • 2
  • 8
  • 21