【入門編】DATEおよびDATETIME組み込み関数の西暦2000年問題と推奨代替関数 – PL/Iの基本構文とデータ制御実践ガイド

皆さん、こんにちは! IBMメインフレームの世界へようこそ。

私は長年、この巨大な鉄の塊(愛を込めてそう呼んでいます)と対話し、基幹システムの心臓部であるPL/Iプログラムの息吹を感じ取ってきたシステムアーキテクトです。JavaやCOBOLの経験はあるけれど、PL/Iは初めて、という方も多いのではないでしょうか。メインフレームの世界は一見すると難しそうに見えますが、一つ一つ丁寧に紐解いていけば、その奥深さと堅牢性に魅了されるはずですよ。ご安心ください、私がそっと隣に寄り添って、PL/Iの魅力をたっぷりお伝えしますね。

今回お話しするのは、PL/Iの歴史に残る大きな節目となった「西暦2000年問題」、そしてそれに深く関わる日付処理のお話です。特に、旧来の`DATE`関数や`DATETIME`関数が抱えていた「時限爆弾」と、その解決策として登場した現代的な関数群について、現場のリアルな経験を交えながら解説していきます。

PL/Iの「ちょっと変わった」お約束:識別子と予約語

まず、本題に入る前に、PL/Iの基本的なルールで、JavaやCOBOLとは大きく異なる点について触れておきましょう。それが「識別子(変数名)の命名規則と、予約語を持たない」というPL/Iのユニークな特徴です。

JavaやCOBOLでは、`public`や`class`、`MOVE`や`PERFORM`といった特定の単語は「予約語」として、変数名やメソッド名として使うことができませんよね。これは、コンパイラがプログラムの構造を正確に理解し、誤作動を防ぐために非常に重要なルールです。

ところが、PL/Iは違います。

PL/Iには「予約語」がない?

そうなんです。PL/Iには、JavaやCOBOLのような厳密な意味での「予約語」という概念がありません。もちろん、`DCL`(DECLARE)や`PROCEDURE`、`END`といったキーワードはありますが、これらを「文脈に応じて」解釈するのがPL/Iの流儀なんです。

これはどういうことかというと、例えば、`DATE`という単語は、組み込み関数として日付を返す機能を持っていますが、同時に「`DATE`という名前の変数」として自分で宣言することもできてしまうんです!

DCL DATE CHAR(6); / DATEという名前の文字型変数 /
DCL MY_DATE CHAR(6); / 普通の変数 /

MY_DATE = DATE(); / ここで組み込み関数DATEを呼び出す /
DATE = ‘202310’; / ここで変数DATEに値を代入 /

どうでしょう? ちょっと混乱しますよね? Javaで`String class = “Hello”;`なんて書いたらコンパイルエラーになるのと同じような感覚かと思います。

PL/Iでは、コンパイラが「これは組み込み関数として使われているな」「いや、これはユーザーが宣言した変数だな」と賢く判断してくれるのですが、これは時にプログラマーを悩ませる原因にもなります。特に、レガシーコードの改修においては、この特性が思わぬ落とし穴になることもあるんです。

この「予約語がない」という特性が、これから話す古い`DATE`関数の問題と深く関わってくるんですよ。

時限爆弾だった`DATE`組み込み関数と西暦2000年問題

さあ、本題です。PL/Iには、日付や時刻を取得するための組み込み関数として、古くから`DATE`や`DATETIME`というものが存在しました。

`DATE`関数が返していた「危ない」形式

旧来の`DATE`関数は、呼び出すと現在日付を`YYMMDD`という形式の`CHAR(6)`(6桁の文字データ)で返していました。例えば、1999年12月31日なら`’991231’`、2000年1月1日なら`’000101’`といった具合です。

これで何が問題だったか、もうお分かりですよね? そう、「世紀情報がない」ことなんです。

現場を震撼させた世紀の曖昧さ

1999年の終わりから2000年にかけて、世界中のシステムエンジニアが冷や汗をかいた「西暦2000年問題(Y2K問題)」。メインフレームの世界も例外ではありませんでした。

