Azure OpenAI GPT-4 Public Preview Waitlist の利用申請が通りましたので試してみました。
今回は、下記に説明するように 600 行の COBOL コードを Java のコードに移行しました。(以降にかかった時間は、数時間〜約半日)
この検証に、GPT-4-32k のモデルを使用しました。32k を利用すると、大量のトークンを扱えるようになるので、このような 600 行程度のコード解析であれば余裕でやってくれます。
※ ちなみに私は COBOL は全く知りませんが、Azure OpenAI GPT-4-32k の力を借りて なんとか Java のコードには変換できました
まず、最初に検証から学んだことを記述し、その後実際の検証内容について紹介していきます。
今回の検証から学んだこと
GPT-4 は GPT-3-Turbo に比べて、格段にクオリティが良くなったと言われます。おそらく、皆様の周りでもよく聞く話かと思います。私も実際にそう思いますし、問い合わせた内容に対する回答の質が、大幅に向上していると思います。さらに 32k を使うと、通常の 8192 トークンしか扱えない環境に比べて、さらにできる事の幅が広がると感じました。
今回の結果から、私が実際に GPT-4-32k を触って感じたことを下記にまとめます。
まだ、決して多くの方が 32k を使える状況にないと思いますが、32k をお試しいただけるようになった際、どうぞ色々とお試しください。
学んだポイント
- chatGPT-4-32k は今までにない、大量のデータを扱える、できる事の幅が大きく広がる
- 大量のデータを扱えるため、大量のソースコード解析、コードマイグレーションも可能
- COBOL から Java、そして Struts 1.x から Spring/Quarkus の移行も期待
- 1200 行を超えるソースコードあたりから、トークン数の上限を意識する必要あり、やり方を考える必要あり
- Azure OpenAI で扱うデータはお客様の物で、学習にも、トレーニングにも使われないため、企業内の機密データやコードでも安心して使える (企業環境で使えます)
- Azure OpenAI の利用は閉じたネットワーク環境で利用可能なため、安全に接続して利用が可能
- GPT-4 になり回答の質が向上しているので、身近に力づよい先輩ができる
- 正しい回答を得るためには、正しい知識、そしてプロンプトの入力が必要
- 全く知識がない場合は、正しく問い合わせができないし、出てきた回答が正しいかどうか判断つかない
- 回答を、いつも 100% 信じてはいけない、最終的に判断するのは人間
- ソースコードの提案も、問い合わせの度に違う提案(ソースコード)をしてくる
- 回答者のペルソナを定義し、期待する回答を明確に定義する
- どのようなペルソナ(人格?)が回答するのかを定義する。(例: 私は Java に詳しいコンサルです。)
- 場合によっては、答えてはならない質問も定義する。 (例: Java 関連以外のことにはお答えできません。)
- 回答は生テキストだけではなく、JSON など違うフォーマットも返せる。(例:結果を JSON で返してください。)
- Java で例外が出力された場合も、例外の内容を含めて、どこがおかしいか聞くと教えてくれる
- これからは、エラーログを chatGPT に送って!!という方が良い時代なのかも?
- 元のソースコードを事前に送っている場合(履歴(配列)に含まれる場合)、元のソースコードを直してくれる時もある
- GPT-4 で生成したコードでエラーが出たよと報告すると、「ごめんなさい」と謝ってきて修正コードを提案してくる場合もある
- chatGPT の実装はバックエンド側で実装すべき
- フロント、例えば JavaScript で RESTで実装すると、自身の API キーが盗まれて悪用される可能性がある
- chatGPT-4, chatGPT-4-32k は、決して安くない、トークン辺りの単価も少し割高
- コストを考えるならば、ユースケースに応じてモデルを変更した方が良い場合もある
- ただし、GPT-4 に限らず Azure OpenAI で提供されているモデルは、24 時間 365 日利用可能 (人間と違って疲れを知らない)
- つまらない質問、簡単な質問にも、怒らず、何度でも繰り返し問い合わせ可能 (価格と引き換え)
- 何を聞いても、何度聞いても絶対に怒らない、優しい先輩がいる感じ?
- こんな事聞いて良いの? 聞いたら怒られないかな? なんて心配はいらなくなります
- 今回のような事を COBOL に精通したエンジニアに問い合わせをすると、1人月幾らかかるか見積もり、利用・有用性を考えてみる
-
24H/365 は聞けない
-
スケジュール調整不要
-
圧倒的な回答スピード、即座に聞いて、即座に回答(数秒)
-
1人月 80〜300万円 vs 利用トークン数 * 単価
-
全く COBOL に対して無知な人間でも、動くのを作るのに 1 日もかかっていない
- 細かなところに関しては、COBOL に熟知した人も必要と考えるが、人間に聞かないといけない部分が大幅に減る
-
- 32k を扱う際の注意点 (価格以外):
大量のデータを保持したまま、会話を続けると、すべての会話履歴を配列に保持したまま繰り返し通信を行うため、HTTP クライアント(RESTCient やブラウザ等)のメモリ使用量はリクエスト事に増加していく。
また、ネットワークで送受信するデータ量も増えるため、適度なタイミングで過去の履歴を削除した方が良い場合もある。(大規模データを繰り返し扱う場合は特に注意が必要) - Auzre OpenAI GPT-4 Public Preview のお試し申請は
COBOL から Java へのマイグレーション !!
それでは、実際に GPT-4-32k を利用してコードのマイグレーションを行った手順について紹介します。
今回利用した COBOL のコード:
* https://github.com/tanto259/cobol-course/blob/main/Module%2016/HOSPEDIT.cbl
1. 私からの問い合わせ
上記の COBOL のコードを詳しく説明してください。
1. Azure OpenAI chatGPT-4-32k からの返信
このCOBOLプログラムは、病院の患者データを処理し、
患者の情報と保険タイプに基づいて統計情報を生成することを目的としています。
プログラムは、患者データを読み取り、検証し、統計を計算し、レポートに書き込む
ことでこれを実現しています。以下は、プログラムの主要な部分の詳細です。
環境部(Environment Division):プログラムで使用されるファイルとデバイスの
定義が含まれています。このプログラムでは、入力ファイル(INFILE)、出力ファイル
(OUTFILE)、エラーファイル(ERRFILE)、レポートファイル(RPTFILE)、
および保険タイプファイル(INSTYPE)が定義されています。
データ部(Data Division):プログラムで使用されるデータ構造と変数の定義が
含まれています。これには、ファイルのレコード構造、作業記憶域の変数、および
患者データのコピー部分が含まれます。
手続き部(Procedure Division):プログラムのロジックが含まれています。
このプログラムでは、以下の主要な段落があります。
a. 000-HOUSEKEEPING:ファイルを開き、作業記憶域のレコードを初期化し、
最初のデータを読み取り、保険タイプファイルから内部テーブルにデータを移動します。
b. 100-MAINLINE:患者データの検証、統計の計算、およびレポートへのレコードの
書き込みを行う段落を実行します。これは、すべてのレコードが処理されるまで繰り返されます。
c. 200-CLEANUP:ファイルを閉じ、フッターを印刷します。
プログラムは、患者のタイプと保険タイプに基づいて、患者の総額と純額を計算し、
レポートに出力します。また、患者のタイプと保険タイプごとのカウンターも更新されます。
最後に、プログラムは、読み取り、書き込み、エラー、および各タイプの患者数などの
統計情報をレポートに出力します。
2. 私からの問い合わせ
上記の COBOL のコードを Java 17 の環境で動作するように Stream API や
NIO.2 など最新のJava言語仕様を利用して書き直してください。
2. Azure OpenAI chatGPT-4-32k からの返信
以下に、Java 17を使用してCOBOLプログラムを書き直したコードを示します。このコードは、Stream APIやNIO.2を使用してファイルの読み書きを行い、患者データの処理と統計情報の生成を行います。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class HospEdit {
private static final String INPUT_FILE = "HOSPIN.txt";
private static final String OUTPUT_FILE = "HOSPOUT.txt";
private static final String ERROR_FILE = "ERRFILE.txt";
private static final String REPORT_FILE = "RPTFILE.txt";
private static final String INS_TYPE_FILE = "INSTYPE.txt";
public static void main(String[] args) {
try {
List<String> inputLines = Files.readAllLines(Path.of(INPUT_FILE));
List<String> insTypeLines = Files.readAllLines(Path.of(INS_TYPE_FILE));
Map<String, String> insTypeMap = insTypeLines.stream()
.collect(Collectors.toMap(line -> line.substring(0, 3), line -> line.substring(3).trim()));
List<String> outputLines = new ArrayList<>();
List<String> errorLines = new ArrayList<>();
List<String> reportLines = new ArrayList<>();
// Print headers
reportLines.add("PATNO PATIENT PHONE-NO TYPE BED ADMIT DAILY AMT DIAG INS STAY NETWORK COPAY DEDUCT");
reportLines.add("===== ===================== ======== ==== ==== ===== ========= ==== ==== ==== ======= ===== ======");
// Process input lines
for (String inputLine : inputLines) {
PatientRecord patientRecord = new PatientRecord(inputLine);
// Validate patient type and insurance type
boolean validPatientType = patientRecord.getPatientType().equals("IN") || patientRecord.getPatientType().equals("OUT");
boolean validInsuranceType = insTypeMap.containsKey(patientRecord.getInsuranceType());
if (!validPatientType || !validInsuranceType) {
outputLines.add(inputLine);
errorLines.add((validPatientType ? "INVALID INS TYPE" : "INVALID PAT TYPE") + " " + inputLine);
} else {
reportLines.add(patientRecord.toReportLine(insTypeMap));
}
}
// Write output files
Files.write(Path.of(OUTPUT_FILE), outputLines);
Files.write(Path.of(ERROR_FILE), errorLines);
Files.write(Path.of(REPORT_FILE), reportLines);
} catch (IOException e) {
System.err.println("Error reading or writing files: " + e.getMessage());
}
}
static class PatientRecord {
private final String inputLine;
private final String patientNumber;
private final String lastName;
private final String firstName;
private final String patientPhone;
private final String patientType;
private final String bedIdentity;
private final LocalDate admitDate;
private final double dailyAmount;
private final String insuranceType;
private final int hospitalStayLength;
private final String network;
private final double copay;
private final double deductible;
public PatientRecord(String inputLine) {
this.inputLine = inputLine;
this.patientNumber = inputLine.substring(0, 5).trim();
this.lastName = inputLine.substring(5, 15).trim();
this.firstName = inputLine.substring(15, 25).trim();
this.patientPhone = inputLine.substring(25, 33).trim();
this.patientType = inputLine.substring(33, 35).trim();
this.bedIdentity = inputLine.substring(35, 39).trim();
this.admitDate = LocalDate.parse(inputLine.substring(39, 45), DateTimeFormatter.ofPattern("yyMMdd"));
this.dailyAmount = Double.parseDouble(inputLine.substring(45, 54).trim());
this.insuranceType = inputLine.substring(54, 57).trim();
this.hospitalStayLength = Integer.parseInt(inputLine.substring(57, 60).trim());
this.network = inputLine.substring(60, 62).trim();
this.copay = Double.parseDouble(inputLine.substring(62, 68).trim());
this.deductible = Double.parseDouble(inputLine.substring(68, 74).trim());
}
public String getPatientType() {
return patientType;
}
public String getInsuranceType() {
return insuranceType;
}
public String toReportLine(Map<String, String> insTypeMap) {
return String.format("%-5s %-10s %-10s (%s) %s-%s %-4s %-4s %-6s $%8.2f %-4s %-4s %-4s %-7s $%5.0f $%5.0f",
patientNumber, lastName, firstName, patientPhone.substring(0, 3), patientPhone.substring(3, 6), patientPhone.substring(6),
patientType, bedIdentity, admitDate.format(DateTimeFormatter.ofPattern("yyMMdd")), dailyAmount,
insuranceType, insTypeMap.get(insuranceType), hospitalStayLength, network, copay, deductible);
}
}
}
このJavaプログラムは、COBOLプログラムと同様に、患者データを読み取り、検証し、統計を計算し、レポートに書き込むことで、患者の情報と保険タイプに基づいて統計情報を生成します。ただし、Javaプログラムは、Stream APIやNIO.2を使用してファイルの読み書きを行うため、COBOLプログラムよりも簡潔でモダンなコードになっています。
2. 上記で出力された Java のコードを確認
上記で出力された内容を確認すると、ファイルを 2 つオープンしていました。
List<String> inputLines = Files.readAllLines(Path.of(INPUT_FILE));
List<String> insTypeLines = Files.readAllLines(Path.of(INS_TYPE_FILE));
なぜ 2 つオープンしているのだろうと、"INS_TYPE_FILE" の記載を確認すると、下記のように型だけが定義されていました。
そこで、これは Enumeration
で置き換えられると判断し、次のリクエストは Enumeration
に変えて実装する提案を行いました。
HMO
PRI
PPO
AFF
MED
3. 私からの問い合わせ
HOSPEDIT-INSTYPE ファイルには HMO, PRI, PPO, AFF, MED が定義されています。
これを Enumeration で定義して、Enumeration を利用して実装するコードに変換してください。
3. Azure OpenAI chatGPT-4-32k からの返信
以下に、Java 17を使用してCOBOLプログラムを書き直したコードを示します。このコードでは、保険タイプをEnumerationで定義し、それを使用して患者データの処理と統計情報の生成を行います。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class HospEdit {
private static final String INPUT_FILE = "HOSPIN.txt";
private static final String OUTPUT_FILE = "HOSPOUT.txt";
private static final String ERROR_FILE = "ERRFILE.txt";
private static final String REPORT_FILE = "RPTFILE.txt";
public static void main(String[] args) {
try {
List<String> inputLines = Files.readAllLines(Path.of(INPUT_FILE));
List<String> outputLines = new ArrayList<>();
List<String> errorLines = new ArrayList<>();
List<String> reportLines = new ArrayList<>();
// Print headers
reportLines.add("PATNO PATIENT PHONE-NO TYPE BED ADMIT DAILY AMT DIAG INS STAY NETWORK COPAY DEDUCT");
reportLines.add("===== ===================== ======== ==== ==== ===== ========= ==== ==== ==== ======= ===== ======");
// Process input lines
for (String inputLine : inputLines) {
PatientRecord patientRecord = new PatientRecord(inputLine);
// Validate patient type and insurance type
boolean validPatientType = patientRecord.getPatientType().equals("IN") || patientRecord.getPatientType().equals("OUT");
boolean validInsuranceType = InsuranceType.isValid(patientRecord.getInsuranceType());
if (!validPatientType || !validInsuranceType) {
outputLines.add(inputLine);
errorLines.add((validPatientType ? "INVALID INS TYPE" : "INVALID PAT TYPE") + " " + inputLine);
} else {
reportLines.add(patientRecord.toReportLine());
}
}
// Write output files
Files.write(Path.of(OUTPUT_FILE), outputLines);
Files.write(Path.of(ERROR_FILE), errorLines);
Files.write(Path.of(REPORT_FILE), reportLines);
} catch (IOException e) {
System.err.println("Error reading or writing files: " + e.getMessage());
}
}
static class PatientRecord {
private final String inputLine;
private final String patientNumber;
private final String lastName;
private final String firstName;
private final String patientPhone;
private final String patientType;
private final String bedIdentity;
private final LocalDate admitDate;
private final double dailyAmount;
private final String insuranceType;
private final int hospitalStayLength;
private final String network;
private final double copay;
private final double deductible;
public PatientRecord(String inputLine) {
this.inputLine = inputLine;
this.patientNumber = inputLine.substring(0, 5).trim();
this.lastName = inputLine.substring(5, 15).trim();
this.firstName = inputLine.substring(15, 25).trim();
this.patientPhone = inputLine.substring(25, 33).trim();
this.patientType = inputLine.substring(33, 35).trim();
this.bedIdentity = inputLine.substring(35, 39).trim();
this.admitDate = LocalDate.parse(inputLine.substring(39, 45), DateTimeFormatter.ofPattern("yyMMdd"));
this.dailyAmount = Double.parseDouble(inputLine.substring(45, 54).trim());
this.insuranceType = inputLine.substring(54, 57).trim();
this.hospitalStayLength = Integer.parseInt(inputLine.substring(57, 60).trim());
this.network = inputLine.substring(60, 62).trim();
this.copay = Double.parseDouble(inputLine.substring(62, 68).trim());
this.deductible = Double.parseDouble(inputLine.substring(68, 74).trim());
}
public String getPatientType() {
return patientType;
}
public String getInsuranceType() {
return insuranceType;
}
public String toReportLine() {
return String.format("%-5s %-10s %-10s (%s) %s-%s %-4s %-4s %-6s $%8.2f %-4s %-4s %-4s %-7s $%5.0f $%5.0f",
patientNumber, lastName, firstName, patientPhone.substring(0, 3), patientPhone.substring(3, 6), patientPhone.substring(6),
patientType, bedIdentity, admitDate.format(DateTimeFormatter.ofPattern("yyMMdd")), dailyAmount,
insuranceType, InsuranceType.valueOf(insuranceType).getDescription(), hospitalStayLength, network, copay, deductible);
}
}
enum InsuranceType {
HMO("HMO", "Health Maintenance Organization"),
PRI("PRI", "Private"),
PPO("PPO", "Preferred Provider Organization"),
AFF("AFF", "Affordable"),
MED("MED", "State/Federal");
private final String code;
private final String description;
InsuranceType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
public static boolean isValid(String code) {
for (InsuranceType type : values()) {
if (type.getCode().equals(code)) {
return true;
}
}
return false;
}
}
}
このJavaプログラムは、COBOLプログラムと同様に、患者データを読み取り、検証し、統計を計算し、レポートに書き込むことで、患者の情報と保険タイプに基づいて統計情報を生成します。ただし、Javaプログラムは、Enumerationを使用して保険タイプを定義し、それを使用して患者データの処理を行うため、COBOLプログラムよりも簡潔でモダンなコードになっています。
上記で出来上がったコードを実行してみます。
すると下記のような例外が出力されました。
> java HospEditOld2.java
Exception in thread "main" java.time.format.DateTimeParseException: Text '112/12' could not be parsed at index 2
at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2052)
at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1954)
at java.base/java.time.LocalDate.parse(LocalDate.java:430)
at HospEditOld2$PatientRecord.<init>(HospEditOld2.java:81)
at HospEditOld2.main(HospEditOld2.java:32)
そこで該当する 81 行目のコードを確認してみます。
すると、下記の日付取得部分で例外出力されていることがわかりました。
this.admitDate = LocalDate.parse(inputLine.substring(39, 45), DateTimeFormatter.ofPattern("yyMMdd"));
そこで、実際の入力データから 39〜45列部分を確認してみます。
11111Smith Joanne 2016521111I000112/12/20050005123010HMO012012345678A1O4H9I0100015
すると、確かに上記の例外でも示されているように、112/12
が該当の箇所で、どうみても日付データにはなりそうにありません。
そこで、生成された Java のソースコードをもう一度確認してみます。
this.patientNumber = inputLine.substring(0, 5).trim();
this.lastName = inputLine.substring(5, 15).trim();
this.firstName = inputLine.substring(15, 25).trim();
this.patientPhone = inputLine.substring(25, 33).trim();
this.patientType = inputLine.substring(33, 35).trim();
this.bedIdentity = inputLine.substring(35, 39).trim();
this.admitDate = LocalDate.parse(inputLine.substring(39, 45), DateTimeFormatter.ofPattern("yyMMdd"));
this.dailyAmount = Double.parseDouble(inputLine.substring(45, 54).trim());
this.insuranceType = inputLine.substring(54, 57).trim();
this.hospitalStayLength = Integer.parseInt(inputLine.substring(57, 60).trim());
this.network = inputLine.substring(60, 62).trim();
this.copay = Double.parseDouble(inputLine.substring(62, 68).trim());
this.deductible = Double.parseDouble(inputLine.substring(68, 74).trim());
上記をみると、Java のコードでは想定している入力データは 0 〜 74 つまり、75文字のデータを想定している事がわかります。
そこで、もう一度実際の入力データを確認し、入力データがどの位あるのかを確認してみます。
すると下記のように 90 文字のデータが含まれている事がわかりました。
> echo "11111Smith Joanne 2016521111I000112/12/20050005123010HMO012012345678A1O4H9I0100015" | wc
1 3 90
なるほど、実際の入力データに対して、データ解析をしている部分が間違えているのだと気づき、この部分を正しく修正しようと考えました。
今一度、COBOL のコードを確認し、Java の変数名との兼ね合いから、この辺りで実際のデータを扱っているのだろうと判断し(繰り返します COBOL は知りません)、この部分のコードの内容を chatGPT で確認する事にしました。
私からの問い合わせ
この COBOL のコードをFILLERの部分を除いて具体的に一行ずつ説明してください。
01 WS-OUTPUT-REC.
05 PATIENT-NBR-O PIC 9(05).
05 FILLER PIC X(02) VALUE SPACES.
05 PATIENT-NAME-O.
10 PATIENT-LNAME-O PIC X(10).
10 FILLER PIC X(02) VALUE SPACES.
10 PATIENT-FNAME-O PIC X(10).
10 FILLER PIC X(02) VALUE SPACES.
05 PATIENT-PHONE-O.
10 FILLER PIC X(01) VALUE "(".
10 PATIENT-AREACODE-O PIC X(03).
10 FILLER PIC X(01) VALUE ")".
10 PATIENT-PREFIX-O PIC X(03).
10 FILLER PIC X(01) VALUE "-".
10 PATIENT-SUFFIX-O PIC X(04).
05 FILLER PIC X(02) VALUE SPACES.
05 PATIENT-TYPE-O PIC X(02).
05 FILLER PIC X(02) VALUE SPACES.
05 BED-IDENTITY-O PIC ZZZ9.
05 FILLER PIC X(02) VALUE SPACES.
05 CURR-DATE-O PIC X(06).
05 FILLER PIC X(02) VALUE SPACES.
05 PATIENT-AMT-PER-DAY-O PIC $$,$$9.99.
05 FILLER PIC X(02) VALUE SPACES.
05 INS-COVERAGE-PERC-O PIC 9(03).
05 FILLER PIC X(02) VALUE SPACES.
05 INS-TYPE-O PIC X(04).
05 HOSPITAL-STAY-LTH-O PIC 9(03).
05 FILLER PIC X(02) VALUE SPACES.
05 NETWORK-O PIC X(06).
05 FILLER PIC X(02) VALUE SPACES.
05 COPAY-O PIC $$,$$9.
05 FILLER PIC X(02) VALUE SPACES.
05 DEDUCT-O PIC $$,$$9.
※ 最初は FILLER は無しで問い合わせしたのですが、FILLER は意味がなかったので、その部分は省略しています。
3. Azure OpenAI chatGPT-4-32k からの返信
このCOBOLコードは、患者の情報を出力するためのデータ構造(レコード)を定義しています。
FILLERの部分を除いてそれぞれの行は以下のような意味を持っています。
01 WS-OUTPUT-REC.
レコード全体を定義するレベル01の変数です。
05 PATIENT-NBR-O PIC 9(05).
患者番号を表す5桁の数値変数です。
05 PATIENT-NAME-O.
患者名を表すグループ変数です。
10 PATIENT-LNAME-O PIC X(10).
患者の姓を表す10文字の英字変数です。
10 PATIENT-FNAME-O PIC X(10).
患者の名前を表す10文字の英字変数です。
05 PATIENT-PHONE-O.
患者の電話番号を表すグループ変数です。
10 PATIENT-AREACODE-O PIC X(03).
患者の電話番号の市外局番を表す3文字の英字変数です。
10 PATIENT-PREFIX-O PIC X(03).
患者の電話番号のプレフィックスを表す3文字の英字変数です。
10 PATIENT-SUFFIX-O PIC X(04).
患者の電話番号のサフィックスを表す4文字の英字変数です。
05 PATIENT-TYPE-O PIC X(02).
患者のタイプを表す2文字の英字変数です。
05 BED-IDENTITY-O PIC ZZZ9.
患者のベッド番号を表す4桁の数値変数です。
05 CURR-DATE-O PIC X(06).
現在の日付を表す6文字の英字変数です。
05 PATIENT-AMT-PER-DAY-O PIC $$,$$9.99.
患者の1日あたりの金額を表す金額変数です。
05 INS-COVERAGE-PERC-O PIC 9(03).
患者の保険カバー率を表す3桁の数値変数です。
05 INS-TYPE-O PIC X(04).
患者の保険タイプを表す4文字の英字変数です。
05 HOSPITAL-STAY-LTH-O PIC 9(03).
患者の入院期間を表す3桁の数値変数です。
05 NETWORK-O PIC X(06).
患者のネットワークを表す6文字の英字変数です。
05 COPAY-O PIC $$,$$9.
患者のコーペイ(自己負担額)を表す金額変数です。
05 DEDUCT-O PIC $$,$$9.
患者の控除額を表す金額変数です。
各データの桁数とソースコードの確認
上記の結果から、2つの問題がわかりました。
- 電話番号の部分からおかしくなっている
- 患者の保険カバー率を表す変数がそもそも定義されていない
this.patientNumber = inputLine.substring(0, 5).trim(); // 5桁(患者番号)
this.lastName = inputLine.substring(5, 15).trim(); // 10文字(姓)
this.firstName = inputLine.substring(15, 25).trim(); // 10文字(名)
// 下記でずれている
this.patientPhone = inputLine.substring(25, 33).trim(); // 10文字(電話番号(3:市外局番, 3:プレフィックス, 4:サフィックス))
this.patientType = inputLine.substring(33, 35).trim(); // 2桁(患者タイプ)
this.bedIdentity = inputLine.substring(35, 39).trim(); // 4桁(ベッド番号)
// ここで例外が出力(当前だ!)
this.admitDate = LocalDate.parse(inputLine.substring(39, 45), DateTimeFormatter.ofPattern("yyMMdd"));
this.dailyAmount = Double.parseDouble(inputLine.substring(45, 54).trim()); // 患者の1日あたりの金額
//さらに "3桁(患者の保険カバー率を表す)" 変数がない!!(DIAG (COVERAGE-PERC) がない!!)
this.insuranceType = inputLine.substring(54, 57).trim(); // 4文字(患者の保険タイプ)
this.hospitalStayLength = Integer.parseInt(inputLine.substring(57, 60).trim()); //3桁(入院期間)
this.network = inputLine.substring(60, 62).trim(); //6文字 (患者のネットワーク)
this.copay = Double.parseDouble(inputLine.substring(62, 68).trim()); //患者のコーペイ(自己負担額)
this.deductible = Double.parseDouble(inputLine.substring(68, 74).trim()); //患者の控除額
ソースコードの修正
そこで、定義されていなかった diag
を新たに定義し
private final String diag;
上記の各データに対する説明と、実際に入力されているデータを参考に、データの解析をしなおしたのがこちらになります。
※ 実際には、上記で説明されている変数の桁数の取り扱いと、実際の入力データで桁数が合わない部分があった為、本家サイトの HOSPEDIT-INFILE という入力データを元に調整を行ないました。
元の COBOL のコードは正しく動くのかな? でも出力部分から取ってきたので違っているのかな?入力部分は違うのかも、ただ COBOL の入力から変数に割り当てる部分がよくわからなかった。ただ、入力データをじっくり見て、なんとなくわかったので(本家で出力されているデータと見比べて同じのがとれたので)今回は進めて行こう!
調整した結果の実装がコチラ
this.inputLine = inputLine;
this.patientNumber = inputLine.substring(0, 5).trim(); // 5桁(患者番号)
this.lastName = inputLine.substring(5, 15).trim(); // 10文字(姓)
this.firstName = inputLine.substring(15, 25).trim(); // 10文字(名)
this.patientPhone = inputLine.substring(25, 35).trim(); // 10文字(電話番号(3:市外局番, 3:プレフィックス, 4:サフィックス))
this.patientType = inputLine.substring(35, 36).trim(); // 2桁(患者タイプ) <---- 入力データから実際には 1 桁だった
this.bedIdentity = inputLine.substring(36, 40).trim(); // 4桁(ベッド番号)
this.admitDate = inputLine.substring(40, 48).trim(); // 6文字(日付) <---- 入力データから実際には 8 桁だった
this.dailyAmount = inputLine.substring(48, 57).trim(); // 患者の1日あたりの金額
this.diag = inputLine.substring(57, 60).trim(); //3桁(患者の保険カバー率を表す)
this.insuranceType = inputLine.substring(60, 63).trim(); // 4文字(患者の保険タイプ) <--- 入力データから実際には 3 文字だった
this.hospitalStayLength = Integer.parseInt(inputLine.substring(63, 66).trim()); //3桁(入院期間)
this.network = inputLine.substring(66, 82).trim(); //6文字 (患者のネットワーク) <---- ネットワークだけでこんなに取る? (違うデータも含まれているかも、ただ出力に該当するデータが見当たらない)
this.copay = Double.parseDouble(inputLine.substring(82, 85).trim()); //患者のコーペイ(自己負担額)
this.deductible = Double.parseDouble(inputLine.substring(85, 89).trim()); //患者の控除額
そして最後に、新たに追加した変数 diag も出力するように、出力用の文字列をフォーマットしている箇所も直しています。
public String toReportLine() {
return String.format(
"%-5s %-10s %-10s (%s) %s-%s %-4s %-4s %-6s %-8s %-4s %-4s %-4s %-7s %-5s %-5s",
patientNumber, lastName, firstName, patientPhone.substring(0, 3), patientPhone.substring(3, 6),
patientPhone.substring(6),
patientType, bedIdentity, admitDate, dailyAmount, diag,
insuranceType, hospitalStayLength, network, copay, deductible);
}
}
さてこれで、修正がある程度終わったので、動くようにはなっているのですが、最後にコードをもう少し綺麗にしたいと思います。
私からの問い合わせ
こちらのコードに対して、private メソッドを含む全メソッドと重要な箇所にコメントをいれ、
エラー処理の実装と、コードの最適化を行い、再修正してください。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class HospEdit2 {
// Constants for input and output file names
private static final String INPUT_FILE = "HOSPIN.txt";
private static final String ERROR_FILE = "ERRFILE.txt";
private static final String REPORT_FILE = "RPTFILE.txt";
public static void main(String[] args) {
try {
// Read input file lines
List<String> inputLines = Files.readAllLines(Path.of(INPUT_FILE));
List<String> errorLines = new ArrayList<>();
List<String> reportLines = new ArrayList<>();
// Print headers
reportLines.add("PATNO PATIENT PHONE-NO TYPE BED ADMIT DAILY AMT DIAG INS STAY NETWORK COPAY DEDUCT");
reportLines.add("===== ===================== ============== ==== ==== ======== ========= ==== ==== ==== ================ ===== ======");
// Process input lines
for (String inputLine : inputLines) {
try {
// Create a PatientRecord object from the input line
PatientRecord patientRecord = new PatientRecord(inputLine);
// Check if the patient record is valid
if (patientRecord.isValid()) {
// Add the formatted report line to the reportLines list
reportLines.add(patientRecord.toReportLine());
} else {
// Add the error message and the input line to the errorLines list
errorLines.add(patientRecord.getErrorMessage() + " " + inputLine);
}
} catch (Exception e) {
// Add the invalid input line to the errorLines list
errorLines.add("INVALID INPUT: " + inputLine);
}
}
// Write output files
Files.write(Path.of(ERROR_FILE), errorLines);
Files.write(Path.of(REPORT_FILE), reportLines);
} catch (IOException e) {
// Handle file read/write errors
System.err.println("Error reading or writing files: " + e.getMessage());
}
}
// PatientRecord class to store and validate patient information
static class PatientRecord {
// Fields to store patient information
private final String inputLine;
private final String patientNumber;
private final String lastName;
private final String firstName;
private final String patientPhone;
private final String patientType;
private final String bedIdentity;
private final String admitDate;
private final String dailyAmount;
private final String diag;
private final String insuranceType;
private final int hospitalStayLength;
private final String network;
private final double copay;
private final double deductible;
// Constructor to parse and store patient information from the input line
public PatientRecord(String inputLine) {
this.inputLine = inputLine;
this.patientNumber = inputLine.substring(0, 5).trim();
this.lastName = inputLine.substring(5, 15).trim();
this.firstName = inputLine.substring(15, 25).trim();
this.patientPhone = inputLine.substring(25, 35).trim();
this.patientType = inputLine.substring(35, 36).trim();
this.bedIdentity = inputLine.substring(36, 40).trim();
this.admitDate = inputLine.substring(40, 48).trim();
this.dailyAmount = inputLine.substring(48, 57).trim();
this.diag = inputLine.substring(57, 60).trim();
this.insuranceType = inputLine.substring(60, 63).trim();
this.hospitalStayLength = Integer.parseInt(inputLine.substring(63, 66).trim());
this.network = inputLine.substring(66, 82).trim();
this.copay = Double.parseDouble(inputLine.substring(82, 85).trim());
this.deductible = Double.parseDouble(inputLine.substring(85, 89).trim());
}
// Check if the patient record is valid
public boolean isValid() {
return isInputLineLengthValid() && isValidPatientType() && isValidInsuranceType() && isNumericFieldsValid() && isPhoneNumberValid();
}
// Get the error message for an invalid patient record
public String getErrorMessage() {
if (!isInputLineLengthValid()) {
return "INVALID INPUT LENGTH";
} else if (!isValidPatientType()) {
return "INVALID PAT TYPE";
} else if (!isValidInsuranceType()) {
return "INVALID INS TYPE";
} else if (!isNumericFieldsValid()) {
return "INVALID NUMERIC FIELD";
} else {
return "INVALID PHONE NUMBER";
}
}
// Check if the input line length is valid
private boolean isInputLineLengthValid() {
return inputLine.length() >= 89;
}
// Check if the numeric fields are valid
private boolean isNumericFieldsValid() {
try {
Integer.parseInt(inputLine.substring(63, 66).trim());
Double.parseDouble(inputLine.substring(82, 85).trim());
Double.parseDouble(inputLine.substring(85, 89).trim());
return true;
} catch (NumberFormatException e) {
return false;
}
}
// Check if the phone number is valid
private boolean isPhoneNumberValid() {
String regex = "\\d{3}\\d{3}\\d{4}";
return patientPhone.matches(regex);
}
// Check if the patient type is valid
private boolean isValidPatientType() {
return patientType.equals("I") || patientType.equals("O");
}
// Check if the insurance type is valid
private boolean isValidInsuranceType() {
return InsuranceType.isValid(insuranceType);
}
// Format the patient record as a report line
public String toReportLine() {
return String.format("%-5s %-10s %-10s (%s) %s-%s %-4s %-4s %-6s %-8s %-4s %-4s %-4s %-7s %-5s %-5s",
patientNumber, lastName, firstName, patientPhone.substring(0, 3), patientPhone.substring(3, 6),
patientPhone.substring(6), patientType, bedIdentity, admitDate, dailyAmount, diag,
insuranceType, hospitalStayLength, network, copay, deductible);
}
}
// Enum for insurance types
enum InsuranceType {
HMO("HMO", "Health Maintenance Organization"),
PRI("PRI", "Private"),
PPO("PPO", "Preferred Provider Organization"),
AFF("AFF", "Affordable"),
MED("MED", "State/Federal");
private final String code;
private final String description;
// Constructor for insurance types
InsuranceType(String code, String description) {
this.code = code;
this.description = description;
}
// Check if the insurance type code is valid
public static boolean isValid(String code) {
for (InsuranceType type : values()) {
if (type.code.equals(code)) {
return true;
}
}
return false;
}
}
}
このコードを実行することで、アプリが動くようにはなりました。
さらに、いろいろな自然言語による改善の結果
その後も、細かな改善を行ないました。
例えば、こんな事言ったり
上記の、dailyAmount を String ではなく double にリファクタリングしてください。
例えば、入力された値が "050005123" の場合、整数の範囲は上位5桁の"05000"、
つまり 5000 になり、下位4桁の"5123"の部分は小数点になります。
数値に変換後、小数点以下第3位を四捨五入し、小数点以下2位で出力してください
※ dailyAmount の部分はコードの解析のため、一旦 String に変換していました。
ですので、String から double に戻したのですが、その際、ルールを決めないととんでもない金額になるため、上5桁、下4桁に分けて出力することを考えました。
アメリカの病院にかかると、びっくりする程高額と言いますので、$5000 で日本円で 67万円なので、5桁で切るのが妥当かな? (皆様、海外での病気や怪我はお気をつけくださいませ!)
さらに、こんな事言ったり
dailyAmount は USD で表示してください
あと、こんな事も言ったり
bedIdentity, dailyAmount, hospitalStayLength, copay, deductible は
右揃えで出力してください。
それでも、期待する通りに、綺麗に出力するコードを自然言語から作るプロンプトの力量を持っていなかったため、下記の出力の整形部分は若干手を加えました。
public String toReportLine() {
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.US);
String dailyAmountFormatted = currencyFormat.format(dailyAmount);
return String.format(
"%-5s %-10s %-10s (%s) %s-%s %4s %4d %-6s %10s %-4s %-4s %4d %-7s %5s %5s",
patientNumber, lastName, firstName, patientPhone.substring(0, 3), patientPhone.substring(3, 6),
patientPhone.substring(6),
patientType, bedIdentity, admitDate.format(DateTimeFormatter.ofPattern("MM/dd/yy")), dailyAmountFormatted, diag,
insuranceType, hospitalStayLength, network, copay, deductible);
}
そして、最後に
この Java のクラス内の private メソッドを含む全てのメソッドに日本語でコメントを記載してください。
parseDailyAmount メソッドは処理内容もコメント中に記載してください。
さらに重要な処理やポイントがあれば、その部分もコメントを付け加えて整形して出力してください。
最終成果物の出来上がり
色々と、極力自然言語で問い合わせを行い、一部細かいデータ解析や、細かい出力形式の調整はしたものの、下記の Java のコードに移行できました。
600行→186行、まぁ全く COBOL を知らない Java エンジニアにしては、ある程度できているのではないでしょうか?
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class HospEdit3 {
private static final String INPUT_FILE = "HOSPIN.txt";
private static final String ERROR_FILE = "ERRFILE.txt";
private static final String REPORT_FILE = "RPTFILE.txt";
public static void main(String[] args) {
try {
// 入力ファイルから行を読み込む
List<String> inputLines = Files.readAllLines(Path.of(INPUT_FILE));
List<String> errorLines = new ArrayList<>();
List<String> reportLines = new ArrayList<>();
// レポートのヘッダーを追加
reportLines.add(
"PATNO PATIENT PHONE-NO TYPE BED ADMIT DAILY AMT DIAG INS STAY NETWORK COPAY DEDUCT");
reportLines.add(
"===== ===================== ============== ==== ==== ======== ========== ==== === ==== ================ ===== ======");
// 入力行をループして、患者レコードを作成し、レポートに追加する
for (String inputLine : inputLines) {
boolean validInput = true;
boolean validPatientType = true;
boolean validInsuranceType = true;
PatientRecord patientRecord = null;
try {
// 患者レコードを作成
patientRecord = new PatientRecord(inputLine);
// 患者タイプが有効かどうかをチェック
validPatientType = patientRecord.getPatientType().equals("I")
|| patientRecord.getPatientType().equals("O");
// 保険タイプが有効かどうかをチェック
validInsuranceType = InsuranceType.isValid(patientRecord.getInsuranceType());
} catch (IllegalArgumentException e) {
validInput = false;
errorLines.add("INVALID INPUT FORMAT: " + inputLine);
} catch (Exception e) {
validInput = false;
errorLines.add("UNKNOWN ERROR: " + inputLine);
}
// 入力が有効でない場合、エラーファイルに追加
if (validInput && !validPatientType) {
errorLines.add("INVALID PAT TYPE: " + inputLine);
} else if (validInput && !validInsuranceType) {
errorLines.add("INVALID INS TYPE: " + inputLine);
} else {
// 患者レコードが有効な場合、レポートに追加
if (patientRecord != null) {
reportLines.add(patientRecord.toReportLine());
}
}
}
// エラーファイルとレポートファイルに書き込む
Files.write(Path.of(ERROR_FILE), errorLines);
Files.write(Path.of(REPORT_FILE), reportLines);
} catch (IOException e) {
System.err.println("Error reading or writing files: " + e.getMessage());
}
}
static class PatientRecord {
private final String inputLine;
private final String patientNumber;
private final String lastName;
private final String firstName;
private final String patientPhone;
private final String patientType;
private final Integer bedIdentity;
private final LocalDate admitDate;
private final double dailyAmount;
private final String diag;
private final String insuranceType;
private final int hospitalStayLength;
private final String network;
private final double copay;
private final double deductible;
// コンストラクタで患者レコードを作成
public PatientRecord(String inputLine) {
this.inputLine = inputLine;
this.patientNumber = inputLine.substring(0, 5).trim();
this.lastName = inputLine.substring(5, 15).trim();
this.firstName = inputLine.substring(15, 25).trim();
this.patientPhone = inputLine.substring(25, 35).trim();
this.patientType = inputLine.substring(35, 36).trim();
this.bedIdentity = Integer.parseInt(inputLine.substring(36, 40).trim());
this.admitDate = parseAdmitDate(inputLine.substring(40, 48).trim());
this.dailyAmount = parseDailyAmount(inputLine.substring(48, 57).trim());
this.diag = inputLine.substring(57, 60).trim();
this.insuranceType = inputLine.substring(60, 63).trim();
this.hospitalStayLength = Integer.parseInt(inputLine.substring(63, 66).trim());
this.network = inputLine.substring(66, 82).trim();
this.copay = Double.parseDouble(inputLine.substring(82, 85).trim());
this.deductible = Double.parseDouble(inputLine.substring(85, 89).trim());
}
// 入力文字列から admitDate を LocalDate に変換する
private LocalDate parseAdmitDate(String dateString) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yy");
return LocalDate.parse(dateString, formatter);
}
// 入力文字列から dailyAmount を double に変換する
// このメソッドでは、入力文字列を整数部と小数部に分割し、
// 整数部をそのまま double に変換し、小数部を BigDecimal に変換してから double に変換し、
// 両者を加算して最終的な dailyAmount を計算しています。
private double parseDailyAmount(String dailyAmountString) {
int integerPart = Integer.parseInt(dailyAmountString.substring(0, 5));
int decimalPart = Integer.parseInt(dailyAmountString.substring(5));
BigDecimal decimalAmount = BigDecimal.valueOf(decimalPart, 3).setScale(2, RoundingMode.HALF_UP);
return integerPart + decimalAmount.doubleValue();
}
public String getPatientType() {
return patientType;
}
public String getInsuranceType() {
return insuranceType;
}
// レポート用の文字列に変換する
public String toReportLine() {
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.US);
String dailyAmountFormatted = currencyFormat.format(dailyAmount);
return String.format(
"%-5s %-10s %-10s (%s) %s-%s %4s %4d %-6s %10s %-4s %-4s %4d %-7s %5s %5s",
patientNumber, lastName, firstName, patientPhone.substring(0, 3), patientPhone.substring(3, 6),
patientPhone.substring(6),
patientType, bedIdentity, admitDate.format(DateTimeFormatter.ofPattern("MM/dd/yy")), dailyAmountFormatted, diag,
insuranceType, hospitalStayLength, network, copay, deductible);
}
}
// 保険タイプを表す列挙型
enum InsuranceType {
HMO("HMO", "Health Maintenance Organization"),
PRI("PRI", "Private"),
PPO("PPO", "Preferred Provider Organization"),
AFF("AFF", "Affordable"),
MED("MED", "State/Federal");
private final String code;
private final String description;
InsuranceType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
// 保険タイプが有効かどうかを判断するメソッド
public static boolean isValid(String code) {
for (InsuranceType type : values()) {
if (type.getCode().equals(code)) {
return true;
}
}
return false;
}
}
}
移行した Java アプリの実行
上記のコードを実際に実行すると、下記のような結果が RPTFILE.txt ファイルに出力されます。
PATNO PATIENT PHONE-NO TYPE BED ADMIT DAILY AMT DIAG INS STAY NETWORK COPAY DEDUCT
===== ===================== ============== ==== ==== ======== ========== ==== === ==== ================ ===== ======
11111 Smith Joanne (201) 652-1111 I 1 12/12/20 $5,005.12 010 HMO 12 012345678A1O4H9I 10.0 15.0
22222 Martin Ricky (212) 444-1212 O 2 10/01/19 $52,003.35 623 PPO 6 001231421B2K5T8N 90.0 25.0
63333 Jones James (202) 555-1212 I 3 11/23/20 $4,000.87 547 HMO 20 071232321C3J6F7I 80.0 35.0
74444 McAllister BillieJoe (876) 234-1234 I 234 09/23/20 $8,027.32 483 PRI 11 045321321D4H7D6I 70.0 45.0
55555 Luthor Lex (764) 523-4324 I 984 12/31/20 $8,047.36 746 MED 0 000123786E5G8S5N 60.0 55.0
98765 Clampett E.M. (546) 443-2432 I 13 08/12/20 $5,002.34 138 PRI 30 004536179G7D0Q6I 40.0 75.0
88888 Watson Thomas (212) 764-3483 O 22 07/25/20 $5,003.11 329 PPO 10 003546321H8S1W3I 30.0 75.0
99999 Hamilton Alex (909) 888-3213 I 2 06/30/20 $6,002.32 483 HMO 20 000047574I9D2E2I 20.0 85.0
10021 Welch John (203) 652-0980 O 1 05/31/20 $7,001.12 010 AFF 40 097812143J0F3F1N 10.0 14.0
12901 Thomas Frank (203) 652-0980 O 1 05/31/20 $7,001.12 010 AFF 50 097812143J0F3F1N 0.0 0.0
上記の結果と、本家サイトにある HOSPEDIT-RPTFILE のファイルの内容と比べると、似たような結果が得られているかと思います。
正直申し上げますと、NETWORK 部分が不安です。本当にあっているんだろうか? もしかしたら、複数のデータが含まれているのでは?という疑問もあるのですが。今の私には分かりません。ここから先は業務仕様をちゃんとわかった方と、お話をしなければ理解できないので、今回はネットワーク部はこのままにしておきます。
そして、ERRFILE.txt ファイルには下記の内容が出力されていました。
UNKNOWN ERROR: 66666Barnum P.H. 7854354354O078321/09/20060000432439HMO025070176321F6F9A4N0500065
INVALID INPUT FORMAT: 88888Bad Pat. Type X GOV
INVALID INPUT FORMAT: 99999Bad Ins. Type O XXX
上記の一行目は何故だろう?と詳しく確認したのですが、20年21月09日という日付がおかしいからですね。
以上で、マイグレーションの検証は終わりとなります。