おい、最近Javaへのリプレイス案件で夜も眠れない日々を送っているそうだな。気持ちは痛いほどよく分かる。
「メインフレームの勘定系バッチをオープン系(Java)に載せ替える」――響きはモダンだが、現場の我々からすると、これは地雷原を裸足で歩くようなものだ。特に、長年連れ添ったPL/Iの `FIXED DECIMAL`(いわゆるパック十進数)を、Javaの `BigDecimal` へ置き換えるときの苦労といったら、胃に穴が開くレベルのイベントだ。
今日は、なぜこのデータ型の移行がこれほどまでに厄介なのか、そしてどうすればあの忌々しい「1ペニーの端数ズレ」や「コンバージョンエラー」の呪縛から逃れられるのか、俺が骨の髄まで叩き込んでやる。心して聞くように。
—
1. なぜPL/Iの `FIXED DECIMAL` はJavaでそのまま再現できないのか?
まず大前提として、PL/Iの `FIXED DECIMAL(p, q)` と、Javaの `java.math.BigDecimal` は、生まれも育ちも違う。
- PL/Iの `FIXED DECIMAL(p, q)`:
ハードウェアレベル(IBM System zのBCD演算命令)でガリガリ処理される。全桁数 $p$(最大31桁)と小数桁数 $q$ が厳密にハードウェアの制約とコンパイラによって管理され、計算途中の丸めも決められたルール(基本は四捨五入ではなく、代入時の切り捨て・切り上げなど文脈依存)で行われる。
- Javaの `BigDecimal`:
多倍長浮動小数点数をソフトウェアでエミュレートしたものだ。非常に柔軟だが、「丸めモード(Rounding Mode)」を明示的に指定しないと、些細な演算でも容赦なく `ArithmeticException` を吐くか、予期せぬスケール(有効桁数)の変動を引き起こす。
特に、VSAMファイルやシーケンシャルファイル(BSAM)のレコードレイアウトをそのままJavaのDTO(Data Transfer Object)にマッピングする際、この違いが致命傷になる。
—
2. 現場で起きた恐怖のトラブル事例
あるメガバンクの勘定系マイグレーションで実際にあった話だ。
PL/Iで書かれた金利計算バッチをJava(Spring Batch)に書き換えたところ、数百万件のテストデータのうち、わずか数件だけ「利息の端数」が1円ズレる現象が発生した。
原因を調査すると、PL/Iのコードでは以下のような暗黙の演算が行われていた。
1
/ PL/I側での定義と演算 /
DCL 利率 FIXED DEC(5,4) INIT(0.0125);
DCL 元本 FIXED DEC(11,2) INIT(1000000.00);
DCL 利息 FIXED DEC(11,2);
/ 掛け算の瞬間、結果の精度はどうなるか? /
利息 = 元本 利率; / 演算結果は (16, 6) となり、受入側 (11, 2) へ暗黙の代入丸めが発生 /
PL/Iでは、乗算結果の精度はコンパイラの仕様(代入先の精度とは独立して一時的に拡張される)に従い、最終的に受入側の変数に代入されるタイミングで、PL/Iランタイムのデフォルトルール(通常は四捨五入ではなく、端数切捨て、あるいはオーバーフローチェック付きの代入)で丸められる。
これをJavaで何も考えずに `principal.multiply(interestRate)` とだけ書くと、Javaの `BigDecimal` は「必要なだけの無限の精度(無限小数にならない限り)」を保持しようとし、さらに割り算や乗算のたびにスケールが変わり、最終的な丸めタイミングがPL/Iと完全にずれてしまうのだ。
—
3. 実践:PL/IコードとJavaリプレイス時の模範解答
では、メインフレーム側の正しいPL/Iの記述と、それをJavaで安全に再現するためのアプローチを見ていこう。
まずは、実務でよくあるVSAMレコードの読み込みと、DECIMAL演算を行うPL/Iのサンプルコードだ。大文字記述、適切なインデント、そして組み込み関数(BUILTIN)の活用に注目してほしい。
1
/ ========================================================== /
/ プログラム名: CALC01PL /
/ 概要: VSAMファイルから元本を読み込み、利息を計算して出力する /
/ ========================================================== /
CALC01PL: PROC OPTIONS(MAIN);
/ 宣言部 /
DCL 1 ACCOUNT_REC,
5 ACC_ID CHAR(8), / 口座番号 /
5 ACC_PRINC FIXED DEC(11,2), / 元本 (総桁11, 小数2) /
5 ACC_RATE FIXED DEC(5,4), / 利率 (総桁5, 小数4) /
5 ACC_INTEREST FIXED DEC(11,2); / 利息 (総桁11, 小数2) /
DCL EOF_FLG CHAR(1) INIT(‘OFF’);
DCL APPR_INTEREST FIXED DEC(13,6); / 演算用一時変数 (精度拡張) /
/ ファイル定義 (VSAM KSDSを想定) /
DCL ACCFILE FILE RECORD SEQUENTIAL INPUT;
ON ENDFILE(ACCFILE) EOF_FLG = ‘ON’;
OPEN FILE(ACCFILE);
READ FILE(ACCFILE) INTO(ACCOUNT_REC);
DO WHILE(EOF_FLG = ‘OFF’);
/ 【重要】PL/Iの演算挙動:
FIXED DEC(11,2) と FIXED DEC(5,4) の乗算。
一時的に高精度な中間バッファ(DEC 16,6相当)で計算される。 /
APPR_INTEREST = ACC_PRINC ACC_RATE;
/ 代入時にターゲットの精度 FIXED DEC(11,2) へ丸められる
(PL/Iのデフォルトは半端な値は切り捨てではなく代入時ストア) /
ACC_INTEREST = APPR_INTEREST;
/ 組み込み関数を用いたデータの加工例 (例: 編集出力) /
PUT SKIP EDIT (ACC_ID, ‘ 利息=’, ACC_INTEREST)
(A(8), A(7), F(11,2));
READ FILE(ACCFILE) INTO(ACCOUNT_REC);
END;
CLOSE FILE(ACCFILE);
RETURN;
END CALC01PL;
このPL/IロジックをJava(BigDecimal)へ正確に移植するポイント
上記のPL/Iの挙動をJavaで完全にエミュレートするには、以下の3点をJava側のコード(またはユーティリティクラス)に強制させなければならない。
1. スケール(小数点以下の桁数)の明示的な維持
2. 乗算・除算時の丸めモード(Rounding Mode)の統一(メインフレームのコンパイラオプションやCOBOL/PL/Iの仕様に合わせる。多くは `RoundingMode.DOWN` または `HALF_UP`)
3. 計算途中の精度あふれ(オーバーフロー)の防止
Javaでこれを実装する場合のイメージはこうだ。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class AccountCalculator {
// PL/Iの FIXED DEC(11,2) 相当を担保するためのスケール定義
private static final int PRINCIPAL_SCALE = 2;
private static final int RATE_SCALE = 4;
private static final int INTEREST_SCALE = 2;
// メインフレームの挙動に合わせたデフォルト丸めモード
// (※プロジェクトの要件定義書で「切り捨て」か「四捨五入」かを必ず確認すること)
private static final RoundingMode DEFAULT_ROUNDING = RoundingMode.DOWN;
public BigDecimal calculateInterest(BigDecimal principal, BigDecimal rate) {
// 1. 元本と利率のバリデーション(PL/Iのピクチャ句やDEC制限の模倣)
if (principal.scale() > PRINCIPAL_SCALE) {
principal = principal.setScale(PRINCIPAL_SCALE, DEFAULT_ROUNDING);
}
// 2. 乘算の実行
// PL/Iの中間変数演算 (DEC 16,6相当) を再現するため、一時的にスケールを広げるか、
// あるいは直接ターゲットのスケール+αで計算する。
BigDecimal intermediateInterest = principal.multiply(rate);
// 3. 最終的な受入側変数 (FIXED DEC(11,2)) への代入時の丸めを再現
BigDecimal finalInterest = intermediateInterest.setScale(INTEREST_SCALE, DEFAULT_ROUNDING);
return finalInterest;
}
}
—
4. ベテランからの現場の助言(まとめ)
いいか、後輩よ。マイグレーションプロジェクトにおいて「動けばいいや」の精神で作られた `BigDecimal` の計算処理は、のちに本番稼働後の監査や決算期に必ず牙をむく。
- 「なんとなく `scale()` を省略するな」:Javaで `new BigDecimal(“0.1”)` を適当に扱うと、背後でとんでもない桁数のゴミデータがくっついてきて、データベース(Db2 for z/OS から PostgreSQL や Oracle への移行など)に格納する際にエラーか切り捨てを引き起こす。
- 「ONユニットの代わりをJavaでどう書くか考えろ」:PL/Iには強力な `ON` 条件ブランチ(OVERFLOWやZERODIVIDEなど)がある。Java側でも例外処理(`ArithmeticException`)をどこでキャッチし、どうメインフレームと同等のエラーログを出力するか、設計段階で共通部品化しておかなければならない。
PL/Iの仕様書、そしてコンパイラの生成するコードの癖を理解しているお前なら、単なる「文法の置き換え」ではなく、「ハードウェア演算のシミュレーション」としてこの問題にアプローチできるはずだ。
分からないことがあれば、いつでも俺の席に来い。一緒にメインフレームのダンプリストとJavaのスタックトレースを並べて睨み合ってやろう。それじゃあ、次のタスクにかかれ!