`DATE`関数が返す`YYMMDD`形式では、`’991231’`と`’000101’`を比較すると、文字列としては`’991231’`の方が大きい、つまり「2000年1月1日は1999年12月31日よりも前の日付だ」と誤って判断されてしまうんです。

考えてみてください。

DCL TODAY_DATE CHAR(6);
DCL REF_DATE CHAR(6) INIT(‘000101’); / 2000年1月1日 /

TODAY_DATE = DATE(); / 今日の日付を取得 (例: ‘991231’) /

IF TODAY_DATE > REF_DATE THEN
PUT SKIP LIST(‘今日は2000年1月1日より後です’);
ELSE
PUT SKIP LIST(‘今日は2000年1月1日より前か、同日です’);

もし1999年中にこのプログラムが実行され、`TODAY_DATE`が`’991231’`だった場合、`’991231′ > ‘000101’`は真となり、「今日は2000年1月1日より後です」と出力されてしまいます。これは完全に誤りですよね。

基幹システムでは、日付を使った様々な計算が行われています。

  • ローン残高の計算(最終返済日が未来かどうか)
  • 保険料の算出(契約期間が有効かどうか)
  • 商品の賞味期限チェック
  • レポートの集計期間

これら全てが、世紀の曖昧さによって誤動作する可能性があったのです。まさに「時限爆弾」でした。当時の現場は、膨大なプログラムコードをレビューし、この`YYMMDD`形式による日付比較箇所を特定し、修正する作業に追われ、まさに戦場でしたね…。

`DATETIME`関数も同様に`YYMMDDHMMSS`形式で返していたため、同じ問題を抱えていました。

救世主の登場:LE/370準拠の新しい日付関数

この巨大な危機を乗り越えるために、IBMはメインフレームの言語環境を一新しました。それが「Language Environment (LE/370)」です。PL/Iを含むCOBOL、C/C++といった言語が共通のランタイム環境で動作するようになり、日付処理についても、世紀の曖昧さがない新しい関数群が提供されることになりました。

これこそが、皆さんがレガシーシステムで出会うであろう、そして積極的に使うべき現代的な日付関数たちです。

CEEDATE、CEEDAYS、CEECVTDTなどの仲間たち

LE/370が提供する日付時刻関数は、その名前の多くが`CEE`で始まります。これらは、日付を内部的に「グレゴリオ暦日(またはRata Die)」という基準日からの経過日数で管理することで、世紀の曖昧さを完全に解消しました。

いくつか代表的な関数をご紹介しましょう。

1. `CEEDAYS`:

  • 日付(`YYYYMMDD`形式など)を受け取り、それを「グレゴリオ暦日」という固定小数点数(`FIXED BINARY(31)`)に変換します。これは、日付を数値として比較・計算するのに非常に便利です。
  • Javaで言うところの`java.time.LocalDate`オブジェクトの内部表現、COBOLで言うところの`YYYYMMDD`形式を数値として扱うイメージに近いかもしれません。

2. `CEEDATE`:

  • `CEEDAYS`で得られたグレゴリオ暦日と、希望する出力形式(`YYYYMMDD`、`MM/DD/YY`など)を指定することで、整形された日付文字列(`CHAR`型)を返します。
  • 日付を人間が読める形式に変換する際に使います。

3. `CEECVTDT`:

  • グレゴリオ暦日と時刻を受け取り、指定されたフォーマットの日付/時刻文字列に変換します。`DATETIME`関数の代替として使えます。

新しい日付関数の使い方(PL/Iコード例)

では、実際のPL/Iコードで見てみましょう。ちょっと見慣れない宣言が多いかもしれませんが、「Javaの`import`文や`new`演算子みたいなもの」と思ってくださいね。

/ ——————————————————————- /
/ LE/370準拠の新しい日付関数を使用した例 /
/ ——————————————————————- /
MY_DATE_PROGRAM: PROCEDURE OPTIONS(MAIN);

/ LE/370関数の宣言:外部関数なのでENTRY属性で宣言します /
DCL CEEDAYS ENTRY (
CHAR(), / 入力日付文字列 (例: ‘YYYYMMDD’) /
CHAR(), / 入力フォーマット文字列 (例: ‘YYYYMMDD’) /
FIXED BINARY(31), / 結果のグレゴリオ暦日 (Rata Die) /
FIXED BINARY(31) / エラーコード /
) EXTERNAL;

