最近更新: 2011-07-26

BCD碼轉文字

日前公司同事請我幫忙解決一個數字顯示的問題。客戶有一個讀卡設備,接著 COM 埠上。 他按照客戶提供的規格手冊,從該設備中讀出卡號。但顯示出來的卡號不是他預期的樣子。 我看了他的程式與執行結果後,我第一時間覺得程式和結果都沒錯啊,哪裡有問題。 又聽了一次他的解釋後,才注意到他忘了一項計算機概論的基本觀念:儲存在記憶體中的數值若要顯示成文字,要經過數值轉文字內碼的程序。 他忘了這件事,所以才一直以為是程式有問題。

當我意識到他的錯誤時,我還稍微向他解釋了一下數值與內碼的差異。 不過看他的表情似乎還是有點迷糊,也不知他是否真的理解了。 總之,我最後還是很快地寫好BCD碼轉ASCII碼的函數給他用。

他的情況可簡化為下列敘述:

$n = 12;
  // in memory: [0x12]
echo $n, "\n";
  // 他預期顯示 12 ,而且他也確實看到 12

$s = "\x00\x12\x34\x56\x78\x90";
  // in memory: [00, 0x12, 0x34, 0x56, 0x78, 0x90]
echo $s, "\n";
  // 他預期顯示001234567890,但他看到的是 ??4Vx?
  // 他認為有問題。

他從客戶的讀卡設備中讀取卡號的方式,就是用 IO 設備的 read 方法,將資料存入資料緩衝區。 而讀入的內容形式,就是像上述程式碼的形式。例如卡號為 12345678,則讀入的資料位元組就是 4 個 bytes,內容為 [0x12, 0x34, 0x56, 0x78]。 這種儲存形式即 Packed BCD 。 通常用於處理超長數字,因為此形式可以忽視計算機本身的硬體限制,諸如CPU 位元數、記憶體排列順序等限制。

以 12345678 這個數字為例,在 x86 架構的機器中,以整數形式儲存時為 [0x4E, 0x61, 0xBC, 0x00]。 以 ASCII 編碼儲存時則為 [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]。 而本案例所使用的 BCD 編碼卻為 [0x12, 0x34, 0x56, 0x78]。 現在常用的程式語言中不提供 BCD 編碼的處理函數,故若要輸出其內容,就需要撰寫額外的轉換函數。

我分別用 PHP 和 Python 寫了 BCD碼與ASCII碼互轉的函數。

第一個是 PHP 的轉碼程式。

<?php
// BCD碼轉文字第一種解法: 推算法。
function bcd_to_ascii_calculate($s) {
    $result = array();
    $len = strlen($s);
    for ($i = 0; $i < $len; ++$i) {
        $code = ord($s[$i]);
        if ($code < 10)
            $result[] = '0' . strval($code);
        else
            $result[] = dechex($code);
    }
    return implode('', $result);
}

// BCD碼轉文字第二種解法: 查表法。速度比較快。
// 開始定義全域變數 asc_table
// 只需要定義一次。
$asc_table = array('00', '01', '02', '03', '04', '05', '06', '07', '08', '09',
      '0a', '0b', '0c', '0d', '0e', '0f');
for ($i = 16; $i < 256; ++$i)
    $asc_table[] = dechex($i);
//echo implode(',', $asc_table);
// 結束

function bcd_to_ascii_search_table($s) {
    global $asc_table;
    $result = array();
    $len = strlen($s);
    for ($i = 0; $i < $len; ++$i) {
        $code = ord($s[$i]);
        $result[] = $asc_table[$code];
    }
    return implode('', $result);
}

function ascii_to_bcd($s) {
    $len = strlen($s);
    $result = array();
    for ($i = $b = 0, $h = true; $i < $len; ++$i) {
        if ($h) { // high bits
            $b = hexdec($s[$i]) << 4;
            $h = false; // 我不想用運算方式判斷高低順序
        }
        else {
            $b |= hexdec($s[$i]);
            $result[] = chr($b);
            $h = true;
        }
    }
    return implode($result);
}

$s = "\x00\x12\x34\x56\x78\x90\xab";
// in memory: [00, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab]
echo $s, "\n"; // you will see ??4Vx??

