Skip to main content

Command Palette

Search for a command to run...

Java 轉換 COBOL COMP-3 資料

Updated
5 min readView as Markdown
Java 轉換 COBOL COMP-3 資料
J

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);
    }
  1. bytes: 要 decode 的 source bytes.

  2. 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());
  }