DCL CEEDATE ENTRY (
FIXED BINARY(31), / グレゴリオ暦日 (Rata Die) /
CHAR(), / 出力フォーマット文字列 (例: ‘YYYYMMDD’) /
CHAR(), / 結果の日付文字列 /
FIXED BINARY(31) / エラーコード /
) EXTERNAL;

/ 変数の宣言 /
DCL TODAY_CHAR8 CHAR(8); / YYYYMMDD形式の今日の日付 /
DCL GREG_DATE FIXED BINARY(31); / グレゴリオ暦日 (内部数値表現) /
DCL LE_FEEDBACK FIXED BINARY(31); / LE/370関数のフィードバックコード /
DCL CURRENT_DATE BUILTIN; / 古い組み込み関数DATEとは異なる、
LE/370準拠のCURRENT_DATE関数。
CHAR(8) YYYYMMDDを返す /

/ 1. 現在日付をYYYYMMDD形式で取得 (LE/370準拠のCURRENT_DATEを使用) /
/ 古いDATE関数はYYMMDDでしたが、CURRENT_DATEはYYYYMMDDを返します。/
TODAY_CHAR8 = CURRENT_DATE();
PUT SKIP LIST(‘現在のYYYYMMDD形式日付:’, TODAY_CHAR8);

/ 2. YYYYMMDD形式の日付をグレゴリオ暦日に変換 /
/ これで日付が数値として扱えるようになり、世紀の曖昧さがなくなります /
CALL CEEDAYS(
TODAY_CHAR8, / 入力日付: ‘20231027’ など /
‘YYYYMMDD’, / 入力フォーマット /
GREG_DATE, / 結果のグレゴリオ暦日 /
LE_FEEDBACK / エラーコード /
);

IF LE_FEEDBACK = 0 THEN DO;
PUT SKIP LIST(‘グレゴリオ暦日 (内部数値):’, GREG_DATE);

/ 3. グレゴリオ暦日を別のフォーマットに変換して表示 /
DCL DISPLAY_DATE CHAR(10); / MM/DD/YYYY 形式用 /
CALL CEEDATE(
GREG_DATE, / 入力グレゴリオ暦日 /
‘MM/DD/YYYY’, / 出力フォーマット /
DISPLAY_DATE, / 結果の日付文字列 /
LE_FEEDBACK / エラーコード /
);

IF LE_FEEDBACK = 0 THEN
PUT SKIP LIST(‘MM/DD/YYYY形式日付:’, DISPLAY_DATE);
ELSE
PUT SKIP LIST(‘CEEDATEエラー:’, LE_FEEDBACK);

/ 4. 日付の比較もグレゴリオ暦日で行う /
DCL FUTURE_DATE_STR CHAR(8) INIT(‘20250101’); / 2025年1月1日 /
DCL FUTURE_GREG_DATE FIXED BINARY(31);

CALL CEEDAYS(
FUTURE_DATE_STR,
‘YYYYMMDD’,
FUTURE_GREG_DATE,
LE_FEEDBACK
);

IF LE_FEEDBACK = 0 THEN DO;
IF GREG_DATE > FUTURE_GREG_DATE THEN
PUT SKIP LIST(‘今日の方が未来の日付です’);
ELSE IF GREG_DATE < FUTURE_GREG_DATE THEN PUT SKIP LIST('未来の日付の方が今日より後です'); ELSE PUT SKIP LIST('今日と未来の日付は同じです'); END; ELSE PUT SKIP LIST('CEEDAYS(FUTURE_DATE)エラー:', LE_FEEDBACK); END; ELSE PUT SKIP LIST('CEEDAYSエラー:', LE_FEEDBACK); END MY_DATE_PROGRAM; コードのポイント解説

  • `DCL CEEDAYS ENTRY (…) EXTERNAL;`
  • Javaの`import static`やCOBOLの`CALL`に相当する宣言です。`ENTRY`は「これは関数(またはサブルーチン)を呼び出すよ」という印で、`EXTERNAL`は「この関数は別のコンパイル単位(システムライブラリなど)にあるよ」という意味になります。引数の型や並びを正確に書く必要があります。
  • `FIXED BINARY(31)`
  • これはJavaの`int`やCOBOLの`COMP-5 S9(9)`に相当する、符号付き31ビットの整数型です。PL/Iでは、数値のビット数を細かく指定できるのが特徴ですね。グレゴリオ暦日はこの形式で扱われます。
  • `CURRENT_DATE`組み込み関数
  • 古い`DATE`関数と異なり、`YYYYMMDD`形式で日付を返す、現代的な組み込み関数です。LE/370環境で推奨されます。

