メインフレームの現場で長年生き抜いてきたエンジニアなら、深夜のバッチ処理でデータ落ちや桁あふれ(S0C7ではない、純粋な数値計算上の丸め誤差や桁落ち)に冷や汗をかいた経験が一度や二度ではないはずだ。
「なぜ、PL/Iで正しく計算できていた金利や税額が、Java(BigDecimal)に移行した途端に数セント(あるいは数円)ズレるのか?」
これは、オープン系へのマイグレーション案件において、若手プログラマからベテランアーキテクトまでが必ずと言っていいほど直面する罠である。今日は、PL/Iの `FIXED DECIMAL` とJavaの `BigDecimal` の仕様の差異の本質を解き明かし、現場で絶対に事故を起こさないための実践知を伝授しよう。
—
1. PL/Iの `FIXED DECIMAL` とは何か:その厳格な世界
PL/Iにおける `FIXED DECIMAL (p, q)` は、単なる「小数点が扱える数値型」ではない。これは「人間が手計算で帳簿をつけるときの厳密さをハードウェア(またはソフトウェアのエミュレーション)に強制する型」である。
- `p` (Precision / 精度): 全体の有効桁数(最大15、あるいは31)。符号は含まない。
- `q` (Scale / スケール): 小数点以下の桁数。
例えば、`DCL ACCT_BAL FIXED DEC (11, 2);` と定義した場合、内部的にはパック十進数(Packed Decimal / COMP-3)としてメモリやVSAMファイル上に保持される。最大 999,999,99.99 までを1パーセントの狂いもなく、完全な10進数演算(基数10)として処理する。
ここに浮動小数点数(`FLOAT` や Javaの `double`)のような「2進数化に伴う無限小数ループ(例: 0.1の表現誤差)」は一切存在しない。この「10進数の厳密性」をJavaで完全に再現するために登場するのが `java.math.BigDecimal` である。しかし、ここに落とし穴がある。
—
2. なぜJava移行でズレが生じるのか?(スケールと丸めの罠)
Javaの `BigDecimal` は非常に強力だが、PL/Iから単純に移行しようとすると、以下の2点で挙動が食い違う。
1. 代入時・演算時のスケール(小数点以下桁数)の自動調整ルール
- PL/Iでは、異なる精度・スケール間の演算(例: `FIXED DEC(5,2)` と `FIXED DEC(7,4)` の乗算)において、言語仕様で厳密な中間結果の精度とスケールが規定されている。
- Javaの `BigDecimal` では、特に乗算(`multiply`)を行うと、スケールは単に「2つのオペランドのスケールの和」になる。例えば、スケール2とスケール2を掛け合わせると、結果のスケールは勝手に「4」になってしまう。
2. 丸めモード(Rounding Mode)のデフォルトの相違
- PL/Iの代入や算術組込み関数(`ADD`, `MULTIPLY` など)における丸めは、基本的には「四捨五入(Truncation / Round to nearest または環境依存の切り捨て・四捨五入)」だが、細かいオーバーフローや丸め動作はコンパイラオプション(`TRUNC` など)や仕様に依存する。
- Javaの `BigDecimal` でスケールを指定せずに演算や除算を行うと、無限小数になった瞬間に `ArithmeticException`(Non-terminating decimal expansion; no exact representable decimal result)がスローされる。また、丸めを指定しないとコンパイルエラーになるケースや予期せぬデフォルト(通常は `HALF_EVEN` など)が適用され、PL/Iの伝統的な「四捨五入」と一致しなくなる。
—
3. 実践:VSAM入出力とPL/Iコードの構造
ここで、実際のメインフレームバッチで使われる、VSAMファイルからデータを読み込み、税額を計算して出力する典型的なPL/Iコードを見てみよう。
1
/ ———————————————— /
/ プログラム名: TAXCALC /
/ 概要: VSAM受発注データから金額を読み込み税額を計算 /
/ ———————————————— /
TAXCALC: PROC OPTIONS(MAIN);
/ — レコード定義(VSAM入力・出力) — /
DCL 1 IN_REC,
5 IN_CUST_ID CHAR(5),
5 IN_BASE_AMT FIXED DEC(11, 2), / 基本金額: 整数9桁, 小数2桁 /
5 IN_RATE_DIV FIXED DEC(3, 3); / 税率区分: 小数3桁 (例: 0.100) /
DCL 1 OUT_REC,
5 OUT_CUST_ID CHAR(5),
5 OUT_TAX_AMT FIXED DEC(11, 2); / 計算後税額 /
DCL EOF_FLAG CHAR(1) INIT(‘0’);
/ ファイル宣言(省略化して記載) /
ON ENDFILE(VSAM_IN) EOF_FLAG = ‘1’;
OPEN FILE(VSAM_IN) INPUT;
OPEN FILE(VSAM_OUT) OUTPUT;
/ — メインループ — /
READ FILE(VSAM_IN) INTO(IN_REC);
DO WHILE (EOF_FLAG = ‘0’);
/ 税額計算: 基本金額 × 税率区分 /
/ MULTIPLY組み込み関数を使用し、明示的に結果の精度を指定する /
OUT_CUST_ID = IN_CUST_ID;
BEGIN;
DCL WK_CALC FIXED DEC(15, 5);
/ PL/Iでは演算結果のスケールが自動、または明示的に制御される /
WK_CALC = MULTIPLY(IN_BASE_AMT, 5, IN_RATE_DIV, 3);
/ FIXED DEC(11,2)へ代入する際に自動的に丸め(または切り捨て)が発生 /
OUT_TAX_AMT = WK_CALC;
END;
WRITE FILE(VSAM_OUT) FROM(OUT_REC);
READ FILE(VSAM_IN) INTO(IN_REC);
END;
CLOSE FILE(VSAM_IN), FILE(VSAM_OUT);
RETURN;
END TAXCALC;
このPL/Iコードの肝は、`MULTIPLY` 組み込み関数や代入文における「桁あふれ(Overflow)と丸め(Round)」の制御である。PL/Iでは、ターゲット変数のサイズ(`FIXED DEC(11,2)`)を超えた上位桁の切り捨てや、下位桁の丸めがコンパイラ生成コードによって暗黙的(または明示的)に行われる。
—
4. Java(BigDecimal)への移行:正確な再現コード
上記のPL/Iの挙動を、Javaの `BigDecimal` で完璧にエミュレートするための実装パターンを以下に示す。マイグレーション後のJavaバッチ層では、この標準をチーム全体で徹底しなければならない。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class TaxCalculator {
// PL/Iの FIXED DEC(11, 2) に相当するスケールと丸め定義
private static final int PLI_SCALE = 2;
// メインフレームの一般的なビジネス要件(四捨五入)に合わせる
private static final RoundingMode PLI_ROUNDING_MODE = RoundingMode.HALF_UP;
public void calculateTax() {
// VSAMから読み込んだと仮定する BigDecimal 値
// データベースやファイルから取得する際は、スケールを必ず意識する
BigDecimal inBaseAmt = new BigDecimal(“1234567.89”); // IN_BASE_AMT (11,2)
BigDecimal inRateDiv = new BigDecimal(“0.100”); // IN_RATE_DIV (3,3)
// 1. 乗算の実行
// Javaの multiply はスケールが加算される (2 + 3 = 5)
BigDecimal wkCalc = inBaseAmt.multiply(inRateDiv);
// wkCalc は現在 123456.78900 (スケール5) になっている。
// これはPL/Iの MULTIPLY(…, 5, …, 3) -> FIXED DEC(15, 5) の状態に一致する。
// 2. ターゲット変数 (FIXED DEC(11, 2)) への代入時の丸めとスケール合わせ
// PL/Iの代入挙動を再現するため、明示的にスケールと丸めモードを指定する
BigDecimal outTaxAmt = wkCalc.setScale(PLI_SCALE, PLI_ROUNDING_MODE);
// 3. 桁あふれ(Overflow)のチェック
// PL/Iの FIXED DEC(11,2) は整数部が最大9桁 (11 – 2 = 9)
// Java側でもメインフレームのピクチャー句や定義長に応じたバリデーションが必須
validatePrecision(outTaxAmt, 9, PLI_SCALE);
System.out.println(“計算後税額 (BigDecimal): ” + outTaxAmt);
}
private void validatePrecision(BigDecimal value, int integerDigits, int scale) {
// 整数部の桁数チェック(メインフレームのS0C7やデータ例外の代替バリデーション)
int intPartLength = value.toBigInteger().abs().toString().length();
// 0の場合は1桁
if (value.compareTo(BigDecimal.ZERO) == 0) {
intPartLength = 1;
}
if (intPartLength > integerDigits) {
throw new ArithmeticException(“PL/I移行エラー: 整数部が許容桁数を超過しました。定義長を確認してください。”);
}
}
}
—
5. ベテランからの実務アドバイス:移行時のチェックリスト
Javaへのマイグレーション設計を行う際、以下の鉄則をプロジェクトのコーディング規約に必ず組み込んでほしい。
1. 「スケールの連鎖」を放置するな
Javaの `BigDecimal` は演算のたびにスケールが変わる。PL/Iのように「この代入の瞬間にはどの精度(Precision)とスケール(Scale)に落ちるべきか」を意識し、計算の要所要所で `.setScale(scale, RoundingMode.HALF_UP)` を挟むこと。これをサボると、チェイン計算の末に数ペニーのズレが生じ、金融監査で致命的な欠陥として指摘される。
2. 丸めモード(Rounding Mode)の統一
Javaのデフォルトに頼るな。プロジェクト全体で「四捨五入(`HALF_UP`)」にするのか、「切り捨て(`DOWN`)」にするのかを統一し、共通ユーティリティクラス(例: `PliMathUtils`)としてラップするのが最も安全である。
3. ゾーン10進数・パック10進数の符号処理
VSAMやKSDSからバイナリ直接でデータをコンバートする場合、正負の符号(特にローエンドニップル)の解釈ミスでマイナス値が狂うことがある。Java側で `BigDecimal` に読み込む前のバイト配列パース処理(COMP-3のアンパック処理)においても、メインフレームの仕様書(コピーブック)を1文字たりとも見落としてはならない。
レガシーシステムの移行は、単なる言語の置き換えではない。「先人が何十年もかけて洗練させたデータの厳密性を、新しいプラットフォームの上でどう担保するか」という思想の継承なのだ。このスケールと丸めの制御をマスターすれば、どんな複雑な金融・基幹バッチのオープン化であっても、恐れるものは何もない。自信を持ってコードに向き合ってほしい。
