Java 轉換 COBOL COMP-3 資料

meow meow meow meowwwww....
老舊的金融系統編碼問題
客戶的系統是早期的 IBM-Z (COBOL), 用的是 EBCDIC 的編碼, 而台灣的環境會搭配 BIG5(cp-950) 的編碼. EBCDIC 是 IBM 系統專用的編碼, 跟 ASCII 差異如下.

為了讓資了可以被處理, 需要進行資料清洗與解碼, 礙於客戶的資安規範與其他因素限制下, 合作過程必須處理 EBCDIC + Binary 的格式文件, 來做資料轉移.
資料格式
文字類型的 BIG5 編碼處理
中文的欄位資料是採用 Big5 (cp950), 撇開難字問題 (先不討論), 以 Java 處理的話還算簡單, 透過 String 建構子的兩個參數.
/**
* Constructs a new {@code String} by decoding the specified array of
* bytes using the specified {@linkplain java.nio.charset.Charset charset}.
* The length of the new {@code String} is a function of the charset, and
* hence may not be equal to the length of the byte array.
*
* <p> This method always replaces malformed-input and unmappable-character
* sequences with this charset's default replacement string. The {@link
* java.nio.charset.CharsetDecoder} class should be used when more control
* over the decoding process is required.
*
* @param bytes
* The bytes to be decoded into characters
*
* @param charset
* The {@linkplain java.nio.charset.Charset charset} to be used to
* decode the {@code bytes}
*
* @since 1.6
*/
public String(byte[] bytes, Charset charset) {
this(Objects.requireNonNull(charset), bytes, 0, bytes.length);
}
bytes: 要 decode 的 source bytes.
charset: 要 decode 的編碼, 以 Big5 (cp950) 為例的處理如下
String utf8String = new String(sourceBytes, Charset.forName("Big5"));
數字類型的處理
而數字類型的欄位處理相對複雜一點, 解釋可以參考
https://learn.microsoft.com/zh-tw/host-integration-server/core/supported-cobol-data-types2
COMP-3 的壓縮
假設 COBOL 在欄位 A 寫入了數字 123.45.
通常 ASCII 的 bytes 會是 6 bytes 的儲存長度.
0x31 0x32 0x33 0x2E 0x34 0x35
1 2 3 . 4 5
然而在 COMP-3 的壓縮格式下, 一個位元 bytes 可以用來表示 2 個數字, 也就是 4 bits 表示 1 個數字, 而小數點並不會被儲存在 bytes 裡面, 而是由 COBOL 的格式標記位置, 123.45 在 COBOL 的格式中會是
01 AMOUNT PIC 9(3)V99 COMP-3.
01: 第一個欄位.
AMOUNT: 欄位定義名稱.
PIC: COBOL 描述欄位的方式
9(3): 表示小數點前有 3 位數字(只可儲存數字 0~9)
V: 虛擬小數點(implied decimal point):不會儲存進資料中,但代表小數點的位置
99: 表示小數點後有 2 位數字
COMP-3: 使用 Packed Decimal (壓縮十進制) 格式儲存,兩位數佔 1 byte,最後一 nibble 為正負符號, nibble 就是最後 4 bits.
因此 123.45 在 COMP-3 壓縮儲存的 bytes 會是 2 + 1 bytes, 資料由 ASCII (6 bytes) 壓縮為 3 bytes 了.
0x01 0x23 0x45 0x0C
1 23 45 +
虛擬小數的標記
由於小數位, 並不會被轉為 bytes 而是由 COBOL 的虛擬小數標記位置, 其類似原理大概就像
Decimal("12345") * Decimal("0.01")
正負符號表示
附註: 一般而言, '0xA'、'0xC'、'0xE' 或 '0xF' 可以用來表示正值,而 '0xB' 或 '0xD' 可以用來表示負值。 實際偏好的表示法取決於實際硬體架構。
從上面 123.45 的 bytes 理解, 是由後面的 nibble 判斷正負值, 而這個可能會因為 IBM 系統版本而有差異.
COMP-3 前綴補 0
為何需要前綴補 0 ? 這是因為偶數長度的 digits 要 pack decimal 的時候會遇到, 最後面的 nibble (正負符號).
舉例來說 12.34
0x12 0x34 0x0C
12 34 +
由於 nibble 0×0c 只有 4 bits, 會導致這筆紀錄是 1.5 bytes, 因此需要前綴填充 0 紀錄處理.
0x00 0x12 0x34 0x0c
0 12 34 +
因此紀錄會變成 2 bytes 的長度.
Java Code
/**
* Converts an EBCDIC S9(11)V9(02) packed decimal byte array to BigDecimal. This method assumes
* the input byte array represents a COMP-3 or packed decimal field.
*
* <p>S9(11)V9(02) means: - S: Sign is present - 9(11): 11 digits for the integer part - V:
* Implied decimal point - 9(02): 2 digits for the fractional part Total digits = 11 (integer) + 2
* (fractional) = 13 digits. In packed decimal, each byte stores two digits, except for the last
* byte which stores one digit and the sign. Number of bytes = (total digits + 1) / 2. For 13
* digits: (13 + 1) / 2 = 7 bytes.
*
* @param ebcdicBytes The byte array from the EBCDIC field.
* @return BigDecimal representation of the EBCDIC packed decimal.
* @throws IllegalArgumentException if the byte array is null, empty, or has an invalid format.
*/
public static BigDecimal convertEbcdicPackedDecimalToBigDecimal(byte[] ebcdicBytes, int scale) {
if (ebcdicBytes == null || ebcdicBytes.length == 0) {
throw new IllegalArgumentException("EBCDIC byte array cannot be null or empty.");
}
// S9(11)V9(02) has 13 total digits. (11 integer, 2 fractional)
// This will occupy (13 digits + 1 sign nibble) / 2 nibbles per byte = 7 bytes.
// Example: For S9(11)V9(02), expected length is 7 bytes.
// If your field definition might vary, you might need to adjust this check or make it more
// dynamic.
// For S9(N)V9(M), total digits P = N + M. Bytes = ceil((P+1)/2).
// In this specific case, P = 11 + 2 = 13. Bytes = ceil(14/2) = 7.
// If the actual byte length differs, it implies a different S9(N)V9(M) structure.
StringBuilder numericString = new StringBuilder();
boolean isNegative = false;
// Process all bytes except the last one
for (int i = 0; i < ebcdicBytes.length - 1; i++) {
byte currentByte = ebcdicBytes[i];
// High nibble (first digit)
numericString.append((currentByte & 0xF0) >>> 4);
// Low nibble (second digit)
numericString.append(currentByte & 0x0F);
}
// Process the last byte (contains the last digit and the sign)
byte lastByte = ebcdicBytes[ebcdicBytes.length - 1];
// High nibble (last digit)
numericString.append((lastByte & 0xF0) >>> 4);
// Low nibble (sign)
int signNibble = lastByte & 0x0F;
switch (signNibble) {
case 0x0A: // Hex A, COBOL: ACEL (Positive)
case 0x0C: // Hex C, Standard Positive
case 0x0E: // Hex E, (Positive)
case 0x0F: // Hex F, Unsigned (treated as Positive)
isNegative = false;
break;
case 0x0B: // Hex B, (Negative)
case 0x0D: // Hex D, Standard Negative
isNegative = true;
break;
default:
throw new IllegalArgumentException(
"Invalid sign nibble: " + Integer.toHexString(signNibble));
}
// Insert the decimal point. The numericString currently has all 13 digits.
// We need to insert the decimal point before the last 'scale' digits.
if (numericString.length() < scale) {
// Pad with leading zeros if the number of digits is less than the scale
// e.g. if numericString is "12" and scale is 3, it becomes "0.012"
// This scenario is less likely with fixed S9(11)V9(02) but good for robustness
int zerosToPad = scale - numericString.length();
for (int i = 0; i < zerosToPad; i++) {
numericString.insert(0, "0");
}
numericString.insert(0, ".");
} else {
numericString.insert(numericString.length() - scale, ".");
}
if (isNegative) {
numericString.insert(0, "-");
}
return new BigDecimal(numericString.toString());
}






