【実務・中級編】マイグレーション時のデータ型不一致:PL/IのFIXED DECとJavaのBigDecimal – PL/Iの基本構文とデータ制御実践ガイド

メインフレームの心臓部を守る者へ:PL/IからJavaへデータを渡すときの「小数点以下の罠」

おい、最近アセスメントが進んでいるオープン系へのマイグレーション案件、お前も頭を悩ませていないか?
「PL/Iで長年動き続けてきた基幹システムのバッチ群を、丸ごとJavaにリプレイスする」――響きはモダンで清々しいが、現場の我々からすれば、地雷原を裸足で歩くようなものだ。

特に、データ構造の移行で最も恐ろしいのは、コンパイルエラーにもならず、テスト環境をもするりと抜けて、本番稼働の土壇場で「金額の端数が合わない」「突然のSIZING(桁あふれ)例外」として牙をむく数値型の暗黙の解釈差異だ。

今回は、PL/Iの看板データ型である `FIXED DECIMAL`(固定小数点数)と、Javaの `java.math.BigDecimal` の間で起きる、スケールと精度の恐るべき仕様のギャップについて、実務の現場目線で徹底的に解説してやろう。

1. なぜPL/Iの `FIXED DEC` とJavaの `BigDecimal` で事故が起きるのか

メインフレームのCOBOLやPL/Iを触ってきた人間なら常識だが、PL/Iの `FIXED DECIMAL(p, q)` は、符号を含む総桁数(Precision: `p`)小数点以下の桁数(Scale: `q`) を完全に物理レベルでコンパイラに刻み込む。

例えば、以下のようなPL/Iの定義を見てほしい。

DCL 1 ACCT_RECORD,
5 ACCT_ID FIXED DEC(9,0), / 口座番号(整数9桁) /
5 BALANCE FIXED DEC(11,2); / 残高(全体11桁、うち小数2桁) /

この `BALANCE` は、内部的には合計11桁分のパック十進数(Packed Decimal / COMP-3)として、メインフレームのメモリやVSAMファイル上にパツパツに詰め込まれている。プラスマイナスの符号を含めて、11桁が確実に保証されるわけだ。

Javaの `BigDecimal` が持つ「自由すぎる」罠

これをJavaに移行する際、大半の自動変換ツールや、深く考えずに実装したプログラマは、単純にこう書く。

// ありふれた、しかし危険なJava側の定義
BigDecimal balance;

Javaの `BigDecimal` は非常に柔軟で素晴らしいクラスだが、「デフォルトでは桁数やスケールの制約を強制しない」という性質を持っている。
PL/I側で `FIXED DEC(11,2)` だったものが、Java側で演算を繰り返すうちにスケールが勝手に変わり(例えば割り算をして小数第5位になったり)、データベースや後続のファイルに書き戻す瞬間に予期せぬ丸めやオーバーフローを引き起こす。

さらに厄介なのは、「丸めモード(Rounding Mode)」のデフォルト値の違いだ。
メインフレームのハードウェア演算やPL/Iランタイムが暗黙的に行う切り捨て・四捨五入と、Javaの `BigDecimal.divide()` や `setScale()` で明示的に指定しない場合の挙動がズレると、日次バッチの総額が「1円合わない」という、経理部を激怒させる悪夢のような現象を引き起こす。

2. 実践:VSAM入出力とONユニットによる例外制御の現実

メインフレームでは、データが定義された桁数を超えて飛んできた場合や、不正なゾーン・パックデータが混入した場合には、ハードウェア割り込みやPL/Iの ONユニット が即座に検知し、システムダウンやリカバリ処理へと導く。

Javaへの移行においても、この「厳格な桁あふれ・精度チェック」を再現しなければならない。まずは、現場でよく見られる堅牢なPL/Iのデータ入力・編集処理のコードを見てみよう。

【PL/I側】厳格なデータ定義とONユニットの例

//
/ プログラム名: MIG01M /
/ 概要 : VSAMファイルから残高データを読み込み、Java連携用に /
/ フォーマットを整えて出力するバッチサブシステム /
//
MIG01M: PROC OPTIONS(MAIN);

DCL 1 VSAM_IN_AREA,
10 IN_ID FIXED DEC(9,0),
10 IN_BAL FIXED DEC(11,2);

DCL OUT_FILE FILE RECORD OUTPUT;
DCL EOF_FLG CHAR(1) INIT(‘0’);

/ 演算上の桁あふれ(SIZING)を捕捉するONユニット /
ON SIZING BEGIN;
PUT SKIP LIST(‘【重大エラー】数値の桁あふれ(SIZING)を検出しました。’);
SIGNAL ERROR;
END;

/ ファイルオープン等の処理は省略(実務標準のメインフレーム環境を想定) /

DO WHILE (EOF_FLG = ‘0’);
/ VSAMからのレコード読み込み(READ文) /
READ FILE(VSAM_IN) INTO(VSAM_IN_AREA);

IF ENDFILE(VSAM_IN) THEN
EOF_FLG = ‘1’;
ELSE DO;
/ データの妥当性チェックとビジネスロジック適用 /
CALL PROCESS_BALANCE(IN_ID, IN_BAL);
END;
END;

RETURN;

PROCESS_BALANCE: PROC(P_ID, P_BAL);
DCL P_ID FIXED DEC(9,0) PARAMETER;
DCL P_BAL FIXED DEC(11,2) PARAMETER;
DCL W_CALC_BAL FIXED DEC(13,4); / 作業用:より精度の高い変数 /

