【入門編】ROUND関数の精度制御と丸め誤差の回避 – PL/Iの基本構文とデータ制御実践ガイド

こんにちは!メインフレームの荒波へようこそ。
JavaやCOBOLといった、現代的あるいはビジネスシーンでおなじみの言語をバリバリ書いてきた方にとって、IBMメインフレームの世界、そして「PL/I(ピーエルアイ)」という言語は、最初に少し身構えてしまう存在かもしれませんよね。

「なんだか変数名のルールが独特らしい…」
「ポインタや謎のデータ属性があって怖そう…」

そんな風に思っていませんか?でも、大丈夫ですよ。一つずつ蓋を開けて中身を覗いていけば、PL/Iは非常に合理的で、かつての人たちが知恵を絞って作り上げた「美しい巨大なエンジン」のようなものだと分かります。

今回は、そんなPL/Iの世界への第一歩として、「識別子の命名規則と予約語を持たない自由な世界」に少し触れたあと、実務のバッチ処理や金融計算の改修で絶対に避けて通れない「ROUND関数の精度制御と丸め誤差の回避」について、じっくりと紐解いていきたいと思います。

肩の力を抜いて、コーヒーでも飲みながら読んでいってくださいね。

—

1. PL/Iって実は優しい?「予約語」がない世界のハナシ

JavaやCOBOLを触ってきた方だと、こんな経験があるのではないでしょうか。
「あ、この英単語、言語のキーワード(予約語)だから変数名に使えないや……別の名前にしよっと」

プログラミング言語の多くは、文法をコンパイラが正しく理解するために、「IF」や「READ」「SUM」といった単語を予約語としてガチガチに固めています。

ところが、PL/Iには「厳密な意味での予約語」がありません。

「えっ、じゃあ `IF` を変数名にしちゃったら、コンパイラが混乱しないの?」と思いますよね。そこがPL/Iの面白いところであり、少しレガシーな愛嬌のあるところです。PL/Iのコンパイラは、文脈(コンテキスト)を見て判断します。

1
/ PL/Iの驚きの世界 /
IF = 10; / これ、変数名としての ‘IF’ です /
IF IF = 20 THEN / こっちは制御文の ‘IF’ です /
/ 処理がここに入ります /

人間が読んだら「なんて読みにくいんだ!」と頭を抱えたくなりますが、コンパイラは前後の文脈でちゃんと「おっ、ここでのIFは変数だな」「こっちは条件分岐のキーワードだな」と見分けてくれます。

とはいえ、実務の現場でこんな書き方をしたら、コードレビューで先輩からこってり絞られます(笑)。実務ではもちろん、分かりやすい変数名をつけますが、「言語仕様としてガチガチに縛られていない柔軟な言語なんだな」と知っておくだけで、PL/Iに対する恐怖心が少し薄れるのではないでしょうか。

—

2. 金融システムの命運を握る「ROUND関数」の正体

さて、ここからが今回の本題です。
基幹システム、特に銀行や保険などの勘定系システムにおいて、お金の計算(金利計算や手数料算出など)は1円の狂いも許されないシビアな世界です。

Javaなら `BigDecimal` を使って丸めモードを指定したり、COBOLなら `ROUNDED` 句を使ったりしますよね。では、PL/Iではどうするのでしょうか?ここで登場するのが `ROUND` 関数 です。

しかし、メインフレームのPL/Iで使われる固定小数点数(`FIXED DECIMAL`など)の計算では、何も考えずに丸めを行っていると、私たちが意図しない「丸め誤差」や「桁落ち」という魔物が忍び寄ってきます。

データのストレージ属性と「内部的な世界」

PL/Iでよく使われる数値データ型に `FIXED DECIMAL` があります。
例えば、以下のような宣言をよく見かけます。

1
DCL WK-AMOUNT FIXED DECIMAL(11, 2) ; / 全11桁、うち小数点以下2桁 /

これ、COBOLでいう `PIC S9(9)V99 COMP-3` と同じ、パック十進数(Packed Decimal)としてメインフレームのメモリ(またはストレージ)に格納されます。人間にとっては10進数で扱いやすいのですが、コンピュータのCPUは本来、2進数の世界です。

この10進数をかけ算したり割り算したりするとき、PL/Iの内部ではどのようなことが起きているのでしょうか?

