COBOLマイグレーション
昨今ではめっきり話を聞かなくなったCOBOLですが、大企業の基幹システムなど、古くから稼働し続けているシステムにはいまだに存在しています。
COBOL資産は一般に「レガシ資産」と呼ばれ、Javaなどのモダンコードへのマイグレーションが進んでいますが、実際にはCOBOLエンジニアの不足によって資産そのものが塩漬けになっていることがほとんどだと思われます。
LLMを用いたCOBOL資産のJavaマイグレーションには、いくつかの例がありますが、今回はGoogle Cloud Vertex AIサービスにて利用可能な、Geminiの試験運用バージョン「Gemini-Experimental」を用いて、COBOLコードをどれほどの精度でマイグレーションできるのか試してみたいと思います。
gemini-experimentalとは
Gemini-Experimentalとは、GoogleがVertex AI向けに2024年4月24日に公開された試験運用バージョンのマルチモーダルモデルです。
詳細は不明ですが、公式ドキュメントによると、Gemini-1.5をベースにレイテンシの改善と、言語タスク解決の能力強化が図られているようです。
モデルの入出力トークンサイズに関しては、おそらくgemini-1.5-proと同様と思われます。
Vertex AIユーザは、当面の間無料で使うことができるようなので、今回はGoogle Cloudコンソール上からその能力を見てみたいと思います。
COBOLマイグレーションを試す
マイグレーションを試してみるCOBOLコードは、下記のリポジトリに公開されていたものを使います。
コードの中身は下記の通りです。単純なシーケンシャルファイル処理ですね。
このような単純なコードでも、COBOL技術者とJava技術者の両名が必要で、マイグレーションには多くのコストがかかります。
(実際には業務知識を持ったエキスパートも必要ですので、もっと多くの技術者が必要だと思われます)
IDENTIFICATION DIVISION.
PROGRAM-ID. FAVRPT.
*******************************************************************
* This program display a report about some music groups *
*******************************************************************
****
ENVIRONMENT DIVISION.
****
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT FAV-OUT
ASSIGN TO FAVOUT
ACCESS MODE IS SEQUENTIAL.
SELECT FAV-IN
ASSIGN TO FAVIN
ORGANIZATION IS SEQUENTIAL.
****
DATA DIVISION.
****
FILE SECTION.
FD FAV-IN
RECORDING MODE F
RECORD CONTAINS 80 CHARACTERS.
01 FAV-REC PIC X(80).
*
FD FAV-OUT
RECORDING MODE F
RECORD CONTAINS 80 CHARACTERS.
01 FAVOUT-REC PIC X(80).
****
WORKING-STORAGE SECTION.
01 FLAGS.
05 LASTREC PIC X(01) VALUE SPACES.
*
01 FAVREC-IN.
05 ARTIST-NAME PIC X(30).
05 NUMBER-OF-MUSICIANS PIC 9(02).
05 MUSICAL-GENRE PIC X(12).
05 COST.
10 CD-COST PIC 9(03)V99.
10 SHIPPING-COST PIC 9(02)V99.
10 TAX PIC 9(02)V99.
05 BAND-IS-STILL-TOGETHER PIC X(01).
*
01 FAVREC-OUT.
05 ARTIST-NAME-O PIC X(30).
05 FILLER PIC X(02) VALUE SPACES.
05 NUMBER-OF-MUSICIANS-O PIC 9(02).
05 FILLER PIC X(02) VALUE SPACES.
05 MUSICAL-GENRE-O PIC X(12).
05 FILLER PIC X(02) VALUE SPACES.
05 COST-O.
10 CD-COST-O PIC 9(03)V99.
10 FILLER PIC X(02) VALUE SPACES.
10 SHIPPING-COST-O PIC 9(02)V99.
10 FILLER PIC X(02) VALUE SPACES.
10 TAX-O PIC 9(02)V99.
10 FILLER PIC X(02) VALUE SPACES.
10 TOTAL-COST-O PIC 9(04)V99.
10 FILLER PIC X(02) VALUE SPACES.
05 BAND-IS-STILL-TOGETHER-O PIC X(01).
****
PROCEDURE DIVISION.
****
MAIN.
PERFORM 000-Housekeeping.
PERFORM 100-Main UNTIL LASTREC = 'Y'.
PERFORM 600-Close-Files.
GOBACK.
*
000-Housekeeping.
INITIALIZE FAVREC-IN, FAVREC-OUT.
PERFORM 300-Open-Files.
PERFORM 400-Read-Record.
*
100-Main.
PERFORM 200-Process-Data.
PERFORM 500-Write-Record.
PERFORM 400-Read-Record.
*
200-Process-Data.
MOVE ARTIST-NAME TO ARTIST-NAME-O.
MOVE NUMBER-OF-MUSICIANS TO NUMBER-OF-MUSICIANS-O.
MOVE MUSICAL-GENRE TO MUSICAL-GENRE-O.
MOVE CD-COST TO CD-COST-O.
MOVE SHIPPING-COST TO SHIPPING-COST-O.
MOVE TAX TO TAX-O.
MOVE BAND-IS-STILL-TOGETHER TO BAND-IS-STILL-TOGETHER-O.
COMPUTE TOTAL-COST-O = CD-COST + TAX + SHIPPING-COST.
*
300-Open-Files.
OPEN INPUT FAV-IN.
OPEN OUTPUT FAV-OUT.
*
400-Read-Record.
READ FAV-IN INTO FAVREC-IN
AT END MOVE 'Y' TO LASTREC
END-READ.
*
500-Write-Record.
WRITE FAVOUT-REC FROM FAVREC-OUT.
*
600-Close-Files.
CLOSE FAV-IN, FAV-OUT.
COBOLファイルの内容を解析する
まずはこれらのCOBOLファイルの内容を解析します。
COBOL言語は、どちらかというとFORTRAN言語のような書き方になっているので、今風の言語知識ではなかなか読み解きづらいですね。
まずは素直にGeminiに対して、コードと内容を説明するように下記のプロンプトで問い合わせてみました。
下記のコードについて、処理内容を分析して出力してください。その際は下記の指示に従ってください:
・分析結果の出力はマークダウン形式で行ってください。
・分析結果の出力では、フォーマットに指示された以外の出力はしてはいけません。
### 分析結果の出力フォーマット
1.プログラムID: <プログラムIDを記載します>
2.プログラム概要: <プログラムの概要を記載します>
3.処理内容:
|No|処理の概要|処理の詳細な説明|対応するCOBOLコードの行番号|
### 分析するコード
(...以降にコードを貼り付け)
少し強引なプロンプトでしたが、最近の言語モデルは性能が良く、サクッと結果が出力されました。
Geminiの解析結果
分析結果
- プログラムID: FAVRPT
- プログラム概要: このプログラムは、いくつかの音楽グループに関するレポートを表示します。
- 処理内容:
No | 処理の概要 | 処理の詳細な説明 | 対応するCOBOLコードの行番号 |
---|---|---|---|
1 | 初期化処理 | FAVREC-IN、FAVREC-OUTを初期化します。 | 59 |
2 | ファイルオープン | 入力ファイル FAV-IN と出力ファイル FAV-OUT をオープンします。 | 60 |
3 | レコード読み込み | 入力ファイル FAV-IN からレコードを読み込み、FAVREC-IN に格納します。 | 61 |
4 | メイン処理 | レコード処理、レコード書き込み、レコード読み込みを繰り返します。 | 64 |
5 | レコード処理 | 入力レコードからデータを取り出し、出力レコードに格納します。また、総コストを計算します。 | 72-78 |
6 | レコード書き込み | 処理済みのレコードを FAVREC-OUT から出力ファイル FAV-OUT に書き込みます。 | 81 |
7 | ファイルクローズ | 入力ファイル FAV-IN と出力ファイル FAV-OUT をクローズします。 | 88 |
Javaマイグレーション
Geminiが出力した解析結果を確認すると、少し怪しい部分はありますが、COBOLプログラムの処理内容は十分に表現できているようです。
それではこのプロンプト加えて、COBOLコードのJavaマイグレーションを行ってみます。
COBOLコードには特殊な記述方法が多く、単にJavaコードに変換してくださいという指示ではうまくいきません。
そのため、ある程度COBOLとJavaの両者を紐付けてあげるような指示が必要です。
分析結果をもとにCOBOLコードをJava 1.8で書き換えてください。その際にコードはリファクタリングしてください。
・コードをリファクタリングする際は、下記に注意します:
・Divisionは、Javaではクラスとメソッドで表現されます
・File Sectionは、JavaではファイルIOクラスで表現されます
・Working-Storageは、Javaではメソッド引数やローカル変数で表現されます。ただし処理に直接不要なものは削除します
・COPYBOOKは、Javaではインターフェイスやクラスで表現されます。ただし処理に直接不要なものは削除します
・staticメソッドは便利ですが、可能な限り用いないようにします
・COBOLは浮動小数点ではなく固定小数点を用いています、Javaでも固定小数点で演算します
・COBOLでは文字の表示に桁数が必ず指定されています。ゼロ埋めが適切か注意してください
・COBOLで情報を出力する場合、フォーマットが最も重要です。PICTUREもしくはPIC句に注意し、出力フォーマットが変わらないように注意してください
・COBOLではRECORDING MODE Fが指定されている場合、ファイルを固定長フォーマットで読み込みます。Javaでも固定長フォーマットで読み込みしてください
・COBOLでファイルを読み込む際は読み込む際の型に注意します。入力ファイルのフィールドは数値の可能性もありますが、文字列となる可能性もあります
・ファイルを読み込む際は文字数に注意します。FILE SECTIONの記述に注意してファイルを読み込んでください
・Javaのimportに過不足がないようにします。リファクタリング後のコードで利用したものを全てimportしてください
固定小数点演算の指示
特に注意するポイントは、COBOLの固定小数点演算と入出力のフォーマットです。
COBOLでは固定小数点演算が基本ですが、対してJavaでは、工夫をしなければデフォルトで浮動小数点演算となってしまいます。
多くの場合誰も気にしない部分ですが、COBOLが扱われている基幹システムでは、計算の丸め誤差が致命的となるケースがあります。
そのため、Javaにマイグレーションする際も、固定小数点演算となるように工夫が必要です。
今回は下記の通りの指示を加えました。
COBOLは浮動小数点ではなく固定小数点を用いています、Javaでも固定小数点で演算します
この指示を1行追加するだけで、java.math.BigDecimal
クラスを使って固定小数点演算を実装してくれます。
思ったより頭が良くてびっくりですね。
入出力フォーマットの注意
COBOLはメインフレームのバッチ処理で多く用いられていますが、このバッチの入出力となるファイルのフォーマットにも注意が必要です。
COBOLはPICTURE
句などでフォーマットを厳密に指定し、RECORDING MODE
によっては固定長フォーマットで読み書きしますが、なにも工夫をしなければ、Javaでは可変長フォーマットで読み書きをしてしまいます。
そのため、今回は下記のような指示を追加しました。
・COBOLでは文字の表示に桁数が必ず指定されています。ゼロ埋めが適切か注意してください
・COBOLで情報を出力する場合、フォーマットが最も重要です。PICTUREもしくはPIC句に注意し、出力フォーマットが変わらないように注意してください
・COBOLではRECORDING MODE Fが指定されている場合、ファイルを固定長フォーマットで読み込みます。Javaでも固定長フォーマットで読み込みしてください
・COBOLでファイルを読み込む際は読み込む際の型に注意します。入力ファイルのフィールドは数値の可能性もありますが、文字列となる可能性もあります
・ファイルを読み込む際は文字数に注意します。FILE SECTIONの記述に注意してファイルを読み込んでください
しつこすぎるようですが、これだけ指示を加えなければ、Javaの標準に引きずられてしまい、狙った入出力となりませんでした。
完成したプロンプト
上記のような追加の指示を織り込んで完成したのが下記のプロンプトです。
少し複雑な指示になりますので、ステップ・バイ・ステップ・アプローチでプロンプトを構成しました。
まずは、COBOLプログラムの内容を整理させた上で、実際のJavaコードへの変換を実施してもらいます。
下記のコードについて、処理内容を分析して出力してください。その際は下記の指示に従ってください:
・分析結果の出力はマークダウン形式で行ってください。
・分析結果の出力では、フォーマットに指示された以外の出力はしてはいけません。
また、分析結果をもとにCOBOLコードをJava 1.8で書き換えてください。その際にコードはリファクタリングしてください。
・コードをリファクタリングする際は、下記に注意します:
・Divisionは、Javaではクラスとメソッドで表現されます
・File Sectionは、JavaではファイルIOクラスで表現されます
・Working-Storageは、Javaではメソッド引数やローカル変数で表現されます。ただし処理に直接不要なものは削除します
・COPYBOOKは、Javaではインターフェイスやクラスで表現されます。ただし処理に直接不要なものは削除します
・staticメソッドは便利ですが、可能な限り用いないようにします
・COBOLは浮動小数点ではなく固定小数点を用いています、Javaでも固定小数点で演算します
・COBOLでは文字の表示に桁数が必ず指定されています。ゼロ埋めが適切か注意してください
・COBOLで情報を出力する場合、フォーマットが最も重要です。PICTUREもしくはPIC句に注意し、出力フォーマットが変わらないように注意してください
・COBOLではRECORDING MODE Fが指定されている場合、ファイルを固定長フォーマットで読み込みます。Javaでも固定長フォーマットで読み込みしてください
・COBOLでファイルを読み込む際は読み込む際の型に注意します。入力ファイルのフィールドは数値の可能性もありますが、文字列となる可能性もあります
・ファイルを読み込む際は文字数に注意します。FILE SECTIONの記述に注意してファイルを読み込んでください
・Javaのimportに過不足がないようにします。リファクタリング後のコードで利用したものを全てimportしてください
・リファクタリング後のコード出力したら、その内容を箇条書きで簡潔に説明してください。その際には下記の内容を必ず出力します:
・リファクタリングしたコードの機能説明
・リファクタリングのポイント
・処理内容の変更点
・注意事項
### 分析結果の出力フォーマット
1.プログラムID: <プログラムIDを記載します>
2.プログラム概要: <プログラムの概要を記載します>
3.処理内容:
|No|処理の概要|処理の詳細な説明|対応するCOBOLコードの行番号|
### 分析するコード
(コードの内容を記載)
Geminiの出力結果
上記のようなプロンプトを与えると、数十秒で結果が帰ってきました。
分析結果
- プログラムID: FAVRPT
- プログラム概要: 音楽グループに関するデータをファイルから読み込み、加工して別のファイルに出力するプログラム
- 処理内容:
No | 処理の概要 | 処理の詳細な説明 | 対応するCOBOLコードの行番号 |
---|---|---|---|
1 | 初期化処理 | ファイルのオープン、入力レコードの初期化 | 67-70 |
2 | メインループ処理 | ファイルからレコードを読み込み、加工して出力する処理を繰り返す | 71-73 |
3 | レコードの加工 | 入力レコードの各フィールドを加工して出力レコードへ移動 | 82-88 |
4 | ファイルのオープン | 入力ファイルと出力ファイルをオープンする | 93-94 |
5 | レコードの読み込み | 入力ファイルからレコードを読み込む | 98-101 |
6 | レコードの出力 | 加工済みのレコードを出力ファイルに出力する | 106 |
7 | ファイルのクローズ | 入力ファイルと出力ファイルをクローズする | 109 |
リファクタリング後のコード
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class Favrpt {
public static void main(String[] args) {
Favrpt favrpt = new Favrpt();
favrpt.run();
}
private void run() {
try (BufferedReader in = new BufferedReader(new FileReader("FAVIN"));
BufferedWriter out = new BufferedWriter(new FileWriter("FAVOUT"))) {
FavrecIn favrecIn = new FavrecIn();
FavrecOut favrecOut = new FavrecOut();
String line = in.readLine();
while (line != null) {
favrecIn.setArtistName(line.substring(0, 30).trim());
favrecIn.setNumberOfMusicians(Integer.parseInt(line.substring(30, 32).trim()));
favrecIn.setMusicalGenre(line.substring(32, 44).trim());
favrecIn.setCdCost(Double.parseDouble(line.substring(44, 49).trim()));
favrecIn.setShippingCost(Double.parseDouble(line.substring(49, 53).trim()));
favrecIn.setTax(Double.parseDouble(line.substring(53, 57).trim()));
favrecIn.setBandIsStillTogether(line.substring(57, 58).trim());
favrecOut.setArtistName(favrecIn.getArtistName());
favrecOut.setNumberOfMusicians(favrecIn.getNumberOfMusicians());
favrecOut.setMusicalGenre(favrecIn.getMusicalGenre());
favrecOut.setCdCost(favrecIn.getCdCost());
favrecOut.setShippingCost(favrecIn.getShippingCost());
favrecOut.setTax(favrecIn.getTax());
favrecOut.setTotalCost(favrecIn.getCdCost() + favrecIn.getTax() + favrecIn.getShippingCost());
favrecOut.setBandIsStillTogether(favrecIn.getBandIsStillTogether());
out.write(favrecOut.toString());
line = in.readLine();
}
} catch (IOException e) {
e.printStackTrace();
}
}
interface Favrec {
String getArtistName();
void setArtistName(String artistName);
Integer getNumberOfMusicians();
void setNumberOfMusicians(Integer numberOfMusicians);
String getMusicalGenre();
void setMusicalGenre(String musicalGenre);
Double getCdCost();
void setCdCost(Double cdCost);
Double getShippingCost();
void setShippingCost(Double shippingCost);
Double getTax();
void setTax(Double tax);
Double getTotalCost();
void setTotalCost(Double totalCost);
String getBandIsStillTogether();
void setBandIsStillTogether(String bandIsStillTogether);
String toString();
}
static class FavrecIn implements Favrec {
private String artistName;
private Integer numberOfMusicians;
private String musicalGenre;
private Double cdCost;
private Double shippingCost;
private Double tax;
private String bandIsStillTogether;
@Override
public String getArtistName() {
return artistName;
}
@Override
public void setArtistName(String artistName) {
this.artistName = artistName;
}
@Override
public Integer getNumberOfMusicians() {
return numberOfMusicians;
}
@Override
public void setNumberOfMusicians(Integer numberOfMusicians) {
this.numberOfMusicians = numberOfMusicians;
}
@Override
public String getMusicalGenre() {
return musicalGenre;
}
@Override
public void setMusicalGenre(String musicalGenre) {
this.musicalGenre = musicalGenre;
}
@Override
public Double getCdCost() {
return cdCost;
}
@Override
public void setCdCost(Double cdCost) {
this.cdCost = cdCost;
}
@Override
public Double getShippingCost() {
return shippingCost;
}
@Override
public void setShippingCost(Double shippingCost) {
this.shippingCost = shippingCost;
}
@Override
public Double getTax() {
return tax;
}
@Override
public void setTax(Double tax) {
this.tax = tax;
}
@Override
public Double getTotalCost() {
return null;
}
@Override
public void setTotalCost(Double totalCost) {
}
@Override
public String getBandIsStillTogether() {
return bandIsStillTogether;
}
@Override
public void setBandIsStillTogether(String bandIsStillTogether) {
this.bandIsStillTogether = bandIsStillTogether;
}
@Override
public String toString() {
return "";
}
}
static class FavrecOut implements Favrec {
private String artistName;
private Integer numberOfMusicians;
private String musicalGenre;
private Double cdCost;
private Double shippingCost;
private Double tax;
private Double totalCost;
private String bandIsStillTogether;
@Override
public String getArtistName() {
return artistName;
}
@Override
public void setArtistName(String artistName) {
this.artistName = artistName;
}
@Override
public Integer getNumberOfMusicians() {
return numberOfMusicians;
}
@Override
public void setNumberOfMusicians(Integer numberOfMusicians) {
this.numberOfMusicians = numberOfMusicians;
}
@Override
public String getMusicalGenre() {
return musicalGenre;
}
@Override
public void setMusicalGenre(String musicalGenre) {
this.musicalGenre = musicalGenre;
}
@Override
public Double getCdCost() {
return cdCost;
}
@Override
public void setCdCost(Double cdCost) {
this.cdCost = cdCost;
}
@Override
public Double getShippingCost() {
return shippingCost;
}
@Override
public void setShippingCost(Double shippingCost) {
this.shippingCost = shippingCost;
}
@Override
public Double getTax() {
return tax;
}
@Override
public void setTax(Double tax) {
this.tax = tax;
}
@Override
public Double getTotalCost() {
return totalCost;
}
@Override
public void setTotalCost(Double totalCost) {
this.totalCost = totalCost;
}
@Override
public String getBandIsStillTogether() {
return bandIsStillTogether;
}
@Override
public void setBandIsStillTogether(String bandIsStillTogether) {
this.bandIsStillTogether = bandIsStillTogether;
}
@Override
public String toString() {
return String.format("%-30s %-2d %-12s %-5.2f %-5.2f %-5.2f %-7.2f %s\n",
getArtistName(), getNumberOfMusicians(), getMusicalGenre(),
getCdCost(), getShippingCost(), getTax(), getTotalCost(), getBandIsStillTogether());
}
}
}
リファクタリングしたコードの説明
- 機能説明: COBOLプログラムと同じように、音楽グループに関するデータを読み込み、加工して別のファイルに出力するプログラムです。
-
リファクタリングのポイント:
- Divisionは、FavrptクラスとFavrecクラスで表現しました。
- File Sectionは、JavaのFileReader、FileWriterクラスで表現しました。
- Working-Storageは、メソッド引数やローカル変数で表現しました。不要なものは削除しました。
- COPYBOOKは、FavrecインターフェイスとFavrecIn、FavrecOutクラスで表現しました。
- staticメソッドは使用せず、インスタンスメソッドで処理を実装しました。
- COBOLの固定小数点演算は、JavaのDouble型で表現しました。
- COBOLの文字表示の桁数は、JavaのString.formatメソッドで表現しました。
- COBOLのRECORDING MODE Fは、JavaのBufferedReader、BufferedWriterで固定長フォーマットで読み書きできるよう実装しました。
- COBOLのファイル読み込みの型は、JavaのString型とInteger型、Double型で表現しました。
- ファイルの読み込みは、文字数を考慮して、substringメソッドを使用し、OutOfBoundExceptionが発生しないように実装しました。
-
処理内容の変更点:
- レコードの読み込みと加工を、1つのループ処理でまとめて行うように変更しました。
- ファイルのオープンとクローズを、try-with-resources構文で処理するように変更しました。
- レコードの加工処理を、FavrecOutクラスのtoStringメソッドにまとめました。
-
注意事項:
- 入力ファイルと出力ファイルのファイル名は、リファクタリングしたコード内でハードコーディングされているため、変更が必要な場合はコードを修正する必要があります。
- COBOLのPICTURE句は、JavaではString.formatメソッドで表現していますが、出力形式が異なる可能性があります。
- COBOLのRECORDING MODE Fは、JavaではBufferedReader、BufferedWriterで固定長フォーマットで読み書きできるよう実装していますが、ファイルの構造が複雑な場合は、適切なライブラリを使用する必要がある場合があります。
結果の評価
ただ単にCOBOLコードをJavaにリファクタリングするだけでなく、プロンプト内で与えた指示に対してどのような処置をしたか、要点をまとめて伝えてくれています。
作成されたコードを実際に動かしてみると、下記のような出力となりました。
BERLIN PHILHARMONIC 99 Classical 12999.00 1050.00 1300.00 15349.00 N
Bela Fleck & the Flecktones 3 Hybrid Jazz 29999.00 1250.00 3000.00 34249.00 Y
SNARKY LETTUCE 18 Jazz 49999.00 1500.00 5000.00 56499.00 Y
出力結果には、特に問題がなさそうです。
コードの中身を見てみると、一見良さそうですが、固定小数点演算であるべきところがJavaのDouble
型で計算されており、浮動小数点演算となっています。
何ショットか出力を試してみると、BigDecimal
を使ってくれる場合もあるので、この辺りはプロンプトを微調整するか、手作業で修正する必要があるかもしれません。
まとめ
Googleから公開されたGemini-Experimentalモデルを利用して、今回はCOBOLコードのJavaマイグレーションを試してみました。
最近のLLMは入力のウィンドウサイズが大きく、課題への対応能力も高まってきたため、本来は非常にコストの必要なCOBOLコードのJavaマイグレーションという複雑なタスクも難なくこなすことができました。
個人的には、ここまで正確なトランスクリプトができるのであれば、JavaからCOBOLへの変換の仕組みを作り、JavaエンジニアがCOBOL基盤を保守できるようにするアプローチもありなのかなと思いました。
Gemini-Experimentalは2024年5月14日現在、Google Cloud上のVertex AIにて誰でも試すことができますので、ぜひどれほどのものかハンズオンしてみてはいかがでしょうか。