/ 内部での掛け算:精度落ちを防ぐために大きめの変数を経由 /
W_CALC_BAL = P_BAL 1.08; / 消費税相当額の計算など /

/ 最終的に出力用バッファへ代入する際に暗黙の丸め・桁あふれチェックが走る /
PUT SKIP EDIT (‘ID:’, P_ID, ‘ CALC_BAL:’, W_CALC_BAL)
(A, F(9), A, F(13,4));

END PROCESS_BALANCE;

END MIG01M;

このPL/Iコードのポイントは、作業用変数 `W_CALC_BAL` に `FIXED DEC(13,4)` を使い、計算途中の精度を保持している点だ。そして、もしこの計算結果が最終的な出力先の桁数制限を超えれば、`ON SIZING` が発火して不正データを即座に検知できる。

3. Java移行時にこの厳格さをどう再現するか?

では、このPL/Iの思想をJavaで完全に再現するにはどうすればよいか。
答えは、「スケール(小数位)」と「プレシジョン(総桁数)」、そして「丸めモード(RoundingMode)」をラップしたユーティリティクラス、あるいはBean設計の徹底だ。

以下に、Java側でPL/Iの `FIXED DEC(11,2)` 相当を安全に取り扱うための実装パターンを示す。

【Java側】`BigDecimal` を用いた厳格な数値制御クラス

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Pl1DecimalHandler {

/

  • PL/Iの FIXED DEC (p, q) の制約をJavaのBigDecimalに強制適用するメソッド
  • @param value 入力値
  • @param precision 全体桁数 (p)
  • @param scale 小数位 (q)
  • @return 丸め処理と桁あふれチェック済みの BigDecimal

/
public static BigDecimal applyPl1DecRules(BigDecimal value, int precision, int scale) {
if (value == null) {
return BigDecimal.ZERO.setScale(scale);
}

// 1. スケール(小数位)の強制と、メインフレームで一般的な「切り捨て (DOWN)」または「四捨五入 (HALF_UP)」の適用
// ※PL/Iの代入時の挙動は基本的に切り捨て(TRUNCATE)か標準的な丸めに依存するため、設計書を確認すること
BigDecimal adjusted = value.setScale(scale, RoundingMode.HALF_UP);

// 2. 整数部の許容最大桁数を算出 (precision – scale)
int integerDigitsLimit = precision – scale;

// 整数部の実際の桁数をチェック(符号を除く、あるいは全体の値としての絶対値チェック)
// 桁あふれ(SIZING)が発生している場合は例外をスローし、メインフレームのONユニット挙動を模倣する
BigDecimal maxAllowed = BigDecimal.TEN.pow(integerDigitsLimit);
if (adjusted.abs().compareTo(maxAllowed) >= 0) {
throw new ArithmeticException(“【Java移行エラー】SIZING: 数値が許容桁数を超過しました。定義値(p=”
+ precision + “, q=” + scale + “)”);
}

return adjusted;
}

// — 実行テスト用メインメソッド —
public static void main(String[] args) {
try {
// 例:FIXED DEC(11,2) への代入をシミュレート
BigDecimal rawInput = new BigDecimal(“123456789.876”);

BigDecimal safeBalance = applyPl1DecRules(rawInput, 11, 2);
System.out.println(“安全に整形された残高: ” + safeBalance); // 出力: 123456789.88

// 桁あふれを起こすテスト
BigDecimal overInput = new BigDecimal(“12345678901.23”);
applyPl1DecRules(overInput, 11, 2);

} catch (ArithmeticException e) {
System.err.println(e.getMessage());
// ログ出力およびバッチ異常終了フローへ接続
}
}
}

4. ベテランからの現場の教訓:移行時のチェックリスト

オープン系への移行プロジェクトで、後輩たちが必ずと言っていいほどハマるポイントをいくつか授けておこう。

1. 「なんとなくdoubleやfloatを使うな」
オープン系に慣れた若いプログラマが、小数を扱うからといってJavaの `double` や `float` を使ったら一発レッドカードだ。浮動小数点数は二進数誤差を生むため、金融系の「1円たりともズレてはならない」世界では絶対に御法度。必ず `BigDecimal` を使え。
2. データベース(RDB)側の定義も合わせろ
Javaで `BigDecimal` を使っても、接続先のOracleやPostgreSQL側のカラム定義が `NUMBER(15,2)` のようになっていなければ意味がない。PL/Iの `FIXED DEC(p,q)` を、RDBの `NUMBER(p, q)` または `DECIMAL(p, q)` に1対1でマッピングするDDL設計が絶対条件だ。
3. テストデータは「境界値」を徹底的に攻めろ
「許容最大値の桁数ぎりぎり」「マイナス値」「小数点以下の端数丸めが発生するケース」この3パターンを、PL/Iの旧システムとJavaの新システムの両方で走らせ、出力ファイルの一致(バイナリ比較または完全一致比較)を取るまでが移行テストだということを忘れるな。

基幹システムの命は「正確性」と「堅牢性」だ。
言語やプラットフォームが変わろうとも、データが持つ重みと、そこに潜むリスクの本質は1ミリも変わらない。

さあ、手を動かして、今日も確実なコードを組み上げようか。何か躓いたら、いつでも俺のところに聞きに来い。

タイトルとURLをコピーしました