このように、LE/370準拠の関数を使うことで、世紀の曖昧さから解放され、安全かつ正確な日付処理が可能になるんです。

レガシー移行への指針と注意点

もし皆さんが既存のPL/Iシステムを調査したり、改修したりする際、古い`DATE`関数や`DATETIME`関数を使っている箇所を見つけたら、それは「潜在的なバグの温床」だと認識してください。

1. コードの棚卸しとリスク評価

  • まずは、`DATE()`や`DATETIME()`を呼び出している箇所を特定しましょう。grepコマンドやメインフレームのISPFエディタのFINDコマンドなどで簡単に検索できます。
  • 見つかった箇所が、単に日付を表示しているだけなのか、それとも日付の比較や計算を行っているのかを慎密に確認してください。後者の場合は、リスクが非常に高いです。

2. 段階的な置き換え

  • いきなり全ての`DATE`関数を置き換えるのは大変です。影響範囲を見極め、小さい単位で段階的に`CEEDATE`や`CEEDAYS`などの関数に置き換えていきましょう。
  • 特に、日付を内部的に`FIXED BINARY(31)`のグレゴリオ暦日で持つように変更すれば、その後の比較や計算が非常に安全になります。

3. 徹底的なテスト

  • 日付処理の変更は、システムの根幹に関わるため、網羅的なテストが不可欠です。
  • 特に、年が変わる境界(12月31日→1月1日)や、特定の年の閏日(2月29日)など、エッジケースのテストは念入りに行いましょう。過去のデータを使って検証することも重要です。

4. 既存のデータ形式への配慮

  • プログラム内の日付処理は変更できても、ファイルやデータベースに`YYMMDD`形式で格納されているデータは、そう簡単には変更できないかもしれません。
  • その場合、プログラム内で`YYMMDD`を`YYYYMMDD`に変換するロジックを挟むなど、データ形式の「ブリッジ」を設ける工夫が必要になります。

たとえ今、古い`DATE`関数を使っているコードが問題なく動いているように見えても、それはたまたま「世紀をまたぐ処理」が最近発生していないだけかもしれません。いつか必ず、その「時限爆弾」が爆発する日が来る可能性があります。未来のトラブルを未然に防ぐためにも、この移行は非常に重要なんです。

まとめ:レガシーは怖くない、知れば知るほど面白い

いかがでしたでしょうか? PL/Iの古い`DATE`関数が抱えていた西暦2000年問題、そしてLE/370準拠の新しい日付関数への移行について、具体的なコード例を交えて解説しました。

PL/Iは、JavaやCOBOLとは異なる独特の癖を持っていますが、その分、非常に柔軟で強力な言語でもあります。特に「予約語がない」という特性は、時に思わぬ挙動を引き起こすこともありますが、その背後にある設計思想を理解すれば、PL/Iコードの深いところまで読み解けるようになります。

メインフレームの世界は、一見すると古い技術の宝庫のように見えますが、実は現代のクラウドネイティブな技術にも通じる、堅牢で安定したシステム構築の知恵が詰まっています。この「レガシー」な部分も、一つずつ紐解けば決して怖くありませんし、むしろ「なるほど!」と唸るような深い知見に満ちています。

これからも皆さんがPL/Iの魅力を発見できるよう、私が全力でサポートさせていただきますね。メインフレームはまだまだ現役、そして未来を支える技術として、その存在感を放ち続けています。ぜひ、この奥深い世界を一緒に探求していきましょう!

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