echo bcd_to_ascii_calculate($s), "\n";

echo bcd_to_ascii_search_table($s), "\n";

$as = bcd_to_ascii_search_table($s);

echo ascii_to_bcd($as), "\n";
?>

第二個是 Python 的轉碼程式。

#!/usr/bin/python
# coding: utf-8

# BCD碼轉文字第一種解法: 推算法。
def bcd_to_ascii_calculate(s):
    result = []
    for ch in s:
        code = ord(ch)
        if code < 10:
            result.append('0' + str(code))
        else:
            result.append(hex(code)[2:])

    return ''.join(result)

## BCD碼轉文字第二種解法: 查表法。速度比較快。
# 開始定義全域變數 asc_table
# 只需要定義一次。
asc_table = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09',
      '0a', '0b', '0c', '0d', '0e', '0f']
for code in xrange(16, 256):
    asc_table.append(hex(code)[2:])
#asc_tuple = tuple(asc_table)
#print(asc_table)
# 結束

def bcd_to_ascii_search_table(s):
    result = []
    for ch in s:
        code = ord(ch)
        result.append(asc_table[code])
    return ''.join(result)

def ascii_to_bcd(s):
    result = bytearray()
    b = 0
    h = True
    for ch in s:
        if h: # high bits

            b = int(ch, 16) << 4
            h = False
        else:
            b |= int(ch, 16)
            result.append(b)
            h = True
    return result

if __name__ == "__main__":
    s = "\x00\x12\x34\x56\x78\x90\xab"
    # in memory: [ 00, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab ]

    print(repr(s))  # you will see '\x00\x124Vx\x90\xab'

    print(bcd_to_ascii_calculate(s))

    print(bcd_to_ascii_search_table(s))

    s2 = bcd_to_ascii_search_table(s)
    print(ascii_to_bcd(s2))

這種轉碼程式的定則是「查表比推算快」。我實際測了一下這兩份程式碼,在 PHP 與 Python 版的表現也符合定則,查表快許多。 針對 Python (2.6版),我還另外測過 list 與 tuple 這兩種表的查表速度。令我意外的是,兩者無分軒輊,list 偶爾還比 tuple 快。 我本以為 tuple 應該快些的。

想當年我在學組合語言時,這道程序還是第一個必寫的課題。由於當年學的很辛苦,所以現在腦子已經習慣這麼思考了。 故而我看了他的程式後,只覺得這樣的輸入內容,本來就是這樣的輸出結果,哪有問題。 當我意識到他的錯誤時,我還稍微向他解釋了一下數值與內碼的差異。不過看他的表情還是有點迷糊,也不知他是否真的理解了。

我後來仔細一想,似乎不能怪他不懂這件事。因為現代的程式語言教程中,數值輸出成文字根本不需要理解數值與內碼的差異。 舉個例子來說,若有一整數 n 其值為 123 ,要輸出為文字。學 C 語言,就是記住 printf() 中要填 %d 才能輸出。 學 C++ 的就是用 cout 。學 Java 語言,也只知 println(n) 就可以了。哪需要什麼數值轉內碼的程序。 或許有些用心的書籍作者,還會提示 Java 的 println() 實際上實作了一個不可被覆寫的內置型態轉換行為,會先調用整數轉字串的方法,才把結果傳給 println() 顯示。 既然現代程式語言都把這些細節隱匿起來了,單憑背誦計算機概論中的隻字片語,大概還是無法體會這之中到底做了什麼。 我想只剩下老派的程式設計人員,還會注意到這個細節吧。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/16150047.html

