おい、最近配属された若手が「先輩、PL/Iって変数名に `IF` とか `SUM` って書いてもエラーにならないんですけど、これってバグですか?」って聞いてきたんだよ。
笑っちまうだろ? 今時の言語に慣れてると、予約語を自由に使えすぎるPL/Iの仕様は逆に気味が悪いらしい。でもな、これこそがIBMメインフレームの歴史と、変態的なまでの言語設計の懐の深さを示しているところなのさ。PL/Iには、いわゆる「完全な予約語」っていうのがほとんど存在しない。コンテキスト(文脈)で判断するから、`IF = IF + 1;` なんていう悪夢のようなコードすら(やろうと思えば)書けてしまう。
まあ、そんな話は置いておいてだ。今回は、現場のバッチ改修やマイグレーション調査で「必ずと言っていいほどハマる」、ビルトイン関数 `FIXED` による数値の精度調整について、骨太に解説してやる。電卓叩くみたいに適当に書いてると、月末の金額計算で数円ずれて夜間バッチがひっくり返るからな。耳の穴かっぽじってよく聞くんだぞ。
—
1. なぜPL/Iの `FIXED` 関数と精度制御が実務で重要なのか
メインフレームの基幹系バッチ処理、例えば勘定系や大量の受発注データを扱うVSAMファイルやレコード入出力の世界では、数値の「桁あふれ(OVERFLOW)」や「暗黙の型変換による精度の欠落」は一発レッドカードの不具合だ。
COBOLなら `COMPUTED` や `ROUNDED` のお作法があるが、PL/Iの世界では、算術演算の精度(スケールとプレシジョン)は「式が評価されるコンテキスト」によって動的に決まる。ここで厄介なのが、浮動小数点演算や割り算を行ったときの「無限小数」や、意図しない有効桁数の肥大化だ。
ここで登場するのが `FIXED` ビルトイン関数 だ。
`FIXED(expression, precision [, scale])` を使うことで、任意の計算結果を、強制的に指定した固定小数点形式(Fixed-Point)の精度にねじ伏せる(=丸め、または切り捨てる)ことができる。
ベテランが教える実務上の鉄則
1. 割り算の後は必ず `FIXED` で受けろ
PL/Iで `/` 演算子を使うと、コンパイラは最大限の精度を維持しようとして、スケールがとんでもなく深い(小数点以下がやたらに長い)値を返してくる。これをそのままDB2のDECIMAL項目や固定長のVSAMレコードに突っ込むと、思わぬデータ切り捨てやS0C7(データ例外)の温床になる。
2. 切り捨てか四捨五入(丸め)かの違いを意識しろ
`FIXED` 関数単体は、基本的には指定桁数への「切り捨て(Truncation)」として動作する。四捨五入したい場合は、算術的なテクニック(5を加算してから切り捨てる等)を組み合わせるか、状況に応じた設計が必要になる。
—
2. 実践:VSAM入出力と `FIXED` を絡めたバッチ処理のコード例
百聞は一見にしかずだ。実際のメインフレームの現場で動いているような、月次売上計算バッチの骨組みを模したPL/Iプログラムを見せてやろう。
このコードでは、入力された売上レコードから単価と数量を読み込み、税率を掛け合わせた上で、特定の精度(整数部7桁、小数部2桁)に `FIXED` 関数でビシッと丸めてVSAM出力ファイルに書き込んでいる。
1
/ ========================================================== /
/ PROGRAM-ID: SLSCALC1 /
/ REMARK : 売上計算およびFIXED関数による精度調整サンプル /
/ ========================================================== /
SLSCALC1: PROC OPTIONS(MAIN);
/ — 1. 宣言部 — /
/ 入力VSAMレコード定義 /
DCL 1 IN_REC,
3 IN_ITEM_ID CHAR(8), / 商品コード /
3 IN_QTY DEC FIXED(5,0), / 数量 /
3 IN_UNIT_PRICE DEC FIXED(9,2); / 単価 /
/ 出力VSAMレコード定義(金額は小数点以下2桁に固定) /
DCL 1 OUT_REC,
3 OUT_ITEM_ID CHAR(8), / 商品コード /
3 OUT_TOTAL_AMT DEC FIXED(9,2); / 計算後金額 /
DCL EOF_FLG CHAR(1) INIT(‘OFF’);
DCL TAX_RATE DEC FIXED(3,3) INIT(1.080); / 消費税率 /
/ ファイル定義 /
DCL IN_FILE FILE RECORD INPUT ENVIRONMENT(CONSECUTIVE);
DCL OUT_FILE FILE RECORD OUTPUT ENVIRONMENT(CONSECUTIVE);
/ — 2. ファイルオープン — /
OPEN FILE(IN_FILE) INPUT;
OPEN FILE(OUT_FILE) OUTPUT;
/ 終了条件(ENDFILE)に対するONユニットの定義 /
ON ENDFILE(IN_FILE) EOF_FLG = ‘ON’;
/ — 3. メインループ — /
READ FILE(IN_FILE) INTO(IN_REC);
DO WHILE (EOF_FLG = ‘OFF’);
/ [重要] 演算結果の精度暴走を防ぐため、FIXED関数で精度を明示的に固定 /
/ 計算式: 数量 × 単価 × 税率 /
/ DECIMAL FIXED(9,2) に形を整えることで、後続処理での桁あふれを防ぐ /
OUT_REC.OUT_ITEM_ID = IN_REC.IN_ITEM_ID;
OUT_REC.OUT_TOTAL_AMT = FIXED(
IN_REC.IN_QTY IN_REC.IN_UNIT_PRICE TAX_RATE,
9, / 全体桁数 (Precision) /
2 / 小数点以下桁数 (Scale) /
);
/ 出力ファイルへ書き出し /
WRITE FILE(OUT_FILE) FROM(OUT_REC);
/ 次レコード読み込み /
READ FILE(IN_FILE) INTO(IN_REC);
END;
/ — 4. 終了処理 — /
CLOSE FILE(IN_FILE);
CLOSE FILE(OUT_FILE);
PUT SKIP LIST(‘ SLSCALC1 NORMAL END ‘);
END SLSCALC1;
—
3. ONユニットの制御フローとエラーハンドリングの極意
さて、上記のコードではさらっと流したが、実務では `FIXED` 関数を使った精度調整や演算の最中に、万が一の「SIZE条件(規模例外)」や「OVERFLOW条件」が発生するリスクを考慮しなければならない。
例えば、`FIXED(expression, 9, 2)` と指定したにもかかわらず、計算結果が整数部で9桁を超えてしまった場合(例: 100000000.00 など)、PL/Iのデフォルトの挙動ではプログラムが異常終了(ABEND)するか、あるいはサイレントに上位桁が切り捨てられてデータ破損を引き起こす。
ここでプロの腕の見せ所となるのが、ONユニットによる例外処理の捕捉だ。
1
/ SIZE条件(桁あふれ)が発生した際の割り込み処理 /
ON SIZE
BEGIN;
PUT SKIP LIST(‘ ERROR: 演算結果が定義された精度を超えました。’);
PUT SKIP LIST(‘商品コード: ‘, IN_REC.IN_ITEM_ID);
/ 必要に応じてダンプ採取やエラーログへの退避、異常終了コードの設定 /
SIGNAL ERROR;
END;
このように、予期せぬデータの暴走に対して、`ON SIZE` ユニットを適切に配置しておくことで、夜間バッチが原因不明のS0C7やデータ不正で沈没するのを防ぎ、スマートにエラーハンドリングを行うことができる。これが組まれていないシステムは、保守フェーズに入ったときに地雷原を歩くようなものだ。
—
4. 先輩からのメッセージ
PL/Iという言語は、古いけれど非常にパワフルだ。コンパイラが「よしなに」やってくれる部分が多い反面、今回紹介した `FIXED` のようなビルトイン関数できっちり手綱を引いてやらんと、とんでもないところで足元をすくわれる。
識別子に予約語がない自由度の高さに甘えず、自分で変数のスコープ、データ属性、そして計算結果の精度を完全にコントロールする。これができるようになれば、お前も立派なメインフレーム・アーキテクトだ。
次の改修案件でも、変数のデータ属性と演算結果の精度のギャップに目を光らせてくれよ。期待してるぞ。
