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() 顯示。 既然現代程式語言都把這些細節隱匿起來了,單憑背誦計算機概論中的隻字片語,大概還是無法體會這之中到底做了什麼。 我想只剩下老派的程式設計人員,還會注意到這個細節吧。
樂多舊回應