1. 乗算・除算による桁あふれの防止
例えば、小数点以下2桁同士を掛け合わせると、数学的には小数点以下4桁になります。PL/Iは精度を落とさないよう、内部的に一時的な作業領域(レジスタや拡張ワークエリア)でより精度の高い状態を保ちます。
2. シフトと加算の組み合わせによる丸め
指定された桁数(例えば小数点以下2桁)に戻す際、PL/Iのランタイムは単に切り捨てるのではなく、指定桁の「1つ下の桁」に `5` に相当する値を加算(四捨五入の場合)してから、不要な下位桁をシフト(切り捨て)するアルゴリズムを内部的に実行します。

「あれ、値がズレる?」丸め誤差を生む落とし穴

この内部的な加算とシフトの仕組み、非常に理にかなっているのですが、「割り算」が絡むと牙を剥きます。

例えば、3で割るような割り切れない割算をしてみましょう。

1
DCL RESULT FIXED DECIMAL(5, 2);
DCL RATE FIXED DECIMAL(5, 4);

RATE = 1 / 3; / 0.3333 になる /
RESULT = ROUND(RATE 100, 2); / さあ、どうなる? /

もし、中間計算の段階で有効桁数が途中で丸められてしまうと、その累積が「誤差」となって、日次バッチの最終的な合計金額が「1円合わない(いわゆる端数ズレ)」という、メインフレームエンジニアが最も恐れる現象を引き起こします。

—

3. 怖くない!丸め誤差を回避する実践的コーディングパターン

では、このレガシーな世界で、どのように精度を維持し、丸め誤差を華麗に回避すればよいのでしょうか。実務で使える安全なコーディングのポイントを、サンプルコードとともに見ていきましょう。

実践コード:安全な丸め処理と精度の維持

1
————————————————————–

  • プログラム名: CALC01
  • 概要 : 割算とROUND関数を用いた安全な金額計算のサンプル

————————————————————–
CALC01: PROC OPTIONS(MAIN);

/ 変数の宣言:FIXED DECIMALで精度を明示的に大きく取る /
DCL W-BASE-AMT FIXED DECIMAL(13, 2) INIT(1000.00); / 元金 /
DCL W-RATE FIXED DECIMAL(7, 5) INIT(0.03333); / 利率 /
DCL W-CALC-WRK FIXED DECIMAL(15, 6); / 中間計算用(余裕を持たせる) /
DCL W-FINAL-AMT FIXED DECIMAL(11, 2); / 最終結果 /

/ 1. 中間計算はできる限り精度を落とさない(桁数を大きく保つ) /
/ × W-FINAL-AMT = W-BASE-AMT W-RATE; (これだと精度が落ちる) /

W-CALC-WRK = W-BASE-AMT W-RATE;

/ 2. ROUND関数を使う際は、丸める直前まで十分な小数点以下の桁数を維持する /
/ ROUND( 表达式, 戻したい小数点以下の桁数 ) /

W-FINAL-AMT = ROUND(W-CALC-WRK, 2);

/ 結果の確認用出力 /
PUT SKIP EDIT (‘計算結果: ‘, W-FINAL-AMT) (A, F(10,2));

END CALC01;

アーキテクトからのワンポイントアドバイス

1. 中間ワークは「ケチらず大きく」宣言する
PL/Iの計算精度は、代入される側の変数だけでなく、「計算式の右辺(中間結果)がどのデータ属性で評価されるか」に強く依存します。右辺の計算途中で桁あふれや意図しない切り捨てが起きないよう、中間ワーク(上記の `W-CALC-WRK` のように)は小数点以下の桁数を多め(例: 6桁や8桁)に確保するのが鉄則です。
2. ROUND関数は「最後の砦」として使う
計算の途中で何度も `ROUND` を挟むと、その都度内部的な加算と丸め(シフト)が行われ、誤差が積み重なります。`ROUND` は、すべての計算が終わった「最後の出力直前」に1回だけ通すのが、誤差を回避する最も美しいアプローチです。

—

おわりに

いかがでしたでしょうか?
予約語がないという少し変わった気質を持ちながらも、PL/Iはデータの精度を極限までコントロールできる、非常に頼もしい言語です。

「内部でどんな加算やシフトが行われているか」を少しだけイメージできるようになると、レガシーなコードを眺める視点もガラリと変わるはずです。

メインフレームのモダナイゼーションや保守開発でPL/Iに出会ったとき、「うわ、難しそう…」と逃げ出したくなったときは、この記事の「内部の仕組み」を思い出してみてくださいね。

それでは、次回のメインフレーム・アーキテクチャでお会いしましょう!

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