樂多舊回應
g943936@oz.nthu.edu.tw(CCC) (#comment-21885073)
Tue, 26 Jul 2011 22:01:08 +0800
也可以用 Python 內建的 binascii 套件
http://docs.python.org/library/binascii.html

import binascii

s = "\x00\x12\x34\x56\x78\x90\xab"
print(binascii.b2a_hex(s))

aoc90058@joen.cc(Joen) (#comment-21893475)
Mon, 01 Aug 2011 02:22:27 +0800
個人認為這還是一個基本素養,如果是正統資工出來的,不會這個就有點傻眼了:(
遊手好閒的石頭成 (#comment-21896227)
Tue, 02 Aug 2011 14:46:00 +0800
那我確實該傻眼了。因為我同事是資工系的,而我是唸國際貿易系的。
LungZeno (#comment-21915655)
Sun, 14 Aug 2011 22:41:29 +0800
我又玩。

// BCD碼轉文字: 完全推算法。
// 把關鍵地方寫出來,應該比較好理解。
// 不過這就跟石頭大大設計的函式不等價了。
function bcd_to_ascii_calculate_fully($s) {
$result = array();
$len = strlen($s);
for ($i = 0; $i < $len; ++$i) {
$int_val = ord($s[$i]);
$result[] = chr(($int_val >> 4) + 48);
$result[] = chr(($int_val & 0x0f) + 48);
}
return implode('', $result);
}

// BCD碼轉文字: 完全查表法。
// 因為用腳本語言,省卻中間轉成整數型別的步驟,速度更快。
$asc_table_2 = array("\0"=>"00","\1"=>"01","\2"=>"02","\3"=>"03","\4"=>"04",
"\5"=>"05","\6"=>"06","\7"=>"07","\x8"=>"08","\x9"=>"09");
for ($i = 10; $i < 256; ++$i)
$asc_table_2[chr($i)] = dechex($i);

function bcd_to_ascii_search_table_fully($s) {
global $asc_table_2;
$result = array();
$len = strlen($s);
for ($i = 0; $i < $len; ++$i)
$result[] = $asc_table[$s[$i]];
return implode('', $result);
}

// 文字轉BCD碼: 查表法。
// 這是我以前學習時很喜歡玩的東西,思考「是否也可以?」。
$bcd_table = array();

for ($j = $i = 0; $j < 16; ++$j) {
$subtable = &$bcd_table[dechex($j)];
$subtable = array();
for ($k = 0; $k < 16; ++$k, ++$i)
$subtable[dechex($k)] = chr($i);
}

function ascii_to_bcd_search_table($s) {
global $bcd_table;
$len = strlen($s);
$result = array();
for ($i = 0; $i < $len; $i += 2)
$result[] = $bcd_table[$s[$i]][$s[$i+1]];
return implode('', $result);
}


其實這類函式在現今強大的腳本語言和數之不盡的程式庫面前,只是寫好玩……
遊手好閒的石頭成 (#comment-21918389)
Tue, 16 Aug 2011 17:14:11 +0800
華生,你突破盲點了。

不過LungZeno的解釋內容中,如果再補上一條內容,就可以讓我們對PHP的理解再往前走一步。
我測了LungZeno與我的程式的效率後,我得到的結論是「PHP 沒有真正的序數陣列(ordered array),PHP 的陣列全都是雜湊表(hash table)」。

我的思路一開始走的是組合語言/C語言那套,在那邊序數陣列與雜湊表的查表演算法完全是兩回事。
序數陣列是位址偏移;雜湊表則是先對鍵值算出雜湊值,再以雜湊值搜尋表(通常是搜尋一棵二元樹)。
兩者的速度天差地遠。

我認定 PHP 會把以「整數」為鍵值的陣列配置為序數陣列,而以「字串」為鍵值的陣列配置為雜湊表。
我希望用序數陣列去查表,避免雜湊表計算雜湊值的損耗。所以建表時做了字串轉數值的動作。
而 LungZeno 的程式碼則是直接用字串為鍵值建表。而實際執行的結果顯示,整數表查起來並未比字串表快。
事實上,我的程式碼比較慢。

如果整數表真的是序數陣列的話,那麼它的查詢速度足以抵消轉型別的損耗,不應該比字串表慢。
因此效率差異的理由除了 LungZeno 所說「少了轉型別的步驟」之外,另一個理由就是「PHP 沒有序數陣列」。

不論我是用整數建表,亦或是LungZeno用字串建表,PHP都是配置成雜湊表。查詢時都少不了求雜湊值與搜尋樹的動作。
而我的程式多了轉型別的動作,就比較慢了。
toons@163.com(同事) (#comment-25330295)
Wed, 05 Aug 2015 03:34:06 +0800
我傻眼了。