2018年3月 | ジョン・パリス、スーザン・ガントナー
前回の記事“RPGの新命令DATA-INTO: XML_INTOとOpen Accessの出会い”では、新しいDATA-INTO命令の基本的な機能とそのためのパーサー作成の基礎について説明しました。 PTFと命令についてはここを参照してください。
今回は、前回の記事で述べたいくつかの追加項目に触れ、CSVパーサーのより一般的なバージョンを紹介したいと思います。また、パーサーの開発を容易にするためにIBMが組み込んだトレース・サポートについても説明します。
パーサーに機能を追加する
まず、CSVパーサーの変更点について説明しましょう。オリジナルの例を単純にするために、そのパーサーは、解析対象のCSVファイル内に2つの特定のデータフィールドしかないことを前提にハードコードされていました。ここでは、元のパーサーを可変数のフィールドを処理できるように改良し、CSVデータには期待されるDSサブフィールドの名前を含めることができます。コードをより一般的なものにするためには、解析中に見つかったCSVデータ中のエラーを処理する方法と、特定のデータがいつ欠落したかを特定する方法を検討する必要があります。
コードの大半は前回の記事と変わらないのでその多くを示すことはしませんが、詳細を調べたいならば、私たちはすぐにソースコード・バンドルへのリンクを投稿します。そこにはメイン・パーサー(PARSECSV2)に加えて、多数のテスト・プログラムと、それらが利用するIFSファイルも含まれています。
フィールド名を提供するために、ファイルの見出し行を使うことにしました。なぜなら、この方法はCSVファイルでは一般的なものだからです。この実装を見てから、別のアプローチについて議論します。
(A) dcl-ds CSVData_T qualified template; count int(5); value varchar(256) Dim(100); end-ds; .... (B) // 列名を格納するためのデータ構造 dcl-ds columnData LikeDS(CSVData_T); // フィールドデータを格納するためのデータ構造 dcl-ds fieldData LikeDS(CSVData_T);
まず(A)で、テンプレートデータ構造 CSVData_Tを定義します。このデータ構造には、単にアクティブな要素の数と各要素の値が含まれます。要素サイズ(256)と要素数(100)は、ほとんどの目的に十分な大きさとなるよう任意に選びました。テンプレートを使用することで、将来的にこれらのサイズの変更が必要になった場合は、これを簡単に行えるようにしました。
次に(B)で、このテンプレートの2つのインスタンスを作成します。columnDataは、ファイル内の最初のレコードから抽出された列見出し(すなわち、フィールド名)を、そしてfieldDataは、後続のレコードから抽出されたフィールド値を保持するために使用されます。
列見出しをロードするプロセスは、レコードを取得することによって(C)から始まります。(D)で得られた値をテストし、データが得られたことを確認します。何も見つからなければ、DATA-INTOのAPIである QrnDiReportError() を使用してエラーを報告します。ひとたびエラーが報告されると、コントロールはパーサーには返されず、RPGが呼出しの2番目のパラメーターとして指定したエラーコードを含むエラーメッセージを発行することに注意してください。ここでは、このエラーステータスに定数NO_RECORDSで定義されるエラーコード値1を与えています。使用する番号はあなた次第ですが、どのコードが何を意味するのかを文書化するために何らかの標準を決めることをお勧めします。この場合、ジョブログのメッセージは次のようになります。
DATA-INTO操作のパーサーがエラーコード1を検出しました。
(E)では、getFields()プロシージャーを呼び出してレコードを渡し、列見出しデータ構造をロードします。このルーチンは、フィールド区切り文字および文字列の両端の二重引用符などのあらゆる特殊文字を処理します。ルーチンの仕組みについては、今後の記事で説明します。
制御が再びメインラインに戻るまでに、レコード見出しはすべてデータ構造 columnDataの値配列に格納され、フィールドcountにはアクティブなエントリの数が格納されています。このcountの重要性は後で明らかになります。
(C) // 列見出しレコード(第1レコード)を取得 record = getRecord(parseInfo); (D) If record = ''; // レコードなし。エラーメッセージを発行 QrnDiReportError(parm.handle: NO_RECORDS: 0); // 注: この呼出しからは戻らない Else; // 第1レコード内のフィールドから列見出しをロード (E) columnData = GetFields(record); EndIf;
列名が取得されると、ファイル内の各レコードを処理して、getFields()ルーチンを呼び出し、データ構造fieldDataに値を格納します。 次に(F)のループで、見つかったフィールドをすべてRPGに通知する処理に進みます。
現在のインデックスが、見つかったカラム名の数を超えているかどうかを(G)で確認していることに注意してください。これは、私たちが予期していたよりも多くのフィールドがレコードにあることを示しています。これはエラーとみなされ、前に行ったようにQrnDiReportError() APIで報告されます。
列見出し(フィールド名)があるフィールドごとに、(H)でその名前とそれに続く値を報告します。これは、レコード内のすべてのフィールドを処理し終わるまで続きます。
列名に関する簡単な要点:デフォルトでは、DATA-INTOは渡されたデータ値から空白文字を自動的に切り捨てて格納します。しかし、私たちは名前に対してこれを行わないという難しい方法を見つけました。末尾の空白文字は問題ではありませんが、先頭の空白文字は名前から削除しなければなりません。そうしないと、フィールド名の不一致が発生します。
この時点では、列見出しよりもデータが少ない、言い換えるとレコードの終わりにフィールドが漏れていると何が起きるのか不思議に思うかもしれません。これに応じるのは実はパーサーの機能ではなく、むしろDATA-INTO自身による制御です。このパーサーを利用したサンプル・プログラムを見て、これがどのように動作するかを見ていきます。
(F) For i = 1 to fieldData.count; (G) If i > columnData.count; // 見出しよりもフィールドが多い? QrnDiReportError( parm.handle : TOO_MANY_FIELDS : parseInfo.lineStartOffset); // 注:制御はこの呼出しから戻らない Else; // 列見出しを使ってサブフィールド名を通知する (H) QrnDiReportName ( parm.handle : %addr(columnData.value(i): *data) : %len(columnData.value(i)) ); // そのサブフィールドに格納する値を通知する QrnDiReportValue( parm.handle : %addr(fieldData.value(i): *data) : %len(fieldData.value(i)) ); EndIf; EndFor;
サンプル・プログラムを見る前に、ファイル内に列名がない場合の列名を取り扱うための代替オプションについて簡単に解説します。最初の最も明白な方法は、レコード内の各列の名前を単純に生成することです(例:column1、column2、column3など)。これは簡単な解決策ですが、大きな欠点はユーザープログラムにひどく意味のないフィールド名が付くことです。意味のある名前を付けるほうがずっと良いので、DATA-INTOの%PARSERパラメーターで用意されている追加ユーザー・パラメーターを使用して、フィールド名を与えるのがより良い方法でしょう。今後の記事では、それがどのように動作するかについて説明しますが、そんなに待てないというのであれば、DATA-INTOと共に出荷されているいくつかのサンプル・プログラムの一部でIBMがその使用法を説明しています。
パーサーの使用
このプログラムで処理されるSample2.csvファイルのデータのサンプルを次に示します。
Account,Name,City,State 3456,IBM,Rochester,MN 4567,"American Falls","Niagara Falls",NY ....
(J)では、DATA-INTOで抽出されたデータを保持するためにDS配列を定義しています。フィールド名は、ファイル内の最初のレコードの列見出しのフィールド名と一致する必要があることに注意してください。この例では、フィールドは列と同じ順序ですが、これは必須ではありません。DATA-INTOは、その兄であるXML-INTOと同様に、与えられた階層内のフィールドの名前に関心があるだけで、与えられたいかなるレベルの要素の並び順にも関心はありません。
(K)は、データ構造accountsにデータを読み込むDATA-INTO操作です。%DATA組み込み関数は、処理されるデータのソースおよび適用されるオプションを特定します。XML-INTOの場合と同様に、doc = fileオプションは、処理される実際のデータではなく、最初のパラメーターがファイルの名前を表すことを示します。同様に、case = anyは、ファイルの列見出しに大文字と小文字の混在を許しつつ、大文字と小文字の違いを無視してRPGのフィールド名と一致するようにします。前述の例と同様に、%PARSERはドキュメントの解析に使用するプログラムを識別します。
DATA-INTO操作が完了すると、(L)ではPSDSの各要素に対応する値を取得し、結果を表示する操作をPSDSの要素の数だけ繰り返します。
J) dcl-ds accounts Qualified Dim(200) Inz; account char(4); name char(30); city char(30); state char(2); end-ds; dcl-s pad char(1) Inz; // numElements にはDATA-INTO操作でaccountsデータ構造に格納された // アクティブな要素の数が格納される dcl-ds pgmStat psds; numElements int(20) pos(372); end-ds; dcl-s i int(10); (K) data-into accounts %data('Sample2.csv' : 'doc=file case=any ccsid=job ') %parser('*LIBL/PARSECSV2'); (L) for i = 1 to numElements; dsply ( 'Account: ' + accounts(i).account + ' City: ' + accounts(i).city); endfor;
DATA-INTOは、期待した値がないような状況を処理する機能も含め、兄のXML-INTOと多くの機能を共有しています。XML-INTOの場合と同様に、これに対処し得る方法の1つは、allowmissing = yesオプションを指定することです。しかし、XML-INTOについて書いた際に指摘したように、これは非常にまずいやり方です。なぜなら、オプションであると考えられるフィールドだけでなく、欠落しているフィールドもすべて無視されてしまうからです。
たとえば、テストシナリオでは米国以外の顧客のレコードがあるとします。そのような場合、州の値は期待されませんが、それ以外のデータは存在することが望まれます。より良いオプションは、このデータ構造accountsの修正版およびDATA-INTO操作で示されているように、countPrefixを使用することです。
dcl-ds accounts Qualified Dim(200) Inz; account char(4); name char(30); city char(30); state char(2); (L) count_state int(5); end-ds; .... (M) data-into accounts %data('Sample3.csv' : 'doc=file case=any ccsid=job - countPrefix=count_' ) %parser('*LIBL/PARSECSV2'); (N) for i = 1 to numElements; if accounts(i).count_state = 0; // 州の情報なし dsply ( 'No state info for Account ' + accounts(i).account ); ....
(L)で、count_stateという名前のフィールドをデータ構造に追加し、(M)でcount_というカウント・プレフィックスを指定しています。その結果、DATA-INTOはstateフィールドに値が格納されている場合にはcount_stateに1を、そうでない場合にはcout_stateに0を格納することで、stateフィールドに値が格納されたかどうかを追跡します。この条件は(N)で検査され、州の情報が省かれていることを通知するメッセージが発行されます。
このロジックのために、CSVファイルには次のような行があります。
5678,”Canadian Customer”,Mississauga
このデータではフィールドの不足によるエラーはもはや発生しません。XML-INTOの場合と同様に、DATA-INTOは“欠落”エラーの状況を通知する方法を与えていない場合にだけエラーを発生させるとするのが普通の考え方です。ですから、州の列が省略された行は問題ありませんが、行を次のように変更した場合、
5678,”Canadian Customer”
市の情報も省略され、データ構造にcount_cityフィールドが存在しないため、このデータではやはりエラーが発生します。
%DATAオプションに関するもう1つの論点があります。列見出しを使用してフィールド名を提供しているので、“Account Number”のような列名が使用された場合に何が起こるのか疑問に思うかもしれません。空白文字はRPGフィールド名では有効ではないため、DATA-INTOのデフォルトの動作はこの名前を不一致として扱います。ただし、これに対処するためにcase = convertオプションが使用できます。このオプションを使用すると、遭遇した空白文字はすべてアンダースコアになります。ですから、“Account Number”は、項目名Account_Numberと一致します。
デバッグ支援
パーサーの開発は、コツを掴むまでは細心の注意を要するものであり、Open Accessとは違ってそれぞれのパーサーが異なるため、Open Accessで使用されるプログラムテンプレートのアプローチは役に立ちません。
特に、パーサーが “失敗”に終わった場合(もちろん我々のコードではそのようなことはありませんでした!)、まさにDATA-INTOランタイムが見たことを理解するのは本当に難しいことです。そのため、IBMは非常に優れたデバッグ・オプションを提供しました。このオプションを有効にすると、何が起こったのかについて多くの情報が得られます。
トレースを使用可能にするには、環境変数QIBM_RPG_DATA_INTO_TRACEの値を*STDOUTに設定しなければなりません。これはWRKENVVARコマンドを使用して行うことができます。これを実行したら、コードパッケージに含まれている単純なプログラムDSPSTDOUTを実行して、トレース・データを端末に表示することができます。このプログラムは初期のIBMの文書に基づいており、コンパイラの一部として出荷するのか、それとも文書に記載するのかは定かではありません。
トレース・データは、DATA-INTOがエラーに遭遇して失敗に終わると自動的に表示されるので、大抵の場合これを意図的に行う必要はありません。将来的にIBMは、トレース・データをファイルに書き込むなどの他のオプションを提供するつもりです。しかし、私たちは現在のオプションがデバッグに十分役立つと気付きました。
提供される情報の種類をお見せするために、レコード内のフィールドが多過ぎるファイルを処理する際に生成されるトレースから抽出されたものを以下に示します。
.... ReportValue: 'Canadian Customer' ReportName: 'City' ReportValue: 'Mississauga' EndStruct StartStruct ReportName: 'Account' ReportValue: '6789' ReportName: 'Name' ReportValue: 'Too much data' ReportName: 'City' ReportValue: 'Anywhere' ReportName: 'State' ReportValue: 'AL' ReportError, RC = 2 - Bytes parsed: 231 Terminating due to error
この情報は、DATA-INTOが見る階層レベル(すなわち、構造体の入れ子)を示すために字下げされていることに注意してください。パーサーが構造体の開始と終了、名前と値の報告などのために行ったありとあらゆる呼び出しの結果を見ることもできます。また、エラーが検出されたファイル内の位置と共に、エラーコード2(フィールド過多)が表示されるのを見ることもできます。
それは便利なものです。しかし、ちょっと待ってください、他にももっとあります!QrnDiTrace()プロシージャーを呼び出し、メッセージとネストレベル情報を指定することで、トレースに独自のデバッグ情報を追加できます。私たちはこの機能が本当に気に入っています。
まとめ
DATA-INTO機能は7.2と7.3向けに、じきにリリースされる予定です。リリースされたら、パッケージに付属しているサンプルのレビューを公開し、目玉となる機能のいくつかを紹介します。
私達と同様に、皆さんもDATA-INTOで楽しく遊ぶことを願っています。私たちは、サードパーティーやオープンソースグループがこれで何をすることができるのかを楽しみにしています。おそらくその点がより重要なことでしょう。この新しいサポートに関するご質問やご意見がありましたら、私たちにお知らせください。