diff --git a/JavaSrc/.gitignore b/JavaSrc/.gitignore new file mode 100644 index 0000000..83444f6 --- /dev/null +++ b/JavaSrc/.gitignore @@ -0,0 +1,5 @@ +# JavaSrc (cobol-tna-system COBOL->Java) +# doc/ lib/ src/ 等工程情报全部纳入版本管理;仅忽略本地临时/构建产物 +out/ +bk/ +tmp/ diff --git a/JavaSrc/README.md b/JavaSrc/README.md new file mode 100644 index 0000000..3e3a6e7 --- /dev/null +++ b/JavaSrc/README.md @@ -0,0 +1,32 @@ +# cobol-tna-system Java 版(JavaSrc) + +勤怠管理 COBOL システム `cobol-tna-system` の COBOL→Java 変換成果を格納するディレクトリ。 + +## 対象プログラム +- **ZAN01-06**:チェック / マッチング / 計算 / 更新 / 日次 など +- **KIN01-09**:入力 / 更新 / 出力 / DB など +- **KYU01-09**:給与計算(CSV → DB → 集計 → 支給データ出力) + +## ディレクトリ構成 +- `src/`:Java ソース(`*Main.java` 各主程序 + `dto/` レコードクラス + `util/` 共通ツール) +- `doc/`:変換ルール・設計書(COBOL変換_作業記録 / Java側規格説明 / 確認Checklist / 転換基準) +- `lib/`:実行時依存ライブラリ(`sqlite-jdbc-3.8.11.2.jar`) +- `compile.bat` / `run.bat` / `run_db.bat`:コンパイル・実行スクリプト(SQLite 利用) + +## ビルド +``` +javac -encoding UTF-8 -d out -cp "out;lib/sqlite-jdbc-3.8.11.2.jar" src/*.java src/dto/*.java src/util/*.java +``` + +## 実行例 +``` +java -cp "out;lib/sqlite-jdbc-3.8.11.2.jar" Kyu01CvtMain +``` +KYU の DB プログラム(KYU02/04/05/06)は SQLite を利用: +``` +java -Dkyu.db.url=jdbc:sqlite:data/kyu.db -cp "out;lib/sqlite-jdbc-3.8.11.2.jar" Kyu04CalMain +``` + +## 備考 +- 変換基準は `doc/転換基準.md` を参照。設計書(詳細設計書_KYU0x* 等)が優先し、COBOL は参考。 +- 動作確認ログはプロジェクトルートの `_AI_USAGE_LOG.md` を参照。 diff --git a/JavaSrc/compile.bat b/JavaSrc/compile.bat new file mode 100644 index 0000000..887b0b8 --- /dev/null +++ b/JavaSrc/compile.bat @@ -0,0 +1,16 @@ +@echo off +rem === Compile script for COBOL-to-Java sample project === +setlocal +set "JDK_BIN=D:\Program Files\Java\jdk1.8.0_144\bin" +if not exist "%JDK_BIN%\javac.exe" set "JDK_BIN=%JAVA_HOME%\bin" + +if exist out rmdir /s /q out +mkdir out + +"%JDK_BIN%\javac.exe" -encoding UTF-8 -d out src\*.java src\dto\*.java src\util\*.java +if errorlevel 1 ( + echo Compile FAILED. + exit /b 1 +) +echo Compile OK. +endlocal diff --git a/JavaSrc/doc/COBOL不备点_调查记录.md b/JavaSrc/doc/COBOL不备点_调查记录.md new file mode 100644 index 0000000..c9f4656 --- /dev/null +++ b/JavaSrc/doc/COBOL不备点_调查记录.md @@ -0,0 +1,151 @@ +# 既存 COBOL コード不備点 調査記録 + +- **作成日**: 2026-08-25 +- **背景**: KIN01‑09 / ZAN01‑06 の COBOL→Java 変換後、Java↔COBOL↔設計書 の三方照合を実施。本ファイルは「既存 COBOL 側に存在する不備・設計書からの乖離」を記録し、後続調査・対応の起点とする。 +- **関連成果物**: + - 修正済 Java: `src/Sub05TimSub.java`(丸め), `src/Kin07DaiMain.java`(未知休暇型), `src/dto/Kin03Rec.java`(数値埋込), `src/Zan03ChkMain.java`(休日フラグ互換), `src/util/ConvUtil.java`(padLeftZero 追加) + - COBOL 側マーキング: 各対象行に `COBOL-DEFICIENCY:` コメントを付与済(参照先=本ファイル) + +--- + +## 1. 照合結果サマリ(全 30 項目) + +判定: ◯=一致(無影響) △=軽差/条件無影響 ✗=差異(有影響) + +| 模块 | 文件 | 判定 | COBOL 側問題? | 備考 | +|------|------|------|--------------|------| +| ZAN01 | Zan01ChkMain | ◯ | – | trim/再符号化のみ | +| ZAN02 | Zan02ChkMain | ◯ | – | キー比較のみ条件差 | +| **ZAN03** | Zan03ChkMain | ✗ | **C類** | 休日フラグ値('1')設計未規定/運用慣習 | +| ZAN04 | Zan04MatMain | ◯ | – | – | +| **ZAN05** | Zan05CalMain | ✗ | –(COBOL正) | Java 側真缺陷(丸め)→已修 | +| ZAN06 | Zan06UpdMain | ◯ | – | ADD は COBOL と一致 | +| **KIN01** | Kin01InpMain | ✗ | **A類** | フィールド数ゲート off‑by‑one | +| KIN02 | Kin02UpdMain | ◯ | – | – | +| KIN03 | Kin03ExpMain | ◯ | – | – | +| **KIN04** | Kin04ChkMain | ✗ | **A類** | フィールド数ゲート off‑by‑one | +| **KIN05** | Kin05MatMain | ✗ | **A類** | WRK‑BEST 未リセット | +| KIN06 | Kin06CldMain | △ | **D類**(条件) | 休日全件取込≤50 件の上限リスク | +| KIN07 | Kin07DaiMain | △ | –(COBOL正) | Java 側真缺陷(未知型)→已修 | +| **KIN08** | Kin08DbuMain | ✗ | **B類** | RESET で MONTHLY のみ削除/T カード 10 名上限 | +| **KIN09** | Kin09CsvMain | ✗ | **B類** | FULL/SHORT 出力範囲反転/時間欄書式 | +| SUB01 | Sub01DatSub | △ | – | TZ 依存(条件) | +| SUB02 | Sub02MsgSub | ◯ | – | – | +| SUB03 | Sub03EndSub | ◯ | – | – | +| SUB04 | Sub04ChkSub | △ | – | parseInt vs NUMVAL(条件) | +| SUB05 | Sub05TimSub | △ | – | double 精度(条件) | +| PAR DTO群 | Zan*Par/Kin*Rec 等 | ◯ | – | – | +| **Kin03Rec** | dto/Kin03Rec | △ | –(copybook正) | Java 側真缺陷(埋込)→已修 | +| EmpRec | dto/EmpRec | – | – | COBOL 対応物なし(照合不可) | + +--- + +## 2. COBOL 側不備の詳細分類 + +### A 類:COBOL 自身のバグ(Java は設計書通りに修正済) + +#### A‑1. KIN01INP — フィールド数ゲート誤り +- **ファイル:行**: `cobol/KIN01INP.cbl:250` `IF WRK-COMMA-CNT NOT = 8` +- **COBOL 挙動**: `WRK-COMMA-CNT` は `UNSTRING ... TALLYING` の**区切り数**(8 フィールド→7)。`7 ≠ 8` が常真となり、形式正しい 8 フィールド行も全て `F` エラー(W02) へ振分け。 +- **設計指定**: 設計書は「フィールド数=8」で判定。 +- **Java 挙動**: `tokens.length == 8` で判定(設計通り)。→ COBOL と W01/W02 が逆転。 +- **COBOL 側問題**: 明確な off‑by‑one バグ。 +- **対応**: Java は設計準拠で正。COBOL 側はレガシー不備。修正不要(Java 維持)。 + +#### A‑2. KIN04CHK — フィールド数ゲート誤り(同上) +- **ファイル:行**: `cobol/KIN04CHK.cbl:271,293` `IF WRK-COMMA-CNT = CNS-FIELD-COUNT-6(=6)` +- **COBOL 挙動**: 区切り数は 5。`5 = 6` は常偽 → 以降の 4 段検証が実行されず、正しい 6 フィールド行も W02 へ。 +- **設計指定**: フィールド数=6。 +- **Java 挙動**: `tokens.length == 6`(設計通り)。 +- **対応**: Java 維持。COBOL 側バグ。 + +#### A‑3. KIN05MAT — WRK‑BEST 未リセット(情報リーク) +- **ファイル:行**: `cobol/KIN05MAT.cbl:400` `2100R01ONLYSOR SECTION.`(R01 単独出力) +- **COBOL 挙動**: `1000ITTSOR`/`2020MATCHSOR` のみ `WRK-BEST-*` を初期化。`2100R01ONLYSOR`(マッチ直後の「休暇なし R01」) ではリセットせず、**直前のマッチの休暇情報(LEAVE‑TYPE/APPL‑ID)がそのまま出力**される。 +- **設計指定**: R01 単独 → '99' 初期化(設計書「R01 単独は '99'」)。 +- **Java 挙動**: 各 R01 で `resetBest()` を実施(設計通り)。 +- **対応**: Java 維持。COBOL 側バグ。 + +### B 類:COBOL が設計書から乖離(Java は設計書通り=正) + +> これらは「COBOL の挙動が設計と食い違っている」点。Java を COBOL に合わせて戻すと**設計違反**になる。原則として **Java を維持**し、COBOL 側を不備として記録する。 + +#### B‑1. KIN09CSV — FULL/SHORT 出力範囲が設計と逆 +- **ファイル:行**: `cobol/KIN09CSV.cbl:160` `IF MODE-SHORT PERFORM 2500-SHORT-MODE` +- **COBOL 挙動**: 日別行は常時出力、`MODE-SHORT` 時にのみサマリ追加 → **FULL=日別のみ / SHORT=日別+サマリ**。 +- **設計指定**: PARM 表 `FULL=日別+月次 / SHORT=月次のみ`(詳細設計書_KIN09CSV L33)。 +- **Java 挙動**: 設計通り `FULL=日別+サマリ / SHORT=サマリのみ`。 +- **対応**: **Java 維持**。COBOL が設計と逆。※設計書 §3 見出し「SHORT 時のみ追加処理」も矛盾含み(設計書自身の不備、後述)。 + +#### B‑2. KIN09CSV — 時間欄編集書式 +- **COBOL 挙動**: `PIC Z(3)9.9` → 先頭空白(例 `" 8.0"`)。 +- **設計指定**: CSV 例 `8.0`(先頭空白なし)。 +- **Java 挙動**: 先頭空白なし(設計例通り)。 +- **対応**: **Java 維持**(設計例に合致)。COBOL が設計例と食い違い。 + +#### B‑3. KIN08DBU — RESET で MONTHLY のみ削除(DAILY を削除しない) +- **ファイル:行**: `cobol/KIN08DBU.cbl:488` `DELETE FROM MONTHLY_ABSENCE` +- **COBOL 挙動**: RESET 時に `MONTHLY_ABSENCE` のみ削除。`DAILY_RECORDS` は削除せず、再実行時に PK 衝突で ABEND リスク。 +- **設計指定**: 使用 DB 表(L45)・SYSIN 仕様(L29)「RESET=既存削除後再 INSERT」→ **DAILY_RECORDS も DELETE+INSERT**。 +- **Java 挙動**: RESET で DAILY+MONTHLY 両削除(設計通り)。 +- **対応**: **Java 維持**。COBOL が設計と乖離(設計書ステップ 3‑1 に MONTHLY 削除しか書かれておらず記述漏れ)。 + +#### B‑4. KIN08DBU — T カード対象社員 10 名上限 +- **ファイル:行**: `cobol/KIN08DBU.cbl:294` `UNSTRING ... INTO TARGET-EMP(1)...(10)` +- **COBOL 挙動**: `UNSTRING` の受信側が `(1)..(10)` のみ(テーブルは OCCURS 100 だが実質 10 名まで)。11 名目以降は集計から漏れる。 +- **設計指定**: T カード上限の記載なし(無制限扱い)。 +- **Java 挙動**: 全件格納(設計通り、無制限)。 +- **対応**: **Java 維持**。COBOL 側の実装上限制(設計未記載のため、COBOL の制限を「仕様」とみなすか要合意)。 + +### C 類:設計書未規定/データ慣習 + +#### C‑1. ZAN03CHK — 休日フラグ値の指定欠落 +- **ファイル:行**: `cobol/ZAN03CHK.cbl:385` `IF WRK-HD-FLG(...) = '1'` +- **COBOL 挙動**: 休日カレンダ(ZAN03R03.DAT) のフラグが `'1'` のとき `OVT-TYPE='H'`。 +- **設計指定**: 設計書は「休日→'H'/平日→'W'」の**出力セマンティクスのみ**規定し、**入力フラグ値('1'/'H'/他)を一切規定せず**。 +- **Java 挙動(修正後)**: `'1'` と `'H'` の両方を受容(運用互換)。 +- **COBOL 側問題**: COBOL が `'1'` を前提としており、設計書がフラグ値を規定していない**設計書側の欠落**。COBOL 自体は「運用慣習通り」で動く。 +- **対応**: 設計書へ「フラグ='1' で休日」と追記推奨。Java は両対応済。 + +### D 類:条件付きリスク(COBOL 側潜在不備) + +#### D‑1. KIN06CLD — 休日取込上限 50 件 +- **ファイル:行**: `cobol/KIN06CLD.cbl` `1200HOLIDAYSOR`(SELECT 全件 ORDER BY、但し `HOLIDAY-COUNT < 50` で打切り) +- **COBOL 挙動**: 休日を最大 50 件までしかロード。対象月の休日が 50 件目以降に位置する場合、一部が「出勤日」と判定される。 +- **Java 挙動**: 対象月のみロード(同様に 50 件上限)。 +- **影響**: 通常 <50 件で無影響。休日マスタが 50 件を超える環境では双方にリスク(COBOL 由来の制約)。 +- **対応**: 本番マスタ件数を確認;超える場合は COBOL/Java 双方で上限緩和を要検討。 + +--- + +## 3. Java 側真缺陷(本調査で発見・既に修正済) + +参考として、COBOL ではなく **Java 側にあった真の缺陷**を記録(いずれも修正完了)。 + +| 項目 | ファイル | 修正内容 | +|------|----------|----------| +| ZAN05 丸め | Sub05TimSub.java | `Math.round` → `Math.floor`(設計=0.1h 切捨) | +| KIN07 未知休暇型 | Kin07DaiMain.java | 未知型を無視(W02 非出力)に変更 | +| Kin03Rec 数値埋込 | dto/Kin03Rec.java | `padRight` → `padLeftZero`(前ゼロ)+ ConvUtil に `padLeftZero(String,int)` 追加 | +| ZAN03 フラグ互換 | Zan03ChkMain.java | フラグ `'1'` と `'H'` の両方を受容 | + +--- + +## 4. 設計書自身の矛盾(COBOL/Java 双方に関係) + +1. **KIN09CSV 詳細設計書**: PARM 表(L33) `FULL=日別+月次` に対し、§3 見出し「SHORT 時のみ追加処理(2500‑SHORT‑MODE)」が逆のニュアンス。正規定義は PARM 表。→ Java は PARM 表準拠(正)。 +2. **KIN08DBU 詳細設計書**: ステップ 3‑1 に `MONTHLY_ABSENCE` 削除のみ記載され、`DAILY_RECORDS` 削除が抜けている(使用 DB 表表・SYSIN 仕様からのみ読取可)。→ Java は両削除(正)。 +3. **ZAN03CHK**: 休日フラグ値の規定なし(C‑1)。 + +--- + +## 5. 推奨対応方針 + +- **A 類 (KIN01/04/05)**: COBOL バグ。Java は設計準拠で正 → **Java 維持**。COBOL 側をレガシー不備として本ファイルに記録。業務確認の上、COBOL を「仕様通り(バグ有)」で据え置くか別途 COBOL 修正するか決定。 +- **B 類 (KIN09/KIN08)**: COBOL が設計から乖離。Java は設計準拠 → **Java 維持**。COBOL 側を不備記録。**厳密な COBOL 出力パリティ(並行運用比較等)が必要な場合のみ**、Java に「COBOL 互換モード」フラグを検討(要合意)。 +- **C 類 (ZAN03)**: 設計書へフラグ値('1')を追記。Java は両対応済。 +- **D 類 (KIN06)**: 本番休日マスタ件数を確認し、50 件超なら双方で上限緩和。 +- **設計書矛盾**: 上記 3 点を設計書へ反映(設計側修正)。 + +--- +*本ファイルは調査記録であり、コード変更は別途チケット/PR で管理すること。COBOL 側マーキング箇所は各 `.cbl` 内の `COBOL-DEFICIENCY:` コメントを参照。* diff --git a/JavaSrc/doc/Java测试基准说明.md b/JavaSrc/doc/Java测试基准说明.md new file mode 100644 index 0000000..1fe218e --- /dev/null +++ b/JavaSrc/doc/Java测试基准说明.md @@ -0,0 +1,362 @@ +# Java版 测试基准说明 + +> **出典**: COBOL测试基准说明.md(程序分类及测试基准.pdf 第2部)のJava转换工程向け适配版 +> **用途**: 步骤5【验证】制定每轮测试计划时使用,结果记入对照表《验证结果》。 + +## 适用性标注 + +| 记号 | 意味 | +|---|---| +| ✅ | 适用(本轮或该类型程序出现即实施) | +| ⚠️ | 部分适用・等价确认 | +| ➖ | 当前不适用(附复活条件) | +| ◎ | 必须测试(来自原基准) | +| ○ | 推荐测试(来自原基准) | + +## 一、主机概念 ↔ Java 对应 + +| 主机概念 | Java等价物 | 确认方法 | +|---|---|---| +| JOB / JOBLOG | java命令执行 + 标准输出重定向 | 日志文件 `data\test_程序名.log` 保存检查 | +| RETURN-CODE = 0 | 进程退出码 0 | `echo %ERRORLEVEL%` | +| ABEND Uxxxx / STOP RUN | System.exit(999),Windows实际退出码=999%256=**231** | ERRORLEVEL=231 + ABEND消息 | +| OPEN FILE STATUS 35 | FileNotFoundException → UncheckedIOException,JVM退出码1 | 异常堆栈 + ERRORLEVEL≠0 | +| FILE STATUS 各值 | 异常机制 / 定长DTO的结构保证 | 见各行基准 | +| SYSOUT | 标准输出 | 与JOBLOG同一控制台流 | +| FB/VB顺序文件 | 文本文件(UTF-8无BOM),1行=1记录;VB不输出RDW前缀 | fc 比对 | +| VSAM(KSDS/RRDS/ESDS) | 未出现(出现时另行定义映射) | — | +| EBCDIC/SJIS编码变换 | 不适用:本工程统一UTF-8 | — | + +--- + +## 二、通用执行手顺 + +``` +1. compile.bat → Compile OK. +2. 准备输入 data\<用例>.csv → 每用例一组文件 +3. java -Dfile.encoding=UTF-8 -cp out 程序名 > data\test_程序名.log 2>&1 +4. echo %ERRORLEVEL% → 记录退出码 +5. fc /b data\期望文件.txt data\实际输出.txt → 每个输出文件逐一比对 +6. 结果记入对照表《验证结果》 +``` + +### Java侧特有注意事项(与主机行为的固有差异,测试时必须意识到) + +| # | 差异点 | 内容 | 对测试的影响 | +|---|---|---|---| +| J1 | 退出码 | 有两条ABEND路径:① 文件I/O异常(IOException)→UncheckedIOException→JVM默认退出码1;② 业务逻辑ABEND(SUB01DAT失败等)→abendPara()→System.exit(999)→Windows实际231 | 路径①期望退出码1;路径②期望231+ABEND CODE消息 | +| J2 | 行分隔符 | println在Windows输出CRLF(\\r\\n) | 期望文件须同方式生成后fc比对 | +| J3 | BOM | OutputStreamWriter("UTF-8")不写BOM | fc /b首字节应为记录内容 | +| J4 | 字符vs字节 | Java substring/padRight按char计;PIC X(n)按byte计 | 全角混入时定长记录字节数会变(数据限半角方针;VF-A002单独验证行为) | +| J5 | 数值溢出 | int溢出静默绕回≠COBOL高位截断 | NP-A004必测 | +| J6 | SIZE ERROR | Java无自动ON SIZE ERROR分支,转换时须显式实现 | NP-N003必查实现存在性 | +| J7 | WRITE异常 | PrintWriter吞IOException(checkError) | 磁盘满等异常无法检测→COM-A005不适用理由之一 | +| J8 | 输出覆盖 | FileOutputStream已有输出时覆盖写入 | 幂等性RR-N001天然满足;主机侧再执行需先删文件 | +| J9 | BigDecimal精度 | BigDecimal运算的scale/precision须与式样书指定的PIC(n)V9(m)完全一致 | NP-N002必查:scale=m、intLen=n、ROUNDING=HALF_UP,三者缺一不可 | + +--- + +## 三、正常系通用基准(对应2.1.1) + +| 编号 | 项目 | 内容 | 优先级 | 适用 | Java中的做法 | +|---|---|---|---|---|---| +| COM-N001 | 最小数据(1条) | 各输入文件1条 | ◎ | ✅ | 每程序至少1组单件用例 | +| COM-N002 | 标准数据(多条) | 正常业务量 | ◎ | ✅ | 正常业务量用例(如KIN01INP的7件) | +| COM-N003 | 最大数据(上限) | 定义上限(含性能) | ○ | ✅ | 生成脚本造大量数据测性能(10万件) | +| COM-N004 | 全部输出文件存在 | 期望输出文件全部生成 | ◎ | ✅ | dir确认 + 件数消息比对 | +| COM-N005 | 输出文件内容确认 | 与期望值完全一致 | ◎ | ✅ | fc /b 逐字节比对每个输出文件 | +| COM-N006 | 输出记录长度确认 | 与规格一致 | ◎ | ✅ | 定长记录字节数检查(80B + 换行符);期望文件须同方式生成(J2) | +| COM-N007 | 输出排序确认 | 键序正确 | ◎ | ⚠️ | 仅当程序有排序/键序输出时适用 | +| COM-N008 | JOBLOG确认 | 日期/件数/结束码 | ◎ | ✅ | 控制台日志保存为 data\test_程序名.log,确认开始/件数/终了消息与退出码0 | + +--- + +## 四、异常系通用基准(对应2.1.2) + +| 编号 | 项目 | 内容 | 期望结果 | 优先级 | 适用 | Java中的做法 | +|---|---|---|---|---|---|---| +| COM-A001 | 文件不存在 | 不存在文件 | OPEN STATUS 35 ABEND | ◎ | ✅ | 文件读取异常→UncheckedIOException→退出码1;业务ABEND→exit(999)→退出码231(两条路径见J1) | +| COM-A002 | 全部0件 | 所有输入为空 | 正常结束 输出0条 | ◎ | ✅ | 空输入文件→正常结束、0条输出、件数消息000000000 | +| COM-A003 | 部分0件 | 部分文件为空 | 正常结束 | ◎ | ✅ | 多输入文件时部分为空→正常结束 | +| COM-A004 | 记录长度不一致 | LRECL不匹配 | ABEND/STATUS错误 | ◎ | ⚠️ | 行式读取无LRECL强制:超长→按PIC长度substring截断;不足→padRight补齐;须确认与原程序转换语义一致 | +| COM-A005 | 磁盘满 | 容量不足 | 顺序文件STATUS 34 | ○ | ➖ | PrintWriter吞IOException(J7),磁盘满无法检测。原则上不实施 | +| COM-A006 | OPEN模式违反 | 只读文件WRITE | STATUS 37 | ◎ | ➖ | Java编译期/读写模式保证,结构上不可能发生 | +| COM-A007 | 再OPEN | OPEN中再OPEN | STATUS 41 | ◎ | ➖ | openFiles()仅一处调用,代码审查确认 | +| COM-A008 | 异常结束码 | Uxxxx确认 | 与期望一致 | ◎ | ✅ | ERRORLEVEL=231(exit(999))+ ABEND CODE消息确认 | + +--- + +## 五、确认项通用基准(对应2.1.3) + +| 编号 | 确认项 | 确认内容 | 优先级 | 适用 | 确认方法 | +|---|---|---|---|---|---| +| COM-C001 | 程序开始 | 程序名/日時/版本 | ◎ | ✅ | 日志确认 MSG[001] 开始消息 + P5=程序ID | +| COM-C002 | OPEN确认 | 各文件OPEN正常 | ◎ | ⚠️ | 无显式OPEN日志;通过正常读取间接确认(或OPEN失败异常) | +| COM-C003 | 输入件数 | READ件数 | ◎ | ✅ | MSG[006] 9位件数 vs 输入文件行数(PowerShell `(Get-Content file).Count`;注意含空行记录) | +| COM-C004 | 输出件数 | WRITE件数 | ◎ | ✅ | MSG[007]×2 vs 输出文件行数(同上方法;空行也计入) | +| COM-C005 | 正常结束 | RETURN-CODE=0 | ◎ | ✅ | ERRORLEVEL=0 + MSG[002] 终了消息 | +| COM-C006 | 异常结束 | 错误码输出 | ◎ | ✅ | ERRORLEVEL≠0 + ABEND CODE消息 | +| COM-C007 | 处理时间 | 作业经过时间 | ◎ | ⚠️ | 可选:日志时刻差 / java启动~退出时刻差 | +| COM-C008 | SYSOUT | 报表输出确认 | ◎ | ✅ | 控制台输出本身(stdout重定向日志) | + +--- + +## 六、类型别基准(对应2.2) + +### 6.1 条件分支系测试(IF / EVALUATE) + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| B-N001 | 正常 | IF 2路分支(ELSE) | IF条件成立/不成立各处理 | ◎ | ✅ | isStatusActive()/isStatusCancel()成立/不成立两侧各走一组数据 | +| B-N002 | 正常 | IF 3路分支(ELSE IF) | ELSE IF链式分支 | ◎ | ✅ | majPara()中 fieldCount≠8 / ltFound≠1 / status判定三级链,各走一组 | +| B-N003 | 正常 | IF 复合条件 AND/OR | 多个条件组合 | ◎ | ✅ | validatePara()中 sDate≠0 AND eDate≠0 AND sTime≠0 及 sDate>eDate||...复合(Q5已改CSV项目) | +| B-N004 | 正常 | IF 88-level条件名 | 88条件直接判断 | ◎ | ✅ | isStatusActive()/isStatusCancel()两侧各走一组 | +| B-N005 | 正常 | IF 嵌套3层 | 内部IF引用外部IF变量 | ◎ | ✅ | majPara() → if/else if/else 嵌套3层已实现 | +| B-N006 | 正常 | EVALUATE WHEN 多值 | 多个WHEN分支 | ◎ | ✅ | errorPara() switch(wrkErrType):case F/L/S/default 四分支 | +| B-N007 | 正常 | EVALUATE ALSO | 多条件组合判断 | ◎ | ⚠️ | 本程序未出现。复活条件:出现多条件组合判定 | +| B-N008 | 正常 | EVALUATE TRUE/FALSE | 条件表达式判定 | ◎ | ⚠️ | 本程序未出现。复活条件:出现EVALUATE WHEN条件表达式时 | +| B-N009 | 正常 | EVALUATE WHEN OTHER | 默认分支覆盖 | ◎ | ✅ | errorPara() switch的default分支 | +| B-N010 | 正常 | CONTINUE / NEXT SENTENCE | 空分支处理 | ○ | ✅ | Java→continue/空块(代码审查确认无遗漏) | +| B-A001 | 异常 | IF未闭合(缺少END-IF) | 编译错误 | ◎ | ✅ | javac保证语法正确(编译期自动) | +| B-A002 | 异常 | EVALUATE无WHEN OTHER | 全不匹配→无操作 | ○ | ⚠️ | 代码审查确认各switch均有default/完整case覆盖 | +| B-R001 | 确认 | COBOL 85条件表达式 | 关系运算符全部覆盖 | ◎ | ✅ | 逐条核对每个判定中的 = / != / > / >= / < / <= 均有测试数据覆盖 | + +### 6.2 内部表检索测试(SEARCH / SEARCH ALL) + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| T-N001 | 正常 | SEARCH ALL 等值查找(找到) | KEY等于目标存在 | ◎ | ➖ | 复活条件:出现SEARCH ALL(二分检索) | +| T-N002 | 正常 | SEARCH ALL 等值查找(未找到) | KEY等于目标不存在 | ◎ | ➖ | 同上 | +| T-N003 | 正常 | SEARCH ALL 多键查找 | 主键+副键组合 | ◎ | ➖ | 同上 | +| T-N004 | 正常 | SEARCH 顺序查找 | KEY条件不唯一用SEARCH | ○ | ✅ | leavserPara() for循环+break,找到/未找到各一组 | +| T-N005 | 正常 | OCCURS 1件表 | 表只有1条时检索 | ◎ | ⚠️ | 复活条件:出现OCCURS DEPENDING ON或固定表边界测试 | +| T-N006 | 正常 | OCCURS DEPENDING ON | 可变长表边界检索 | ◎ | ⚠️ | 复活条件:出现OCCURS DEPENDING ON | +| T-N007 | 正常 | INDEXED BY索引操作 | SET/SEARCH ALL 使用 INDEX | ◎ | ✅ | Java→for循环索引idx,0<=idx或反射) | +| C-N004 | 正常 | CALL+RETURN-CODE | RETURN-CODE设置和检查 | ◎ | ✅ | 子程序执行后检查DTO返回码字段(getC01ChkRrc/getD01FkiCod等) | +| C-N005 | 正常 | CALL+EXIT PROGRAM | 子程序正常返回 | ◎ | ✅ | execute()方法正常return | +| C-N006 | 正常 | CALL+GOBACK | GOBACK返回(含RETURN-CODE) | ◎ | ✅ | 同上(Java无GOBACK概念,方法return即可) | +| C-N007 | 正常 | IS INITIAL指定程序 | 每次CALL时WS初始化确认 | ◎ | ✅ | 调用前reset()确认(各main段均有reset) | +| C-N008 | 正常 | 非IS INITIAL程序 | CALL跨调用WS值保持 | ◎ | ✅ | 子程序static无状态时N/A;有WS字段时字段保持确认 | +| C-N009 | 正常 | CALL多层嵌套(A→B→C) | 3层CALL传递参数 | ○ | ✅ | KIN01INP→SUB04CHK两层已验证;更深层出现时追加 | +| C-A001 | 异常 | CALL不存在的程序 | 未加载→ABEND | ◎ | ✅ | Java→编译错(javac保证类存在);等价确认 | +| C-A002 | 异常 | CALL USING参数长度不匹配 | CALL侧与LINKAGE侧长度不同 | ◎ | ✅ | DTO类型编译期保证;字段长度在DTO中padRight固定 | +| C-A003 | 异常 | CALL CANCEL后再CALL | CANCEL→INITIAL状态 | ◎ | ⚠️ | 本程序未出现。复活条件:出现子程序复用场景 | +| C-A004 | 异常 | CALL递归调用自身 | 需RECURSIVE编译选项 | ○ | ➖ | 复活条件:出现递归调用需求 | +| C-R001 | 确认 | CALL前STOP RUN | CALL前正常结束 | ◎ | ✅ | termPara()正常返回→main结束→exit(0) | +| C-R002 | 确认 | CALL后RETURN-CODE检查 | 子程序的返回码正确传递 | ◎ | ✅ | SUB01DAT的getD01FkiCod、SUB04CHK的getC01ChkRrc均检查 | + +### 6.4 校验测试(SUB04CHK类) + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| VF-N001 | 正常 | 字段校验通过 | 输入值在允许范围内→正常输出 | ◎ | ✅ | 有効申请5项检查全通过→W01输出 | +| VF-N002 | 正常 | 字段校验拒绝 | 输入值异常→错误输出文件 | ◎ | ✅ | EMP-ID/START-DATE/START-TIME/END-DATE/END-TIME各NG→W02 ERROR-LOG | +| VF-N003 | 正常 | 重复检测(正常) | 首次出现该键→通过 | ◎ | ⚠️ | 本程序无重复检测(无排序键比较逻辑) | +| VF-N004 | 正常 | 重复检测(重复) | 同一键第二次出现→错误输出 | ◎ | ⚠️ | 同上 | +| VF-N005 | 正常 | 半角长度20桁 | 半角20桁以内→通过 | ◎ | ✅ | padRight固定长字段宽度=半角桁数,已实现 | +| VF-N006 | 正常 | 半角长度4桁 | 半角4桁以内→通过 | ◎ | ✅ | 同上 | +| VF-A001 | 异常 | 半角超长(21桁) | 半角21桁→截断/错误 | ◎ | ✅ | 须确认超长输入时截断/报错语义(padRight不截断——代码审查确认转换语义) | +| VF-A002 | 异常 | 全角混入 | 半角字段全角文字进入PIC X(半角)域 | ◎ | ✅ | 结合J4(字符vs字节差异):全角占2-3字节,定长记录字节数超长;须明确原程序转换语义 | +| VF-R001 | 确认 | 错误件数计数 | 正常件+错误件=输入件数 | ◎ | ✅ | 输入7件=W01输出3件+W02输出4件;全用例均满足 | + +### 6.5 CSV→FB转换测试(无换行 / 有换行) + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| CF-N001 | 正常 | 无换行(STRING合并) | 多项目合1记录 | ◎ | ✅ | toRecordLine()输出80B定长(Kin01Rec/Kin05Rec) | +| CF-N002 | 正常 | 有换行(改行展开) | 1行入力→多行出力 | ◎ | ⚠️ | 复活条件:出现含改行展开的CSV转换程序 | +| CF-N003 | 正常 | 项目内逗号/引号 | 分隔符处理 | ◎ | ⚠️ | UNSTRING不识别引号;Java split同样不识别=忠实等价;须确认原程序式样 | +| CF-N004 | 正常 | 引号内字符串 | "..."内逗号/改行保持 | ◎ | ⚠️ | 同上(若原程序支持引号解析,须在转换规则中单独实现) | +| CF-N005 | 正常 | 空项目连续逗号 | 空字符串处理 | ◎ | ✅ | split(",",-1)保持尾部空串(UNSTRING TALLYING语义) | +| CF-N006 | 正常 | 全项目最大长 | 各项目到PIC定义上限 | ◎ | ✅ | padRight补齐最大宽度,字符串长度不超过PIC(n) | +| CF-A001 | 异常 | 改行位置超记录长 | 改行展开后1行超定义长 | ◎ | ✅ | padRight截断/补位语义与COBOL一致(String=n char天然保持) | +| CF-A002 | 异常 | 引号未闭合 | 剩余全部为1项目 | ◎ | ⚠️ | 同CF-N003/004;原程序不识别引号→忠实等价 | + +### 6.6 文件编成测试 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| FO-N001 | 正常 | 顺次读取 | 先头→最终全件正常READ | ◎ | ✅ | BufferedReader.readLine() 逐行读至null | +| FO-N005 | 正常 | LINE SEQUENTIAL读取 | 改行区切り文本文件READ | ◎ | ✅ | 同上(Java文本文件即LINE SEQUENTIAL) | +| FO-N002~004 | 正常 | VSAM(KSDS/RRDS) | 索引文件/相对文件 | ◎ | ➖ | 复活条件:出现索引/相对文件需求 | +| FO-A001~004 | 异常 | VSAM异常 | OPEN/READ/WRITE异常 | ◎ | ➖ | 复活条件:出现VSAM文件 | +| FO-R001 | 确认 | FILE STATUS确认 | 各I/O后STATUS值 | ◎ | ⚠️ | Java无FILE STATUS;通过异常机制等价:IOException/UncheckedIOException | + +### 6.7 数值精度补充说明 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| NP-N001 | 正常 | COMP-3小数位对齐 | PIC S9(7)V99与S9(9)V9的ADD对齐 | ◎ | ✅ | COMP-3→long整数运算;有小数时→BigDecimal | +| NP-N002 | 正常 | ROUNDED选项 | 计算结果四舍五入到指定小数位 | ◎ | ✅ | setScale(..., RoundingMode.HALF_UP) 一致性确认 | +| NP-N003 | 正常 | ON SIZE ERROR | 结果超过PIC定义域时触发 | ◎ | ⚠️ | J6:Java无自动SIZE ERROR;转换时须显式实现(throw/标记);必查实现存在性 | +| NP-N004 | 正常 | 除算余数(REMAINDER) | DIVIDE...REMAINDER | ◎ | ⚠️ | 复活条件:出现DIVIDE REMAINDER;Java→BigInteger.remainder() | +| NP-N005 | 正常 | COMP(二进制)符号处理 | S9(4) COMP的符号位正确 | ◎ | ✅ | int/long天然有符号(补码) | +| NP-N006 | 正常 | COMPUTE中间精度 | 中间结果精度确保最终结果正确 | ◎ | ✅ | BigDecimal运算顺序注意(scale/precision) | +| NP-N007 | 正常 | 符号付/符号なし混算 | S9(5)与9(5)的ADD/SUBTRACT | ○ | ⚠️ | Java无符号short/byte→全部用int/long(补码),混算语义与COBOL不完全等价;须按转换规则实现 | +| NP-A001 | 异常 | 超过PIC定义域(无SIZE ERROR) | 高位截断→数据错误 | ◎ | ✅ | padLeftZero位数语义与COBOL一致(9位零填充等) | +| NP-A002 | 异常 | COMP-3符号误判 | 符号nibble('C'/'D')不一致 | ◎ | ➖ | Java无pack/unpack自写代码(ConvUtil统一转换,无nibble操作) | +| NP-A003 | 异常 | 除算0除 | DIVIDE BY ZERO→ABEND | ◎ | ✅ | ArithmeticException≈ABEND;须在转换中显式捕获或检查除数 | +| NP-A004 | 异常 | COMP赋值溢出 | 超范围数值MOVE到COMP字段 | ◎ | ⚠️ | J5:int溢出静默绕回≠COBOL截断;必测溢出用例 | +| NP-R001 | 确认 | 演算结果精度一致 | 同一演算反复执行→结果一致 | ◎ | ✅ | 多次运行同一用例输出一致(幂等性+无随机因素) | +| NP-R002 | 确认 | 小数点位置一致 | 编辑字段十进制位正确 | ◎ | ✅ | padLeftZero / 编辑输出字段的小数点位置与COBOL一致 | + +### 6.8 循环处理测试 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| LP-N001 | 正常 | PERFORM VARYING(计数循环) | VARYING I FROM 1 BY 1 UNTIL I > N | ◎ | ✅ | Java→for循环 | +| LP-N002 | 正常 | PERFORM UNTIL(条件循环) | 条件达到后退出 | ◎ | ✅ | Java→while | +| LP-N003 | 正常 | PERFORM TIMES(固定次数) | N回执行 | ◎ | ✅ | Java→for(0..N) | +| LP-N004 | 正常 | PERFORM THRU(段落跨入) | THRU指定多个段落 | ○ | ✅ | Java→顺序方法调用(重构方针下THRU已拆分) | +| LP-N005 | 正常 | 内联PERFORM(END-PERFORM) | COBOL85内联循环 | ◎ | ✅ | Java→while/for | +| LP-N006 | 正常 | 嵌套PERFORM(3层) | 内层PERFORM完整执行后外层继续 | ◎ | ✅ | Java→嵌套for/while(检查嵌套≤3层) | +| LP-N007 | 正常 | EXIT PERFORM / EXIT PERFORM CYCLE | 中途退出/跳过本次循环 | ◎ | ✅ | Java→break/continue | +| LP-A001 | 异常 | PERFORM VARYING 0回 | 初始值已超终值→0回执行 | ◎ | ✅ | Java→for条件初值已超终值→循环0次 | +| LP-A002 | 异常 | 无限循环(条件永不满足) | UNTIL条件永不成立→超时 | ○ | ⚠️ | Java→while(true)无退出时;超时观察○(本程序无此风险) | +| LP-A003 | 异常 | PERFORM嵌套越界 | 内层PERFORM THRU跨入外层段落 | ◎ | ➖ | 重构方针下不存在(方法拆分隔离) | +| LP-A004 | 异常 | EXIT SECTION/GOBACK误用 | 循环内意外退出程序 | ◎ | ➖ | Java无SECTION概念;方法return不会意外退出主程序 | +| LP-R001 | 确认 | 循环回数确认 | 实际执行回数与期望一致 | ◎ | ✅ | 计数器(cunR01Inn等)/ 日志确认;或循环中加临时计数器验证 | + +### 6.9 日期处理测试 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| D-N001 | 正常 | 通常日期(YYYYMMDD) | 正常日期处理 | ◎ | ✅ | Java→int/String YYYYMMDD;SUB04CHK DATE检查 | +| D-N002 | 正常 | 闰年判定 | 2000/2004/2100年 | ◎ | ✅ | SUB04CHK DATE内含闰年判定逻辑 | +| D-N003 | 正常 | 月末日 | 1月31日/2月28日/2月29日 | ◎ | ✅ | 同上(SUB04CHK DATE) | +| D-N004 | 正常 | 日期计算(±日) | FUNCTION INTEGER-OF-DATE | ◎ | ⚠️ | 复活条件:出现日期加减计算 | +| D-N005 | 正常 | 日期比较 | 日期前后比较 | ○ | ✅ | validatePara() sDate>eDate 已比较;各种比较语义确认 | +| D-A001 | 异常 | 日期逻辑异常 | 2月30日/4月31日 | ◎ | ✅ | 测试数据开始月=13等异常日期→ERROR(已测:KIN01R01.csv #6) | +| D-A002 | 异常 | 西历/和历混在 | YYYYMMDD与RYYMMDD混用 | ◎ | ➖ | 本工程统一YYYYMMDD;复活条件:出现和历处理程序 | +| D-A003 | 异常 | 2桁年(YYMMDD) | 2000年区分问题 | ◎ | ➖ | 本工程统一4桁年;复活条件:出现2桁年处理 | + +--- + +## 七、不适用类别清单(当前未出现,出现时按原基准全面启用) + +| 类别编号 | 类别 | 复活条件(该类型程序出现时) | 主要测试项摘要 | +|---|---|---|---| +| MT-* | 匹配基础测试 | 出现匹配处理程序(主从件键对应) | MT-N001~006正常+异常、MT-R001匹配件数合计 | +| SY-* | SYSIN读取测试 | 出现批处理参数读取(main(args)/系统属性) | SY-N001~003正常、SY-A001~003异常 | +| S-* | 分割系测试 | 出现文件分割程序 | S-N001~007正常(50/25/100分割)、S-R001~002确认 | +| KB-* | key切测试 | 出现汇总/聚合/中断处理程序 | KB-N001~006正常、KB-A001~002异常 | +| SR-* | SORT类型测试 | 出现排序处理(Arrays.sort+Comparator) | SR-N001~008正常(升序/降序/多键/INPUT PROCEDURE)、SR-A001~003异常 | +| MR-* | MERGE类型测试 | 出现多文件合并 | MR-N001~003正常、MR-A001异常 | +| ED-* | 编辑输出测试 | 出现报表类程序(改行/改页/编辑字段) | ED-N001~005正常、ED-A001~002异常、ED-R001行数合计 | +| AE-* | ASCII↔EBCDIC编码转换 | 出现编码转换需求(本工程统一UTF-8) | AE-N001~003正常、AE-A001~003异常、AE-R001转换后长度 | +| OL-* | online/CICS测试 | 出现在线处理程序(DFHCOMMAREA/MAP) | OL-N001~004正常、OL-A001~003异常、OL-R001响应时间 | +| DB-* | DB操作/检索/SELECT条件 | 出现DB处理程序(已备DbSample模式) | DB-N001~006正常、DB-A001~003异常、DB-R001 SQLCODE | +| AM-* | 高级匹配测试 | 出现多段匹配/混合处理程序 | AM-N001~008正常、AM-A001~002异常、AM-R001各段件数 | + +--- + +## 八、横跨功能测试基准(对应2.3) + +### 8.1 可变长入出力 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| VL-N003 | 正常 | LINE SEQUENTIAL文件读写 | 行可变长文件(LF结尾)逐行READ | ◎ | ✅ | BufferedReader.readLine() 逐行读 | +| VL-N005 | 正常 | 可变长→固定长转换 | 可变长记录READ到固定长记录域 | ◎ | ✅ | CSV行→80B定长记录(toRecordLine) | +| VL-N001~004 | 正常 | RECORD IS VARYING | 可变长记录直接读写 | ◎ | ⚠️ | 复活条件:出现COBOL RECORD VARYING(Java无此概念;按行式文本文件等价处理) | +| VL-N006 | 正常 | RECORD VARYING FROM 1 TO 99 | 最小~最大件数之间的任意长度 | ◎ | ⚠️ | 同上 | +| VL-A001~004 | 异常 | ODO/VARYING异常 | 超限/截断/STATUS 47 | ◎ | ➖ | 复活条件:出现OCCURS DEPENDING ON | +| VL-R001 | 确认 | RECORD LENGTH确认 | WRITE后文件LRECL与定义一致 | ◎ | ✅ | 输出行字节数检查(80B定长 + 换行符) | + +### 8.2 数值精度 + +见6.9(NP-*系列详细表) + +### 8.3 日文文字处理 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| J-K004 | 异常 | 全角/半角混用 | 混用数据处理 | ◎ | ✅ | 结合J4:全角占2-3字节,定长记录字节数超长;须明确转换语义 | +| J-S001 | 正常 | 全角空格TRIM | TRIM对全角空格 | ◎ | ⚠️ | Java trim()仅去半角空格;全角空格须正则替换(转换规则决定) | +| J-D001~004 | 正常 | Shift-JIS 5C/7C | 特殊字节处理 | ◎ | ➖ | 本工程统一UTF-8无SJIS处理 | +| J-X系列 | 正常/异常 | EBCDIC↔SJIS转换 | 编码转换 | ◎ | ➖ | 本工程统一UTF-8 | +| J-N/G/S系列 | 正常/异常 | PIC N/外字/全角空格 | 全角字段/外字/编辑 | ◎ | ➖ | 复活条件:出现PIC N字段或外字需求 | + +### 8.4 RERUN / RESTART 测试 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| RR-N001 | 正常 | 同一数据再执行 | 幂等性确认(不重复更新) | ◎ | ✅ | 同一输入再执行一次,输出文件完全一致(J8:覆盖模式) | +| RR-A001 | 异常 | 输出文件未删除 | 再执行STATUS 95 | ◎ | ⚠️ | Java→FileOutputStream覆盖=正常结束(与主机STATUS95差异已记录;须确认转换规则) | +| RR-A002 | 异常 | 中间ABEND→RESTART | 中间状态重新开始 | ◎ | ➖ | 复活条件:出现检查点/重启机制需求 | +| RR-A003 | 异常 | ABEND时输出文件状态 | 中途废弃/部分输出 | ◎ | ⚠️ | Java→ABEND后已输出行保留(未close时flush未保证);测试确认部分输出行为 | + +### 8.5 排他/竞争测试 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| EX-N001 | 正常 | 同时READ | 多个JOB同时READ | ◎ | ➖ | 单进程批处理对象外;复活条件:出现多实例并发需求 | +| EX-A001~003 | 异常 | WRITE冲突/DELETE | STATUS 48/95/97 | ◎ | ➖ | 同上 | + +### 8.6 性能/容量测试 + +| 编号 | 区分 | 项目 | 内容 | 优先级 | 适用 | Java确认方法 | +|---|---|---|---|---|---|---| +| PV-N001 | 正常 | 10万件处理 | 批量处理性能 | ○ | ✅ | 生成脚本造10万条CSV,记录处理时间(time命令) | +| PV-A001 | 异常 | SORT区不足 | RC=20 | ○ | ➖ | 复活条件:出现SORT程序 | + +--- + +## 九、测试计划制定方法(每轮固定手顺) + +1. **确定该程序类型**:文件处理/匹配/分割/SORT/DB等,按第六节选对应类别 +2. **选必测底座**:第三节~第五节的◎项 = 每轮必测(COM-N001/002/004/005/006/008,COM-A001/002/004/008,COM-C001/003/004/005/008) +3. **选类型别适用项**:第六节按程序类型勾选适用行(✅必测,⚠️确认等价,➖跳过并注明) +4. **选横跨适用项**:第八节勾选适用行 +5. **派生数据用例**:每个基准项派生具体输入数据+期望输出,形成测试计划表 +6. **执行**:按第二节手顺运行 +7. **记录**:结果记入对照表《验证结果》 + +### 测试计划表模板 + +| 用例ID | 基准编号 | 目的 | 输入数据 | 期望结果 | 实际结果 | 结果 | +|---|---|---|---|---|---|---| +| TC001 | COM-N001 | 最小数据1条 | 1条正常记录 | W01输出1件,内容一致 | | OK/NG | +| TC002 | COM-A001 | 文件不存在 | 删除R01输入文件 | 异常+退出码≠0 | | OK/NG | +| ... | ... | ... | ... | ... | | | + +--- + +## 附录:与COBOL测试基准的ID映射速查 + +| COBOL原ID | Java对应ID | 说明 | +|---|---|---| +| COM-N001~008 | COM-N001~008 | 通用正常系(全保留,按适用性筛选) | +| COM-A001~008 | COM-A001~008 | 通用异常系(部分降级为等价行为) | +| COM-C001~008 | COM-C001~008 | 通用确认项(SYSOUT→stdout) | +| B-N/A/R | B-N/A/R | 条件分支(全保留) | +| T-N/A/R | T-N/A/R | 内部表检索(保留适用项) | +| C-N/A/R | C-N/A/R | 子程序调用(保留适用项) | +| VF-N/A/R | VF-N/A/R | 校验测试(保留适用项) | +| CF-N/A/R | CF-N/A/R | CSV转换(保留适用项) | +| FO-N/A/R | FO-N/A/R | 文件编成(仅LS适用) | +| LP-N/A/R | LP-N/A/R | 循环处理(全保留) | +| NP-N/A/R | NP-N/A/R | 数值精度(保留适用项) | +| D-N/A | D-N/A | 日期处理(保留适用项) | +| VL-N/A/R | VL-N/A/R | 可变长(LS相关项适用) | +| RR-N/A | RR-N/A | RERUN(幂等性适用) | +| PV-N | PV-N | 性能(10万件○) | +| MT/SY/S/KB/SR/MR/ED/AE/OL/DB/AM | (不适用) | 按第七节复活条件启用 | + +--- + +**本文档基准版本**: v1.0(2026-08-22 作成) +**来源文档**: COBOL测试基准说明.md(程序分类及测试基准.pdf 第2部) +**适用范围**: 本项目所有COBOL→Java转换程序的步骤5验证 diff --git a/JavaSrc/doc/代码确认Checklist.md b/JavaSrc/doc/代码确认Checklist.md new file mode 100644 index 0000000..419d6a1 --- /dev/null +++ b/JavaSrc/doc/代码确认Checklist.md @@ -0,0 +1,165 @@ +# 代码确认 Checklist(模板) + +步骤6【代码确认】使用的确认模板。每轮转换时复制本文件内容到 `doc\conv\程序名_对照表.md` 中填写结果,本文件本身保持为空白基准模板。 + +本 Checklist 从七个维度审查:**机能正确性**、**规约符合性**、**可读性**、**保守性(维护性)**、**健壮性(异常处理)**、**性能与效率**、**安全性与可观测性**。 + +- **机能正确性**(一):以 `doc\design\` 详细设计书为基准,逐机能项核对实现与验证证据,并覆盖分支、边界、精度、异常路径。 +- **规约符合性**(二):按转换规则的映射条目逐条检查。 +- **可读性 / 保守性**(三 / 四):在**不破坏与 COBOL 的 1:1 对应关系**这一前提下检查;凡与忠实转换原则冲突时,以忠实转换为优先并记录理由。 +- **健壮性(五)**:异常路径、边界、资源安全、ABEND 一致性——批量程序须保证任何单笔异常不泄露为进程崩溃且资源不泄漏。 +- **性能与效率(六)**:IO/转换开销、不必要对象创建、批量规模下的稳定性。 +- **安全性与可观测性(七)**:日志内容恰当、无敏感信息泄露、权限与编码等运行前提明确。 + +任一条目 NG 时记入《不合格项记录》,修正后复检;全部通过后方可交付。 + +--- + +## 一、机能确认清单 + +> 机能项从详细设计书中提取,逐项标注实现位置和验证证据。 + +| No | 机能项(设计书出处) | 实现位置(类#方法) | 验证方式 | 结果 | +|---|---|---|---|---| +| 1 | (例:1.1 日次取込处理) | Sampl01Main#initPara | 测试数据运行比对 | OK/NG | +| 2 | | | | | + +### 正确性验证要点 + +> 机能项清单验证"实现了没有",本节验证"实现得对不对、全不全"。逐条打勾,NG 记入不合格项。 + +- [ ] 分支覆盖:设计书描述的每个判定条件至少有 1 组测试数据走过真/假两侧;COBOL 原程序的各 IF/EVALUATE 分支在 Java 中均有对应且条件等价 +- [ ] 边界值:空输入 / 最大长度 / 最小值·最大值 / 0 除 / 文件末尾记录 等边界已验证 +- [ ] 计算精度:金额类 BigDecimal 的 scale 与 ROUNDED 舍入模式与原程序一致 +- [ ] 异常路径等价:文件不存在、记录格式错、DB 访问失败等异常时,消息内容与 ABEND 行为(退出码)与原程序/设计书一致 +- [ ] 输出件数与内容:输出文件件数、汇总值、消息编号与设计书预期一致(不只看"能跑通") +- [ ] COBOL→Java 差异等价性:REDEFINES 多视图写入方式与原 COBOL 一致(整区覆写 vs 单视图写入);INITIALIZE 后 String 非 null;MOVE 定长化与原程序一致;参考修正起始位置正确 + +## 二、规范确认清单 + +### 1. 命名规范(对应规则 3.1) + +- [ ] 类名 = PROGRAM-ID 驼峰化 + Main/Sub 后缀 +- [ ] `WS-XXX` → `wsXxx` 字段,无遗漏、无改名 +- [ ] 段落 `XXX-PARA` → `xxxPara()` 方法一一对应 +- [ ] VALUE 常量 → `CST_XXX`;88 级 → 常量 + `isXxx()` 判定方法 +- [ ] COPY 句 → `dto\` 下 DTO 类 +- [ ] 文件 SELECT ASSIGN → `INP_FILE` / `OUT_FILE` 路径常量 + +### 2. 类型映射(对应规则 3.2) + +- [ ] PIC X(n) → String;PIC 9(n) → int/long;PIC 9(n)V99 → BigDecimal +- [ ] ROUNDED → setScale(…, RoundingMode.HALF_UP) +- [ ] 固定长记录 substring 的起止位置与 05 层定义一致,且有位置注释 +- [ ] PIC 转换统一调用 util.ConvUtil,无散落自写转换逻辑 + +### 3. 结构映射(对应规则 3.3 / 3.4) + +- [ ] WORKING-STORAGE → 私有字段区;段落 → private 方法 +- [ ] PERFORM UNTIL → while;PERFORM VARYING → for;THRU → 顺序方法调用 +- [ ] IF/ELSE → if/else;EVALUATE(含 WHEN OTHER)→ switch/default +- [ ] GO TO 处理符合方针:保持原结构,仅冗长时小范围重构(已记录) +- [ ] CALL 子程序 → 独立类方法调用,参数传递与原程序一致 +- [ ] OCCURS / DEPENDING ON / REDEFINES / COMP-3 按规则处理 +- [ ] 字段为 instance(每轮 new 新实例),非 static(子程序 static execute 方法内无状态字段例外) + +### 4. 文件处理 + +- [ ] OPEN/CLOSE:同段落内用 try-with-resources;跨段落(OPEN在初期处理、CLOSE在终了处理)时用字段保持+显式 close 模式 +- [ ] READ AT END → readLine() == null;READ 循环逻辑与原程序一致 +- [ ] WRITE 输出格式(定长化/编辑)与原程序报表格式一致 +- [ ] 文件编码明确(默认 UTF-8,原文件为其他代码页时已注明) +- [ ] 非 try-with-resources 模式时,closeFiles() 在所有路径(正常+异常)均被调用,无资源泄漏 + +### 5. DB 处理 + +- [ ] EXEC SQL → PreparedStatement,宿主变量 → setXxx 一一对应 +- [ ] 游标 DECLARE/OPEN/FETCH/CLOSE → executeQuery()/rs.next()/自动关闭 +- [ ] COMMIT / ROLLBACK 时机与原程序一致 +- [ ] WHENEVER SQLERROR → try-catch + rollback 模式 +- [ ] SQL 与详细设计书/DDL 的表名列名一致 + +### 6. 注释与可追溯性(对应规则 3.5) + +- [ ] 类头有 COBOL↔Java 整体结构对应说明 +- [ ] 字段/方法标注了对应的 COBOL 数据项/段落 +- [ ] 关键语句行内标注原 COBOL 语句 + +### 7. 通用约束(第四节) + +- [ ] compile.bat 编译通过 +- [ ] 源码 UTF-8;纯 JDK8,未引入新依赖 +- [ ] 提供测试数据时已运行比对,输出一致 +- [ ] 未操作项目目录以外的任何内容 +- [ ] 包结构明确:无 package 语句(默认包)时确认符合预期;有 package 时目录结构一致 +- [ ] run.bat 已适配本轮主类名(java -cp out 主类名) + +### 8. COBOL→Java 差异确认清单(对应规则 3.7) + +> COBOL 与 Java 存在系统性语义差异,转换时须逐项确认等价性。详见 `doc\转换基准.md` 3.7 节。 + +- [ ] **INITIALIZE 等价性**:String 字段显式初始化为 `""`(非 null);数值字段默认 0 或显式赋 0;setter 方法有 null 防护(`(v == null) ? "" : v`);reset() 与 COBOL INITIALIZE 逐项对应 +- [ ] **REDEFINES 视图一致性**:reset() 清零主存储(等价 INITIALIZE 整区内存归零);单视图写入(MOVE 分组项)与 COBOL 语义一致(仅修改对应位);整区覆写(MOVE 整个 REDEFINES 项)须同时写入所有视图(确认原 COBOL 的 MOVE 目标) +- [ ] **MOVE 定长化**:String 字段用 `ConvUtil.padRight` / `padLeftZero` 显式定长,不依赖 Java 自动处理;定长记录的 substring 位置与 05 层定义一致且有注释 +- [ ] **参考修正起始位置**:COBOL 参考修正 `X(1:1)` 起始位置为 1;Java `substring(0, 1)` 起始位置为 0。确认原 COBOL 是否使用参考修正,Java 实现是否正确偏移 +- [ ] **UNSTRING 字段数判定**:COBOL TALLYING 统计分隔符个数(8字段=7分隔符);Java `split(-1).length` 返回字段个数(=8)。判定逻辑须等价(`cnt+1 == length`),不可直接等同 +- [ ] **REDEFINES 整区覆写确认**:检查原 COBOL 的 MOVE 目标是整个 REDEFINES 项(如 WRK-DEMO-AREA)还是某个分组项(如 WRK-DEMO-TYPE)。整区覆写→Java 须同时 set 所有视图;单视图写入→Java 仅 set 对应视图 + +## 三、可读性确认清单 + +> 前提:不破坏与 COBOL 的 1:1 对应关系。凡为可读性而做的改动(如方法拆分、变量别名)必须已记录在对照表《差异》节中。 + +- [ ] 方法长度适当:单个段落方法过长时(目安 >80 行),已在保持段落对应的前提下拆出私有辅助方法,且拆分点有注释说明 +- [ ] 命名自明:字段/方法名见名知义;与 COBOL 名差异大的(如缩写)在注释中标注了原文名 +- [ ] 无魔数:字面量 0/1 以外的判定值、固定长位置均使用 CST_ 常量或带注释 +- [ ] 无死代码:无被注释掉的大段代码、无不可达分支;转换过程中的临时调试代码已清除 +- [ ] 嵌套深度:if/for 嵌套不超过 3 层;超出的已用早退(return/continue)或条件提取改善,且不改变判定语义 +- [ ] 注释质量:类头/段落头注释能让人不看 COBOL 也理解该块做什么;无“翻译腔”逐行废话注释 + +## 四、保守性(维护性)确认清单 + +> 前提同上。本节检查后续维护者改错、改规格时的难易度。 + +- [ ] 共通逻辑收敛:PIC 转换、定长拆分、消息输出等重复逻辑统一走 util / 子程序类,无复制粘贴副本(改一处即可全生效) +- [ ] 职责单一:主程序不直接做文件格式细节,细节收敛在 DTO / util 中 +- [ ] 复用性:子程序入口签名统一 execute(par),連絡領域 DTO 可被其他程序复用 +- [ ] 变更影响面小:字段追加/条件变更时只需改一处定义(DTO 或常量区),不需散改多处 substring 位置 +- [ ] 异常处理方针一致:所有 ABEND 场景使用同一模式(消息 + 统一退出码),新增维护代码可照抄现有写法 +- [ ] 与标准例一致性:结构、命名、异常处理写法与本工程 `src\Sample01Main.java` 等标准例风格一致,维护者可举一反三 + +## 五、健壮性(异常处理·边界·资源安全)确认清单 + +> 批量/文件处理程序的核心质量属性。任何单笔数据异常不应导致进程崩溃或资源泄漏;所有异常路径的消息与退出码须与原程序/设计书一致。 + +- [ ] 单笔异常隔离:单条记录格式错/校验错/解析异常被捕获并按设计书输出 ERROR-LOG,不中断整批处理(除非设计书要求 ABEND) +- [ ] 异常路径等价:文件不存在、记录格式错、DB 访问失败等时,消息内容与 ABEND 行为(退出码)与原程序/设计书一致 +- [ ] 资源安全:所有 I/O 流在正常与异常路径均关闭(try-with-resources 或 closeFiles 全覆盖),无句柄泄漏 +- [ ] 数值/格式安全:Integer.parseInt 等解析对空/非数字输入有防护或已在更前置的校验拦截(不产生未捕获异常) +- [ ] 边界值:空输入 / 最大长度 / 0 值 / 文件末尾记录 等边界已验证 +- [ ] ABEND 一致性:所有 ABEND 场景统一走 消息+退出码 模式,退出码(含 Windows 取模后实值)与设计书一致 + +## 六、性能与效率确认清单 + +> COBOL 批处理转换后须保持同等规模下的可接受性能与稳定性。 + +- [ ] IO 效率:逐行读写、无整文件无谓加载;大文件场景下内存占用平稳 +- [ ] 转换开销:PIC/定长处理复用 util.ConvUtil,无重复创建大对象或正则等重操作 +- [ ] 不必要输出:无调试用 System.out/print 遗留;消息输出符合设计书 +- [ ] 循环与批量:READ 循环/段落调用结构与 COBOL 等价,无额外嵌套或重复计算 +- [ ] 资源复用:DTO/缓冲在同一实例内复用,无每笔 new 大对象的不必要开销 + +## 七、安全性与可观测性确认清单 + +> 日志与运行前提的可控、可排查。 + +- [ ] 日志恰当:消息编号/参数与设计书一致,运维可据此定位;无暴露内部栈或敏感数据 +- [ ] 无敏感信息泄露:输出/日志不含密码、密钥、个人敏感字段的超范围暴露 +- [ ] 编码明确:源码 UTF-8;输入/输出文件编码在设计书或对照表中注明 +- [ ] 运行前提:所需 JDK 版本、外部依赖(JDBC 驱动等)在 compile.bat/run.bat 或对照表中明确 +- [ ] 权限与范围:仅操作项目目录内,未触碰系统或其他工程文件(见 README 前提事项) + +## 八、不合格项记录 + +| No | 检查项 | 不合格内容 | 修正措施 | 复检结果 | +|---|---|---|---|---| +| 1 | | | | | diff --git a/JavaSrc/doc/转换基准.md b/JavaSrc/doc/转换基准.md new file mode 100644 index 0000000..cdf8e1f --- /dev/null +++ b/JavaSrc/doc/转换基准.md @@ -0,0 +1,224 @@ +# COBOL→Java 转换 作业流程与规则(项目基准) + +本文件为全项目转换作业的技术基准(流程+规则+环境要求)。作业前提事项(目录权限·备份规则·文件归置)见根目录 `README.md`。 + +**核心原则:代码转换优先依据式样书(详细设计书),原 COBOL 代码仅作参考。式样书与原代码不一致时,以式样书为准(已确认的差异记入对照表《疑问·回答记录》);测试验证优先依据 `doc\Java测试基准说明.md`。** + +## 一、目录约定 + +``` +项目根\ +├── README.md 项目导览·前提事项 +├── compile.bat 编译脚本(javac 直调,不改系统配置) +├── run.bat 运行主示例 +├── run_db.bat 运行 DB 示例 +├── src\ Java 源码(主程序、子程序、DTO、工具类) +├── cobol\ COBOL 源码(主程序、COPY 句、子程序) +│ ├── copy\ COPY句成员 +│ └── sub\ 子程序 COBOL 源码 +├── doc\ 文档(基准文档·式样书·DDL·各轮对照表) +│ ├── design\ 式样书·详细设计书(Markdown,按程序名命名) +│ ├── db\ 表定义 DDL / 字段一览 +│ ├── conv\ 每程序的《转换对照表》(Markdown) +│ ├── 转换基准.md 本文件 +│ ├── 代码确认Checklist.md(步骤6使用的确认模板) +│ └── Java测试基准说明.md(测试用例编写基准) +├── data\ 测试数据、输入输出文件 +│ └── test\ 测试资产(按程序名分目录) +├── lib\ JDBC 驱动 jar +├── out\ 编译输出(compile.bat 每次清空重建) +├── bk\ 备份(按日期时刻命名,详见 README 第五节) +├── tmp\ 作业临时区(可随时清空) +├── tools\ 自动化脚本(按需追加) +└── gitPushFolder\ git 推送暂存目录(复制 src\+3bat 后在此 push;git 不碰其他文件夹) +``` + +标准例:`Sample01Main`(常用语法映射)、`FileSample`(文件处理)、`DbSample`(DB访问)、`dto\EmpRec`(记录布局)、`util\ConvUtil`(PIC 转换)。转换时套用该模式。 + +## 二、作业流程(每个 COBOL 程序一轮) + +| 步骤 | 内容 | 产出 | +|---|---|---| +| 1. 受理确认 | 核对主程序/COPY句/式样书/DDL 是否齐全 | 文件清单 | +| 2. 分析 | 通读代码+COPY+式样书,整理变量一览、段落一览、文件/SQL/CALL 一览 | 转换分析表 | +| 3. 疑问确认 | 业务逻辑不明点、代码与式样书不一致处列疑问表,用户答复后才动工 | 疑问·回答记录 | +| 4. 转换 | 按第三节规则生成 Java,套用标准例模式 | src\ 下源码 | +| 5. 验证 | ① javac 编译通过 ② COBOL↔Java 逐项核对(分支/计算/文件IO/SQL)③ 有测试数据则运行比对输出 | 核对结果 | +| 6. 代码确认 | 按 `doc\代码确认Checklist.md` 从四个维度审查:① **机能正确性**:设计书机能项逐项核对实现位置,并覆盖分支/边界/精度/异常路径;② **规约符合性**:命名/类型映射/结构/注释等逐条检查;③ **可读性**、④ **保守性(维护性)**:在不破坏 COBOL 1:1 对应关系的前提下检查(冲突时以忠实转换为优先并记录理由) | 四维度确认清单(记入对照表) | +| 7. 交付 | 源码 + `doc\conv\程序名_对照表.md` | 对照表 | + +- 每轮一个程序,用户验收后再进下一个。 +- 第6步先由转换方自查出清单并修正不合格项,最后由用户最终验收。 + +## 三、转换规则 + +### 3.1 命名规则 + +| COBOL | Java | +|---|---| +| PROGRAM-ID `SAMPL01`(主程序) | 类 `Sampl01Main`(驼峰化 + Main 后缀) | +| PROGRAM-ID `SUBPROG`(子程序) | 类 `SubprogSub`(驼峰化 + Sub 后缀) | +| `WS-XXX` 变量 | `wsXxx` 字段 | +| `XXX-PARA` 段落 | `xxxPara()` 方法(private) | +| VALUE 常量 / 88 级条件名 | `CST_XXX` 常量 / `isXxx()` 判定方法 | +| COPY 句(记录用,如 KIN01REC) | `dto\` 下 DTO 类,命名 `XxxRec` | +| COPY 句(連絡領域用,如 ZANCHKAC) | `dto\` 下 DTO 类,命名 `XxxPar` | +| CALL 'SUBX' USING ...PAR | `SubxSub.execute(par)`(子程序统一入口方法名 execute) | +| 文件 SELECT ASSIGN | `INP_FILE` / `OUT_FILE` 路径常量 | + +### 3.2 类型规则(统一走 util.ConvUtil) + +| COBOL | Java | +|---|---| +| PIC X(n) | String(padRight 定长化) | +| PIC 9(n) | int / long(padLeftZero) | +| PIC 9(n)V99 | BigDecimal(toDecimal / formatZoned) | +| COMPUTE … ROUNDED | setScale(…, RoundingMode.HALF_UP) | +| 固定长记录拆分 | substring(位置注释对应 05 层定义) | + +### 3.3 语句·结构映射 + +| COBOL | Java | +|---|---| +| WORKING-STORAGE SECTION | 类私有字段区 | +| PROCEDURE DIVISION 段落 | private 方法 | +| PERFORM 段落 | 方法调用 | +| PERFORM UNTIL | while | +| PERFORM VARYING | for | +| PERFORM A THRU B | 一个方法内依次调用 aPara()…bPara() | +| IF / ELSE / END-IF | if / else | +| EVALUATE … WHEN OTHER | switch / default | +| DISPLAY | System.out.println | +| READ … AT END | readLine() == null | +| OPEN / CLOSE | 同段落→try-with-resources;跨段落→字段保持+显式 close(参照标准例 FileSample) | +| WRITE | writer.println / write | +| CALL 子程序 | 子程序独立类的方法调用(参数按引用→返回值或可变对象承载) | +| EXEC SQL(游标/单行/COMMIT/ROLLBACK/WHENEVER SQLERROR) | JDBC PreparedStatement / commit / rollback / try-catch(见 DbSample) | +| OCCURS | 数组 / List | +| DEPENDING ON | 动态数组 / List(长度由变量决定) | +| REDEFINES | 同一数据的两种解析视图。reset()清零主存储即可(等价INITIALIZE);单视图写入与COBOL MOVE语义一致,无需同时清零其他视图。整区覆写时须同时写入所有视图(详见3.7(2)) | +| COMP-3(packed) | ConvUtil 扩展二进制解析方法(需要时追加) | + +### 3.4 GO TO 方针 + +原则上原代码未使用 GO TO。若出现:**尽量保持原段落结构;仅当特别冗长时做小范围结构化重构**(段内跳过→if 包裹、跨段→方法调用/标志位)。不做 goto 机械模拟。 + +### 3.5 注释规则 + +- 类头:COBOL↔Java 整体结构对应说明。 +- 字段/方法:标注对应 COBOL 数据项/段落。 +- 关键语句行内:标注原 COBOL 语句(MOVE/COMPUTE/EXEC SQL 等),保证可追溯。 + +### 3.6 异常处理统一模式 + +所有程序的 ABEND 处理使用同一模式,便于维护者举一反三: +- 业务逻辑异常:`abendPara()` → `Sub03EndSub.execute(e01AbdPar)` → `System.exit(999)`(Windows 退出码 231) +- 文件 I/O 异常:`UncheckedIOException`(JVM 默认退出码 1) +- 消息输出统一走 `msgOutPara()`,不散落 System.out + +### 3.7 COBOL→Java 差异点特殊处理 + +COBOL 与 Java 在语义上存在多处系统性差异,转换时须逐项确认等价性。以下为必须特殊考虑的差异点: + +#### (1) INITIALIZE vs Java 默认值 + +| 项目 | COBOL | Java | 注意事项 | +|---|---|---|---| +| 数值字段 | INITIALIZE → ZERO | int/long 默认 0 | 等价,无需额外处理 | +| 字符字段 | INITIALIZE → SPACES | String 默认 **null**(非 SPACES) | **必须显式初始化为 `""`**,否则后续拼接/比较会 NPE | +| setter 方法 | MOVE SPACES → 字段 | setter 须加 null 防护:`(v == null) ? "" : v` | 防止外部传入 null | +| reset() 方法 | INITIALIZE 整个记录 | 数值→0、文字→"" | 每个 DTO 须实现 reset(),与 COBOL INITIALIZE 逐项对应 | + +#### (2) REDEFINES 多视图 + +COBOL 的 REDEFINES 表示同一内存区域的多种解析视图。写入任一视图,其他视图同时反映变化。 + +| 场景 | COBOL 行为 | Java 实现 | 注意事项 | +|---|---|---|---| +| reset()( INITIALIZE 等价) | 整个内存区域清零,所有视图同时归零 | reset() 中清零主存储字段 | reset() 不需要逐视图清零,清零主存储即可 | +| 单视图写入(MOVE 分组项) | MOVE WRK-DEMO-TYPE → 仅写入前4位,后4位不变 | setDemoType() 仅修改主存储的对应位 | **与 COBOL 语义一致**,无需同时清零其他视图 | +| 整区覆写(MOVE 整个 REDEFINES 项) | MOVE 10 TO WRK-DEMO-AREA → 整个8位变为 00000010 | 须同时写入所有视图,或直接操作主存储 | 确认原 COBOL 是"整区覆写"还是"单视图写入",Java 实现须与原程序一致 | + +**判定方法**:检查原 COBOL 的 MOVE 目标是整个 REDEFINES 项(如 WRK-DEMO-AREA)还是某个分组项(如 WRK-DEMO-TYPE)。整区覆写→Java 须同时 set 所有视图;单视图写入→Java 仅 set 对应视图。 + +#### (3) VALUE 子句初始化 + +COBOL 字段声明时的 VALUE 子句指定初始值。Java 对应方式: + +```cobol +03 WRK-R01EOF PIC X(001) VALUE '0'. → Java: private String wrkR01Eof = "0"; +03 WRK-COMMA-CNT PIC 9(002) COMP. → Java: private int wrkCommaCnt; (默认0) +``` + +注意:INITIALIZE 后须重新赋值(INITIALIZE 覆盖 VALUE 设定值)。 + +#### (4) 88 级条件名 + +COBOL 88 级是条件名(condition name),表示"当宿主字段等于某值时条件为真"。 + +```cobol +88 WRK-STATUS-ACTIVE VALUE '1'. → Java: private boolean isStatusActive() { return "1".equals(wrkCsvStatus); } +88 WRK-STATUS-CANCEL VALUE '9'. → Java: private boolean isStatusCancel() { return "9".equals(wrkCsvStatus); } +``` + +注意:Java 用 `equals()` 比较,不用 `==`(String 值比较)。 + +#### (5) MOVE 定长化语义 + +COBOL 的 MOVE 是定长传送(自动截断/补空格),Java 需显式定长化: + +| COBOL MOVE | Java 实现 | 说明 | +|---|---|---| +| `MOVE "ABC" TO WS-X-10`(PIC X(10)) | `ConvUtil.padRight("ABC", 10)` → `"ABC "` | 左对齐,右补空格 | +| `MOVE 123 TO WS-N-5`(PIC 9(5)) | `ConvUtil.padLeftZero(123, 5)` → `"00123"` | 左补零 | +| `MOVE WS-X-10 TO W01-REC`(定长记录) | substring 截取 + padRight/padLeftZero | 位置注释对应 05 层定义 | + +#### (6) 参考修正(Reference Modification) + +COBOL 参考修正语法 `M00...(1:1)` 表示取字段第1位开始的1个字符。**COBOL 起始位置为 1**,Java substring 起始位置为 0。 + +```cobol +M00UMKDATS22-03(1:1) → Java: substring(0, 1) // 取第1个字符 +``` + +本项目已用 `padRight("2", 30)` 等价实现(先头1字符上写),无需 substring。但其他程序若使用参考修正,须注意起始位置偏移。 + +#### (7) UNSTRING TALLYING 语义差异 + +COBOL 的 UNSTRING TALLYING 统计的是**分隔符个数**,Java split(-1) 返回的是**字段个数**: + +| COBOL | Java | 含义差异 | +|---|---|---| +| `TALLYING IN WRK-COMMA-CNT`(8字段需7个分隔符) | `r01Line.split(",", -1).length` = 8 | COBOL: cnt=7;Java: length=8 | +| 判定:`IF WRK-COMMA-CNT ≠ 7` | 判定:`fieldCount() ≠ 8` | **等价**(cnt+1=length) | + +注意:若用 split(-1).length 判定,须与 COBOL 的 TALLYING+1 比较,不可直接等同。 + +#### (8) COMP-3 符号处理 + +COBOL 的 COMP-3(packed decimal)用最后一个 nibble 存储符号(C=正,D=负)。Java 的 long 是二进制补码,无压缩十进制表示。 + +| 项目 | 处理方式 | +|---|---| +| 正数 | COBOL COMP-3 → Java long:直接转换,无差异 | +| 负数 | 须确认原 COBOL 数据是否含负数;若含负数,须用专用解包方法 | +| 本项目 | 计数器(cunR01Inn 等)均为非负数,无需特殊处理 | + +转换时须确认:原 COBOL 的 COMP-3 字段是否有负数可能性。若有,须追加 COMP-3 解包方法到 ConvUtil。 + +## 四、作业约束 + +1. 只操作项目根目录(含其子目录)内,不改动系统与本工程以外的任何内容。 +2. 源码编码 UTF-8。 +3. 不猜不漏:一切业务逻辑不明点记入疑问表,确认后才写码。 +4. 编译运行验证以 compile.bat / run.bat 为准,交付前必须通过。 + +### 语言版本与软件环境(团队共通要求) + +| 项目 | 内容 | +|---|---| +| Java 版本 | Java SE 8(JDK 1.8)为基准;语法按 Java 8 编写,不使用更高版本特性。Java 11 / 17 语法兼容,可直接编译运行,无需修改源码 | +| 字符编码 | UTF-8 统一(编译 `javac -encoding UTF-8`;运行 `-Dfile.encoding=UTF-8` + `chcp 65001`) | +| 外部依赖 | 无第三方框架;仅允许 `lib\` 下 JDBC 驱动 jar(当前未配置,DB 程序启用前补充) | +| 构建运行方式 | Windows 批处理直调 `javac` / `java`;无 Maven / Gradle 等构建工具 | +| 本机差异 | JDK 安装路径等本机配置仅存于 `compile.bat`(`JDK_BIN` 变量 / `JAVA_HOME` 环境变量),不入本文档 | diff --git a/JavaSrc/lib/sqlite-jdbc-3.8.11.2.jar b/JavaSrc/lib/sqlite-jdbc-3.8.11.2.jar new file mode 100644 index 0000000..70be427 Binary files /dev/null and b/JavaSrc/lib/sqlite-jdbc-3.8.11.2.jar differ diff --git a/JavaSrc/run.bat b/JavaSrc/run.bat new file mode 100644 index 0000000..d8a09b8 --- /dev/null +++ b/JavaSrc/run.bat @@ -0,0 +1,10 @@ +@echo off +rem === Run Kin01InpMain (leave-application CSV intake / validation) === +rem Input: data/KIN01R01.csv (UTF-8), prepared in advance. +rem Optional arg overrides input path: run.bat C:\path\to\input.csv +rem CSV format (comma-separated, 8 fields, fixed order): +rem 1.APPL-ID(9) 2.EMP-ID(8) 3.START-DATE(YYYYMMDD) +rem 4.START-TIME(HHMM) 5.END-DATE(YYYYMMDD) 6.END-TIME(HHMM) +rem 7.LEAVE-TYPE(2:01-04) 8.STATUS(1:1=active/9=cancel) +chcp 65001 >nul +java -Dfile.encoding=UTF-8 -cp out Kin01InpMain %1 diff --git a/JavaSrc/run_db.bat b/JavaSrc/run_db.bat new file mode 100644 index 0000000..d8facd0 --- /dev/null +++ b/JavaSrc/run_db.bat @@ -0,0 +1,4 @@ +@echo off +rem === Run DbSample (needs JDBC driver jar under lib) === +chcp 65001 >nul +java -Dfile.encoding=UTF-8 -cp "out;lib/*" DbSample diff --git a/JavaSrc/src/DbSample.java b/JavaSrc/src/DbSample.java new file mode 100644 index 0000000..fcb2ee7 --- /dev/null +++ b/JavaSrc/src/DbSample.java @@ -0,0 +1,127 @@ +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * ============================================================ + * 【COBOL→Java 标准例】数据库访问(EXEC SQL → JDBC) + * ============================================================ + * 对应关系: + * EXEC SQL CONNECT TO ... END-EXEC → DriverManager.getConnection(url, user, pass) + * EXEC SQL DECLARE C1 CURSOR FOR SELECT ... → conn.prepareStatement(sql) + * EXEC SQL OPEN C1 → rs = ps.executeQuery() + * EXEC SQL FETCH C1 INTO :WS-X → rs.next() + rs.getXxx(...) + * EXEC SQL CLOSE C1 → try-with-resources 自动 close + * EXEC SQL INSERT/UPDATE ... :HOST-VAR END-EXEC → ps.setXxx(i, hostVar) + executeUpdate() + * EXEC SQL COMMIT END-EXEC → conn.commit() + * EXEC SQL ROLLBACK END-EXEC → conn.rollback() + * EXEC SQL WHENEVER SQLERROR ... → try-catch (SQLException) + * + * 运行前提:lib 目录下放置所用数据库的 JDBC 驱动 jar,例如: + * DB2 : db2jcc4.jar (com.ibm.db2.jcc.DB2Driver) + * Oracle : ojdbc8.jar (oracle.jdbc.OracleDriver) + * PostgreSQL : postgresql.jar (org.postgresql.Driver) + * 单独运行:java -cp "out;lib/*" DbSample + */ +public class DbSample { + + /** CONNECT 参数(按实际环境修改) */ + private static final String URL = "jdbc:db2://localhost:50000/TESTDB"; + private static final String USER = "db2inst1"; + private static final String PASS = "password"; + + public static void main(String[] args) { + Connection conn = null; + try { + // EXEC SQL CONNECT TO TESTDB USER db2inst1 USING *** END-EXEC + conn = DriverManager.getConnection(URL, USER, PASS); + conn.setAutoCommit(false); // 批处理程序的标准做法:显式事务控制 + + selectCursorPara(conn); // 游标读取 + updatePara(conn); // 单行更新 + + // EXEC SQL COMMIT END-EXEC + conn.commit(); + System.out.println("[db] NORMAL END (COMMIT)"); + + } catch (SQLException e) { + // EXEC SQL WHENEVER SQLERROR GO TO ERROR-PROC END-EXEC + System.out.println("[db] SQLERROR code=" + e.getErrorCode() + " msg=" + e.getMessage()); + rollbackQuiet(conn); + } finally { + closeQuiet(conn); + } + } + + /** + * 游标读取段落。 + * COBOL: + * DECLARE C1 CURSOR FOR + * SELECT EMP_ID, EMP_NAME, EMP_SALARY + * FROM TBL_EMP + * WHERE EMP_DEPT = :WS-DEPT + * OPEN C1 + * PERFORM UNTIL SQLCODE NOT = ZERO + * FETCH C1 INTO :WS-EMP-ID, :WS-EMP-NAME, :WS-SALARY + * END-PERFORM + * CLOSE C1 + */ + private static void selectCursorPara(final Connection conn) throws SQLException { + String sql = "SELECT EMP_ID, EMP_NAME, EMP_SALARY FROM TBL_EMP WHERE EMP_DEPT = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, "10"); // 宿主变量 :WS-DEPT → setXxx + try (ResultSet rs = ps.executeQuery()) { // OPEN C1 + while (rs.next()) { // FETCH 循环(SQLCODE=100 即结束) + String empId = rs.getString("EMP_ID"); // :WS-EMP-ID + String empName = rs.getString("EMP_NAME"); // :WS-EMP-NAME + BigDecimal salary = rs.getBigDecimal("EMP_SALARY"); // :WS-SALARY + System.out.println("[db] " + empId + " " + empName + " " + salary.toPlainString()); + } + } // CLOSE C1(自动) + } + } + + /** + * 单行更新段落(宿主变量 → setXxx)。 + * COBOL: + * EXEC SQL + * UPDATE TBL_EMP + * SET EMP_SALARY = :WS-NEW-SALARY + * WHERE EMP_ID = :WS-EMP-ID + * END-EXEC + */ + private static void updatePara(final Connection conn) throws SQLException { + String sql = "UPDATE TBL_EMP SET EMP_SALARY = ? WHERE EMP_ID = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setBigDecimal(1, new BigDecimal("40000.00")); // :WS-NEW-SALARY + ps.setString(2, "00001"); // :WS-EMP-ID + int updCnt = ps.executeUpdate(); // 更新件数(SQLERRD(3) 相当) + System.out.println("[db] UPDATE COUNT=" + updCnt); + } + } + + /** EXEC SQL ROLLBACK END-EXEC(异常时) */ + private static void rollbackQuiet(final Connection conn) { + if (conn != null) { + try { + conn.rollback(); + System.out.println("[db] ROLLBACK DONE"); + } catch (SQLException e) { + System.out.println("[db] ROLLBACK FAILED: " + e.getMessage()); + } + } + } + + private static void closeQuiet(final Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + System.out.println("[db] CLOSE FAILED: " + e.getMessage()); + } + } + } +} diff --git a/JavaSrc/src/FileIo.java b/JavaSrc/src/FileIo.java new file mode 100644 index 0000000..acdaf86 --- /dev/null +++ b/JavaSrc/src/FileIo.java @@ -0,0 +1,32 @@ +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * ============================================================ + * 【COBOL→Java】ファイル入出力ヘルパ(JDK8 互換) + * ============================================================ + * JDK8 は FileReader/FileWriter の (String, Charset) コンストラクタを持たないため、 + * InputStreamReader/OutputStreamWriter 経由で UTF-8 固定長処理を提供する。 + * デフォルトパッケージに配置し、各 ZAN メインから import 不要で利用可能。 + */ +public final class FileIo { + + private FileIo() { + } + + public static BufferedReader openReader(final String path) throws IOException { + return new BufferedReader( + new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8)); + } + + public static BufferedWriter openWriter(final String path) throws IOException { + return new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8)); + } +} diff --git a/JavaSrc/src/FileSample.java b/JavaSrc/src/FileSample.java new file mode 100644 index 0000000..bc343c2 --- /dev/null +++ b/JavaSrc/src/FileSample.java @@ -0,0 +1,114 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import dto.EmpRec; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java 标准例】顺序文件处理 + * ============================================================ + * COBOL 与 Java 的对应关系: + * SELECT EMPFILE ASSIGN TO SYS001 → 文件路径常量 INP_FILE + * FD EMPFILE. 01 EMPREC. → EmpRec DTO + substring 拆分 + * OPEN INPUT EMPFILE → new BufferedReader(...) + * READ EMPFILE AT END MOVE 'Y' TO EOF-FLG → line = br.readLine(); line == null 即 AT END + * CLOSE EMPFILE → try-with-resources 自动 close + * OPEN OUTPUT REPFILE → new PrintWriter(...) + * WRITE REP-REC → pw.println(...) + * + * 文件编码统一为 UTF-8(原 COBOL 为系统代码页时,转换时需确认)。 + */ +public class FileSample { + + /** SELECT EMPFILE ASSIGN TO ... */ + private static final String INP_FILE = "data/EMPFILE.txt"; + private static final String OUT_FILE = "data/REPOUT.txt"; + + public static void execute() { + List list = readFilePara(); + writeFilePara(list); + } + + /** + * 读入处理段落。 + * COBOL: + * READ-EMP-PARA. + * PERFORM UNTIL EOF-FLG = 'Y' + * READ EMPFILE AT END MOVE 'Y' TO EOF-FLG + * NOT AT END PERFORM REC-PROC-PARA + * END-PERFORM. + */ + private static List readFilePara() { + List list = new ArrayList(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(INP_FILE), "UTF-8"))) { + String line; + while ((line = br.readLine()) != null) { // AT END → readLine() == null + list.add(parseRec(line)); + } + } catch (IOException e) { + // FILE STATUS 35(文件不存在)等 → 异常处理 + throw new UncheckedIOException(e); + } + return list; + } + + /** + * 记录拆分:01 层各 05 项按字节位置 substring(固定长记录的标准转换方式)。 + * 01 EMPREC. + * 05 EMP-ID PIC X(5). → [0,5) + * 05 EMP-NAME PIC X(20). → [5,25) + * 05 EMP-DEPT PIC X(2). → [25,27) + * 05 EMP-SALARY PIC 9(7)V99. → [27,36) 隐含小数2位 + */ + private static EmpRec parseRec(final String rec) { + EmpRec r = new EmpRec(); + r.setEmpId(rec.substring(0, 5)); + r.setEmpName(rec.substring(5, 25).trim()); + r.setEmpDept(rec.substring(25, 27)); + r.setEmpSalary(ConvUtil.toDecimal(rec.substring(27, 36), 2)); + return r; + } + + /** + * 报表输出段落。 + * COBOL: + * WRITE-REP-PARA. + * OPEN OUTPUT REPFILE + * PERFORM UNTIL EOF-FLG = 'Y' (逐件 WRITE + 集计) + * CLOSE REPFILE. + */ + private static void writeFilePara(final List list) { + BigDecimal wsTotal = BigDecimal.ZERO; // 05 WS-TOTAL PIC 9(7)V99. + int wsCnt = 0; // 05 WS-CNT PIC 9(3). + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(OUT_FILE), "UTF-8"))) { + pw.println("*** EMP SALARY REPORT ***"); + for (EmpRec r : list) { + // WRITE REP-REC(PIC 编辑:padRight 对应 X(20) 定长化) + pw.println(ConvUtil.padRight(r.getEmpId(), 6) + + ConvUtil.padRight(r.getEmpName(), 20) + + ConvUtil.padRight(r.getEmpDept(), 4) + + r.getEmpSalary().setScale(2).toPlainString()); + // ADD EMP-SALARY TO WS-TOTAL. / ADD 1 TO WS-CNT. + wsTotal = wsTotal.add(r.getEmpSalary()); + wsCnt++; + } + pw.println("CNT=" + ConvUtil.padLeftZero(wsCnt, 3) + + " TOTAL=" + wsTotal.setScale(2).toPlainString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + System.out.println("[file] report output: " + OUT_FILE); + } +} diff --git a/JavaSrc/src/Kin01InpMain.java b/JavaSrc/src/Kin01InpMain.java new file mode 100644 index 0000000..695cbf4 --- /dev/null +++ b/JavaSrc/src/Kin01InpMain.java @@ -0,0 +1,647 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import dto.Kin01Rec; +import dto.Kin05Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanchkacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KIN01INP 休暇申請CSV取込・検証処理 + * ============================================================ + * PROGRAM-ID. KIN01INP → 类 Kin01InpMain(PGMパターン:振り分け) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R01INNFIL(KIN01R01 CSV入力 F80B) → R01INN_FILE + * W01OUTFIL(KIN01W01 WORK-LEAVE F80B)→ W01OUT_FILE(COPY KIN01REC → dto.Kin01Rec) + * W02OUTFIL(KIN01W02 ERROR-LOG V200B)→ W02OUT_FILE(COPY KIN05REC → dto.Kin05Rec) + * 使用SUB(CALL → 类#execute): + * SUB01DAT→Sub01DatSub / SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub / SUB04CHK→Sub04ChkSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R01INNSOR→readR01InnPara() + * 2000MAJSOR→majPara() 2010CSVSOR→csvPara() 2020LEAVSERSOR→leavserPara() + * 2030VALIDATESOR→validatePara() 2040CANCELSOR→cancelPara() 2050ERRORSOR→errorPara() + * 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 与原代码的差异(详见 doc\conv\KIN01INP_对照表.md 疑问·回答记录): + * #Q1 字段数判定按设计意图(split后字段数≠8),TALLYING分隔符计数仅用于错误消息 + * #Q2 有効申請のW01出力は APPL-ID=0(原代码为CSV值,以设计书为准) + * #Q3 FUNCTION WHEN-COMPILED → 程序启动时刻代替 + * #Q5 複合条件チェック改用 WRK-CSV-* 项目(原代码误用W01项目恒不成立) + */ +public class Kin01InpMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KIN01INP'. */ + private static final String CST_PRGIDX = "KIN01INP"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. サブエラー */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数 */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数 */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-STAT-1 PIC X(001) VALUE '1'. */ + private static final String CST_STAT_1 = "1"; + /** 03 CNS-STAT-9 PIC X(001) VALUE '9'. */ + private static final String CST_STAT_9 = "9"; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KIN01R01.(CSV入力) */ + private static final String R01INN_FILE = "data/KIN01R01.csv"; + /** SELECT W01OUTFIL ASSIGN TO KIN01W01.(WORK-LEAVE出力) */ + private static final String W01OUT_FILE = "data/KIN01W01.txt"; + /** SELECT W02OUTFIL ASSIGN TO KIN01W02.(ERROR-LOG出力) */ + private static final String W02OUT_FILE = "data/KIN01W02.txt"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + /** 03 CUN-W02OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW02Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-U06 PIC 9(008). 運用日付 */ + private int wrkU06; + /** 03 WRK-R01EOF PIC X(001). 読込フラグ */ + private String wrkR01Eof = "0"; + /** 03 WRK-COMMA-CNT PIC 9(002) COMP. UNSTRING TALLYING(分隔符计数) */ + private int wrkCommaCnt; + /** 03 WRK-LT-FOUND PIC X(001). 休暇種別検索結果 */ + private String wrkLtFound = "0"; + /** 03 WRK-ERR-TYPE PIC X(001). エラー種別 F/L/S */ + private String wrkErrType = ""; + /** 03 WRK-CSV-APPL-ID PIC X(009). */ + private String wrkCsvApplId = ""; + /** 03 WRK-CSV-EMP-ID PIC X(008). */ + private String wrkCsvEmpId = ""; + /** 03 WRK-CSV-START-DATE PIC X(008). */ + private String wrkCsvStartDate = ""; + /** 03 WRK-CSV-START-TIME PIC X(004). */ + private String wrkCsvStartTime = ""; + /** 03 WRK-CSV-END-DATE PIC X(008). */ + private String wrkCsvEndDate = ""; + /** 03 WRK-CSV-END-TIME PIC X(004). */ + private String wrkCsvEndTime = ""; + /** 03 WRK-CSV-LEAVE-TYPE PIC X(002). */ + private String wrkCsvLeaveType = ""; + /** 03 WRK-CSV-STATUS PIC X(001). +REDEFINES数値+88条件 */ + private String wrkCsvStatus = ""; + + /** 88 WRK-STATUS-ACTIVE VALUE 1.(REDEFINES WRK-STATUS-NUM の88条件) */ + private boolean isStatusActive() { + return CST_STAT_1.equals(wrkCsvStatus); + } + + /** 88 WRK-STATUS-CANCEL VALUE 9. */ + private boolean isStatusCancel() { + return CST_STAT_9.equals(wrkCsvStatus); + } + + /** + * 03 WRK-LEAVE-TYPE-TABLE./05 WRK-LT-ENTRY OCCURS 4 TIMES/07 WRK-LT-CODE PIC X(002). + * OCCURS → 配列。INDEXED BY WRK-LT-IDX → ループ変数。 + */ + private final String[] wrkLtCode = new String[4]; + + /** + * 03 WRK-DEMO-AREA PIC 9(008). + REDEFINES 3视图(同一領域・複数型)。 + * 主存储=数值视图;文字视图/分组视图以方法形式提供(REDEFINES 标准转换方式)。 + */ + private int wsDemoArea; + + /** MOVE x TO WRK-DEMO-TYPE(前4桁分组视图写入)。 */ + private void setDemoType(final int v) { + wsDemoArea = (wsDemoArea % 10000) + v * 10000; + } + + /** MOVE x TO WRK-DEMO-VALUE(后4桁分组视图写入)。 */ + private void setDemoValue(final int v) { + wsDemoArea = (wsDemoArea / 10000) * 10000 + v; + } + + /** WRK-DEMO-ALPHA REDEFINES WRK-DEMO-AREA PIC X(008). 文字视图。 */ + private String demoAlphaView() { + return ConvUtil.padLeftZero(wsDemoArea, 8); + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANDATAC.(SUB01DAT用) */ + private final ZandatacPar d01UbsPar = new ZandatacPar(); + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + /** COPY ZANCHKAC.(SUB04CHK用) */ + private final ZanchkacPar c01ChkPar = new ZanchkacPar(); + + // ==================== 出力レコード(FILE SECTION → DTO) ==================== + + /** 01 W01OUTREC.(FD W01OUTFIL) */ + private final Kin01Rec w01 = new Kin01Rec(); + /** 01 W02OUTREC.(FD W02OUTFIL) */ + private final Kin05Rec w02 = new Kin05Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + // ※OPENが初期処理・CLOSEが終了処理と別段落のため、try-with-resources でなく + // フィールド保持+termPara()で明示 close を採用(OPEN/CLOSE 対応を優先)。 + + private BufferedReader r01InnFil; + private PrintWriter w01OutFil; + private PrintWriter w02OutFil; + + /** FD R01INNFIL の 01 R01INNREC. 03 R01LINE PIC X(80).(現在レコードバッファ) */ + private String r01Line = ""; + + /** 入力ファイルパス。デフォルトは SELECT ASSIGN 対応の R01INN_FILE; + * コマンドライン引数で上書き可(テスト時等に任意パスを指定)。 */ + private final String r01InnFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(String[] args) { + // 入力ファイルパス:引数指定があれば上書き、なければ R01INN_FILE(COBOL ASSIGN 映射を維持) + final String inputFile = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + new Kin01InpMain(inputFile).run(); // PERFORM 0000MAJCOLSOR 相当 + } + + /** デフォルトコンストラクタ:入力パス=R01INN_FILE(SELECT ASSIGN 対応)。 */ + public Kin01InpMain() { + this(R01INN_FILE); + } + + /** 引数付コンストラクタ:入力ファイルパスを指定(テスト等で使用)。 */ + public Kin01InpMain(final String r01InnFile) { + this.r01InnFile = r01InnFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!"1".equals(wrkR01Eof)) { // PERFORM 2000MAJSOR UNTIL WRK-R01EOF = '1' + majPara(); + } + termPara(); + } finally { + // 确保所有路径(正常+异常)均关闭文件,防止资源泄漏 + closeFiles(); + } + // GOBACK → main 正常返回 + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→種別テーブル設定→運用日付取得→OPEN→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※疑问#Q3:WHEN-COMPILED→启动时刻代替 + String compiled = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + wrkU06 = 0; + wrkCommaCnt = 0; + wrkLtFound = "0"; + wrkErrType = ""; + wrkCsvApplId = ""; + wrkCsvEmpId = ""; + wrkCsvStartDate = ""; + wrkCsvStartTime = ""; + wrkCsvEndDate = ""; + wrkCsvEndTime = ""; + wrkCsvLeaveType = ""; + wrkCsvStatus = ""; + wsDemoArea = 0; + + // *** 休暇種別テーブル設定 MOVE '01'..'04' TO WRK-LT-CODE(1..4). + wrkLtCode[0] = "01"; // 年休/年假 + wrkLtCode[1] = "02"; // 事假 + wrkLtCode[2] = "03"; // 因公特批假 + wrkLtCode[3] = "04"; // 病欠 + + // *** 運用日付取得 CALL 'SUB01DAT' USING D01UBSPAR. + d01UbsPar.reset(); // INITIALIZE D01UBSPAR. + Sub01DatSub.execute(d01UbsPar); + if (d01UbsPar.getD01FkiCod() != 0) { + // IF D01FKICOD NOT = ZERO → メッセージ出力後 ABEND + msgOutPara(CST_MSGSUBEEK, "SUB01DAT", + ConvUtil.padLeftZero(d01UbsPar.getD01FkiCod(), 4)); // S9(4) COMP→4桁表示 + abendPara(); + } + // MOVE D01UBSUDATE TO WRK-U06. + wrkU06 = d01UbsPar.getD01UbsUdate(); + + // *** 入出力ファイル OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. + openFiles(); + + // *** #R01#を読み込み(1回目) + readR01InnPara(); + } + + /** + * 段落 1100R01INNSOR:##R01##読込処理。 + * READ AT END / NOT AT END のマッピング。 + */ + private void readR01InnPara() { + try { + String line = r01InnFil.readLine(); + if (line == null) { + // AT END: MOVE '1' TO WRK-R01EOF. + wrkR01Eof = "1"; + } else { + // NOT AT END: ADD 1 TO CUN-R01INN.(レコードバッファへ格納) + r01Line = line; + cunR01Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * CSV分解→種別検索→エラー判定(IF/ELSE連鎖による振り分け)→次レコード読込。 + */ + private void majPara() { + // *** CSV分解 + csvPara(); + + // *** 休暇種別テーブル検索 + leavserPara(); + + // *** エラー判定(フィールド数/休暇種別) + if (fieldCount() != 8) { + // ※疑问#Q1:設計書どおり「フィールド数≠8」で判定(TALLYING値はメッセージ用に保持) + wrkErrType = "F"; + errorPara(); + } else if (!"1".equals(wrkLtFound)) { + wrkErrType = "L"; + errorPara(); + } else { + // *** ステータス判定(IF/ELSE連鎖+88条件名) + if (isStatusActive()) { + validatePara(); + } else if (isStatusCancel()) { + cancelPara(); + } else { + wrkErrType = "S"; + errorPara(); + } + } + + // *** 次のレコード読込 + readR01InnPara(); + } + + /** 現在レコードのCSVフィールド数(疑问#Q1:設計意図どおり項目数で判定するための補助)。 */ + private int fieldCount() { + return r01Line.split(",", -1).length; + } + + /** + * 段落 2010CSVSOR:CSV分解処理。 + * UNSTRING DELIMITED BY ',' INTO 8項目 TALLYING IN WRK-COMMA-CNT. + */ + private void csvPara() { + // MOVE ZERO TO WRK-COMMA-CNT./INITIALIZE 各CSV項目. + wrkCommaCnt = 0; + wrkCsvApplId = ""; + wrkCsvEmpId = ""; + wrkCsvStartDate = ""; + wrkCsvStartTime = ""; + wrkCsvEndDate = ""; + wrkCsvEndTime = ""; + wrkCsvLeaveType = ""; + wrkCsvStatus = ""; + + // UNSTRING R01INNREC DELIMITED BY ',' INTO ... (split(-1)で空項目保持) + String[] tokens = r01Line.split(",", -1); + // 先頭8項目を受信領域へ(PIC X(n) 左詰め・右空白埋め=UNSTRING転送規則) + wrkCsvApplId = ConvUtil.padRight(tok(tokens, 0), 9); + wrkCsvEmpId = ConvUtil.padRight(tok(tokens, 1), 8); + wrkCsvStartDate = ConvUtil.padRight(tok(tokens, 2), 8); + wrkCsvStartTime = ConvUtil.padRight(tok(tokens, 3), 4); + wrkCsvEndDate = ConvUtil.padRight(tok(tokens, 4), 8); + wrkCsvEndTime = ConvUtil.padRight(tok(tokens, 5), 4); + wrkCsvLeaveType = ConvUtil.padRight(tok(tokens, 6), 2); + wrkCsvStatus = ConvUtil.padRight(tok(tokens, 7), 1); + // TALLYING IN WRK-COMMA-CNT:分隔符个数(8項目なら7)。※判定には使用しない(#Q1) + wrkCommaCnt = tokens.length - 1; + } + + /** UNSTRING受信領域への転送補助(項目不足時は空文字=未転送扱い)。 */ + private static String tok(final String[] tokens, final int idx) { + return idx < tokens.length ? tokens[idx] : ""; + } + + /** + * 段落 2020LEAVSERSOR:休暇種別テーブル検索処理。 + * SEARCH WRK-LT-ENTRY VARYING WRK-LT-IDX ... のマッピング。 + */ + private void leavserPara() { + // MOVE '0' TO WRK-LT-FOUND./SET WRK-LT-IDX TO 1. + wrkLtFound = "0"; + for (int idx = 0; idx < wrkLtCode.length; idx++) { // SEARCH VARYING + // WHEN WRK-LT-CODE(WRK-LT-IDX) = WRK-CSV-LEAVE-TYPE + if (wrkLtCode[idx].equals(wrkCsvLeaveType)) { + wrkLtFound = "1"; + break; // SEARCH一致で脱出 + } + } + // AT END: CONTINUE(見つからなければそのまま) + } + + /** + * 段落 2030VALIDATESOR:有効申請処理。 + * SUB04CHKによる5項目チェック→複合条件→W01出力。 + * ※段落内の GO TO 2030VALIDATESOR-EXT は return にマッピング(GO TO 方针)。 + */ + private void validatePara() { + // *** W01レコード初期化 INITIALIZE W01OUTREC. + w01.reset(); + + // --- SUB04CHKで社員番号チェック --- + c01ChkPar.reset(); // INITIALIZE C01CHKPAR. + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvEmpId, 80)); // MOVE ... TO C01CHKDAT. + c01ChkPar.setC01ChkTyp("EMPID"); // MOVE 'EMPID' TO C01CHKTYP. + Sub04ChkSub.execute(c01ChkPar); // CALL 'SUB04CHK' USING C01CHKPAR. + if (c01ChkPar.getC01ChkRrc() != 0) { + outputItemError("EMP-ID ERROR EMP=" + wrkCsvEmpId); + return; // GO TO 2030VALIDATESOR-EXT 相当 + } + + // --- SUB04CHKで開始日付チェック --- + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvStartDate, 80)); + c01ChkPar.setC01ChkTyp("DATE"); + Sub04ChkSub.execute(c01ChkPar); + if (c01ChkPar.getC01ChkRrc() != 0) { + outputItemError("START-DATE ERROR EMP=" + wrkCsvEmpId + + " DATE=" + wrkCsvStartDate); + return; + } + + // --- SUB04CHKで開始時刻チェック --- + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvStartTime, 80)); + c01ChkPar.setC01ChkTyp("TIME"); + Sub04ChkSub.execute(c01ChkPar); + if (c01ChkPar.getC01ChkRrc() != 0) { + outputItemError("START-TIME ERROR EMP=" + wrkCsvEmpId + + " TIME=" + wrkCsvStartTime); + return; + } + + // --- SUB04CHKで終了日付チェック --- + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvEndDate, 80)); + c01ChkPar.setC01ChkTyp("DATE"); + Sub04ChkSub.execute(c01ChkPar); + if (c01ChkPar.getC01ChkRrc() != 0) { + outputItemError("END-DATE ERROR EMP=" + wrkCsvEmpId + + " DATE=" + wrkCsvEndDate); + return; + } + + // --- SUB04CHKで終了時刻チェック --- + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvEndTime, 80)); + c01ChkPar.setC01ChkTyp("TIME"); + Sub04ChkSub.execute(c01ChkPar); + if (c01ChkPar.getC01ChkRrc() != 0) { + outputItemError("END-TIME ERROR EMP=" + wrkCsvEmpId + + " TIME=" + wrkCsvEndTime); + return; + } + + // *** 複合条件デモ(AND+OR+3段ネスト) + // ※疑问#Q5:原代码引用W01项目(MOVE前で全てZERO、恒不成立)→ + // 設計書意図どおり WRK-CSV-* 项目を使用するよう修正。 + int sDate = Integer.parseInt(wrkCsvStartDate); + int eDate = Integer.parseInt(wrkCsvEndDate); + int sTime = Integer.parseInt(wrkCsvStartTime); + int eTime = Integer.parseInt(wrkCsvEndTime); + if (sDate != 0 && eDate != 0 && sTime != 0) { + if (sDate > eDate || (sDate == eDate && sTime >= eTime)) { + setDemoType(10); // MOVE 10 TO WRK-DEMO-TYPE. + } else { + setDemoType(20); // MOVE 20 TO WRK-DEMO-TYPE. + } + } + + // *** W01出力(新規/変更) + // ※疑问#Q2:設計書「有効時APPL-ID=0」→ INITIALIZE直後の0のまま移動しない + // (原代码は MOVE WRK-CSV-APPL-ID TO W01APPL-ID)。 + w01.setEmpId(wrkCsvEmpId); // MOVE ... TO W01EMP-ID. + w01.setLeaveType(wrkCsvLeaveType); // MOVE ... TO W01LEAVE-TYPE. + w01.setStartDate(Integer.parseInt(wrkCsvStartDate)); // MOVE ... TO W01START-DATE. + w01.setStartTime(Integer.parseInt(wrkCsvStartTime)); // MOVE ... TO W01START-TIME. + w01.setEndDate(Integer.parseInt(wrkCsvEndDate)); // MOVE ... TO W01END-DATE. + w01.setEndTime(Integer.parseInt(wrkCsvEndTime)); // MOVE ... TO W01END-TIME. + w01.setStatus(wrkCsvStatus); // MOVE ... TO W01STATUS. + writeW01(); // WRITE W01OUTREC. + } + + /** + * 項目チェックNG時の ERROR-LOG 出力(2030内5箇所の共通パターン)。 + * MOVE '01' TO W02ERR-CATEGORY./STRING ... INTO W02ERR-DETAIL./WRITE W02OUTREC. + */ + private void outputItemError(final String detail) { + w02.reset(); // INITIALIZE W02OUTREC. + w02.setErrCategory(1); // MOVE '01' TO W02ERR-CATEGORY. + w02.setErrDetail(detail); // STRING ... INTO W02ERR-DETAIL. + writeW02(); // WRITE W02OUTREC./ADD 1 TO CUN-W02OUT. + } + + /** + * 段落 2040CANCELSOR:取消申請処理。 + * 取消レコードをW01出力(検証は行わない、APPL-ID=CSV値)。 + */ + private void cancelPara() { + // *** W01レコード初期化 INITIALIZE W01OUTREC. + w01.reset(); + + // *** W01出力(取消: APPL-ID=CSV値, STATUS='9') + w01.setApplId(Integer.parseInt(wrkCsvApplId)); // MOVE ... TO W01APPL-ID. + w01.setEmpId(wrkCsvEmpId); + w01.setLeaveType(wrkCsvLeaveType); + w01.setStartDate(Integer.parseInt(wrkCsvStartDate)); + w01.setStartTime(Integer.parseInt(wrkCsvStartTime)); + w01.setEndDate(Integer.parseInt(wrkCsvEndDate)); + w01.setEndTime(Integer.parseInt(wrkCsvEndTime)); + w01.setStatus(wrkCsvStatus); + writeW01(); + } + + /** + * 段落 2050ERRORSOR:エラー処理。 + * EVALUATE WRK-ERR-TYPE(F/L/S/OTHER)によりメッセージ編集しW02出力。 + */ + private void errorPara() { + // *** W02レコード初期化 INITIALIZE W02OUTREC./MOVE '01' TO W02ERR-CATEGORY. + w02.reset(); + w02.setErrCategory(1); + + // *** エラー種別判定 EVALUATE WRK-ERR-TYPE ... END-EVALUATE. + switch (wrkErrType) { + case "F": + setDemoType(1); // MOVE 1 TO WRK-DEMO-TYPE. + setDemoValue(wrkCommaCnt); // MOVE WRK-COMMA-CNT TO WRK-DEMO-VALUE. + w02.setErrDetail("FIELD COUNT ERROR CNT=" + demoAlphaView()); + break; + case "L": + setDemoType(2); + setDemoValue(0); + w02.setErrDetail("INVALID LEAVE TYPE=" + wrkCsvLeaveType + + " EMP=" + wrkCsvEmpId + " ERR=" + demoAlphaView()); + break; + case "S": + setDemoType(3); + setDemoValue(0); + w02.setErrDetail("INVALID STATUS=" + wrkCsvStatus + + " EMP=" + wrkCsvEmpId + " ERR=" + demoAlphaView()); + break; + default: + setDemoType(9); + setDemoValue(0); + w02.setErrDetail("UNKNOWN ERROR EMP=" + wrkCsvEmpId + + " ERR=" + demoAlphaView()); + break; + } + + writeW02(); // WRITE W02OUTREC./ADD 1 TO CUN-W02OUT. + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→入出力件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + // *** 入出力ファイル CLOSE R01INNFIL W01OUTFIL W02OUTFIL. + closeFiles(); + + // *** 入出力ファイル件数出力(数値→X(30)転送は9桁ゼロ埋め表示形) + msgOutPara(CST_MSGIINKES, "KIN01R01", ConvUtil.padLeftZero(cunR01Inn, 9)); + msgOutPara(CST_MSGOUTKES, "KIN01W01", ConvUtil.padLeftZero(cunW01Out, 9)); + msgOutPara(CST_MSGOUTKES, "KIN01W02", ConvUtil.padLeftZero(cunW02Out, 9)); + + // *** 終了メッセージ出力 + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); // INITIALIZE M00MHOPAR. + m00MhoPar.setM00MsgCod(msgCod); // MOVE ... TO M00MSGCOD. + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); // MOVE ... TO M00UMKDATS22-01. + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); // MOVE ... TO M00UMKDATS22-02. + } + // MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1).(参照修改→先頭1桁上書き、残り29桁はSPACE) + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); // MOVE CNS-PRGIDX TO ...-05. + Sub02MsgSub.execute(m00MhoPar); // CALL 'SUB02MSG' USING M00MHOPAR. + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); // MOVE CNS-ABD999 TO E01ABDCOD. + Sub03EndSub.execute(e01AbdPar); // CALL 'SUB03END'. + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), "UTF-8")); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), "UTF-8")); + w02OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W02OUT_FILE), "UTF-8")); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. */ + private void writeW01() { + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** WRITE W02OUTREC./ADD 1 TO CUN-W02OUT. */ + private void writeW02() { + w02OutFil.println(w02.toRecordLine()); + cunW02Out++; + } + + /** CLOSE R01INNFIL W01OUTFIL W02OUTFIL. */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w01OutFil); + closeQuiet(w02OutFil); + r01InnFil = null; + w01OutFil = null; + w02OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } +} diff --git a/JavaSrc/src/Kin02UpdMain.java b/JavaSrc/src/Kin02UpdMain.java new file mode 100644 index 0000000..b5216ac --- /dev/null +++ b/JavaSrc/src/Kin02UpdMain.java @@ -0,0 +1,532 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import dto.Kin01Rec; +import dto.Kin05Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KIN02UPD 休暇申請DB更新処理 + * ============================================================ + * PROGRAM-ID. KIN02UPD → 类 Kin02UpdMain(PGMパターン:DB更新) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R01INNFIL(KIN01W01 WORK-LEAVE F80B) → R01INN_FILE(COPY KIN01REC → dto.Kin01Rec) + * W01OUTFIL(KIN02W01 ERROR-LOG V200B) → W01OUT_FILE(COPY KIN05REC → dto.Kin05Rec) + * LEAVE_RECORDS(SQLite: data/kin.db) + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R01INNSOR→readR01() + * 2000MAJSOR→majPara() 2100PROCSOR→procPara() 2110INSERTSOR→insertPara() + * 2120UPDATESOR→updatePara() 2130DELETESOR→deletePara() 3000STPSOR→termPara() + * 4000MSGOUTSOR→msgOutPara() 9100DBERRSOR→dbErrPara() 9999ABDSOR→abendPara() + * + * EXEC SQL → JDBC 映射(详见 DbSample 标准例): + * CONNECT TO 'data/kin.db' → DriverManager.getConnection("jdbc:sqlite:data/kin.db") + * SELECT MAX+1 → PreparedStatement.executeQuery() + * INSERT/DELETE → PreparedStatement.executeUpdate() + * COMMIT / ROLLBACK → conn.commit() / conn.rollback() + */ +public class Kin02UpdMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KIN02UPD'. */ + private static final String CST_PRGIDX = "KIN02UPD"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. サブエラー */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数 */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数 */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-STAT-1 PIC X(001) VALUE '1'. */ + private static final String CST_STAT_1 = "1"; + /** 03 CNS-STAT-9 PIC X(001) VALUE '9'. */ + private static final String CST_STAT_9 = "9"; + + // ==================== ファイル・DB定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KIN01W01.(WORK-LEAVE入力 F80B) */ + private static final String R01INN_FILE = "data/KIN01W01.txt"; + /** SELECT W01OUTFIL ASSIGN TO KIN02W01.(ERROR-LOG出力 V200B) */ + private static final String W01OUT_FILE = "data/KIN02W01.txt"; + /** EXEC SQL CONNECT TO 'data/kin.db'(SQLite JDBC URL) */ + private static final String DB_URL = "jdbc:sqlite:data/kin.db"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-DBXINS PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunDbxIns; + /** 03 CUN-DBXDEL PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunDbxDel; + /** 03 CUN-DBXUPD PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunDbxUpd; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-R01EOF PIC X(001). 読込フラグ */ + private String wrkR01Eof = "0"; + /** 03 WS-APPL-ID PIC 9(009). */ + private int wsApplId; + /** 03 WS-NEXT-APPL-ID PIC 9(009). 新規ID生成用 */ + private int wsNextApplId; + /** 03 WS-EMP-ID PIC X(008). */ + private String wsEmpId = ""; + /** 03 WS-LEAVE-TYPE PIC X(002). */ + private String wsLeaveType = ""; + /** 03 WS-START-DATE PIC X(008). */ + private String wsStartDate = ""; + /** 03 WS-START-TIME PIC X(004). */ + private String wsStartTime = ""; + /** 03 WS-END-DATE PIC X(008). */ + private String wsEndDate = ""; + /** 03 WS-END-TIME PIC X(004). */ + private String wsEndTime = ""; + /** 03 WS-STATUS PIC X(001). */ + private String wsStatus = ""; + /** 03 WRK-SQLCODE-DISP 用:直近SQLCODE */ + private int sqlcode; + /** 直近SQLエラーメッセージ(ログ用・非必須) */ + private String sqlErrMsg = ""; + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 入出力レコード(FILE SECTION → DTO) ==================== + + /** 01 R01INNREC.(FD R01INNFIL)COPY KIN01REC → R01 */ + private final Kin01Rec r01 = new Kin01Rec(); + /** 01 W01OUTREC.(FD W01OUTFIL)COPY KIN05REC → W01 */ + private final Kin05Rec w01 = new Kin05Rec(); + + // ==================== ファイル・DBハンドル ==================== + + private BufferedReader r01InnFil; + private PrintWriter w01OutFil; + private Connection conn; + + /** FD R01INNFIL の 01 R01INNREC(現在レコードバッファ)。 */ + private String r01Line = ""; + + /** 入力ファイルパス。デフォルトは SELECT ASSIGN 対応の R01INN_FILE; + * コマンドライン引数で上書き可(テスト時等に任意パスを指定)。 */ + private final String r01InnFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(String[] args) { + // 入力ファイルパス:引数指定があれば上書き、なければ R01INN_FILE(COBOL ASSIGN 映射を維持) + final String inputFile = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + new Kin02UpdMain(inputFile).run(); // PERFORM 0000MAJCOLSOR 相当 + } + + /** デフォルトコンストラクタ:入力パス=R01INN_FILE(SELECT ASSIGN 対応)。 */ + public Kin02UpdMain() { + this(R01INN_FILE); + } + + /** 引数付コンストラクタ:入力ファイルパスを指定(テスト等で使用)。 */ + public Kin02UpdMain(final String r01InnFile) { + this.r01InnFile = r01InnFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!"1".equals(wrkR01Eof)) { // PERFORM 2000MAJSOR UNTIL WRK-R01EOF = '1' + majPara(); + } + termPara(); + } finally { + // 确保所有路径(正常+异常)均关闭文件,防止资源泄漏 + closeFiles(); + } + // GOBACK → main 正常返回 + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→DB接続→OPEN(R01/W01)→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※KIN01INP#Q3 同様:WHEN-COMPILED→起動時刻代替 + String compiled = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + resetWork(); + + // *** DB接続 EXEC SQL CONNECT TO 'data/kin.db' + try { + Class.forName("org.sqlite.JDBC"); + conn = DriverManager.getConnection(DB_URL); + conn.setAutoCommit(false); // 批处理程序:显式事务控制 + } catch (ClassNotFoundException | SQLException e) { + sqlcode = (e instanceof SQLException) ? ((SQLException) e).getErrorCode() : -1; + sqlErrMsg = e.getMessage(); + // 接続失敗 → DBエラー処理(接続未確立のため ROLLBACK/W01書込は保護付き) + dbErrPara(); + } + + // *** R01ファイルOPEN(入力)+ W01ファイルOPEN(出力) + openFiles(); + + // *** 最大APPLICATION_ID取得(新規ID生成用) + wsNextApplId = selectMaxApplId(); + + // *** R01を初回読込(1100R01INNSOR) + readR01(); + } + + /** INITIALIZE WRKARA 相当(数値→0、文字→SPACE)。 */ + private void resetWork() { + wsApplId = 0; + wsNextApplId = 0; + wsEmpId = ""; + wsLeaveType = ""; + wsStartDate = ""; + wsStartTime = ""; + wsEndDate = ""; + wsEndTime = ""; + wsStatus = ""; + sqlcode = 0; + sqlErrMsg = ""; + cunR01Inn = 0; + cunDbxIns = 0; + cunDbxDel = 0; + cunDbxUpd = 0; + cunW01Out = 0; + } + + /** + * 段落 1100R01INNSOR:##R01##読込処理。 + * READ AT END / NOT AT END のマッピング。 + */ + private void readR01() { + try { + String line = r01InnFil.readLine(); + if (line == null) { + // AT END: MOVE '1' TO WRK-R01EOF. + wrkR01Eof = "1"; + } else { + // NOT AT END: R01解析→バッファ格納、ADD 1 TO CUN-R01INN. + r01.fromRecordLine(line); + cunR01Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } catch (NumberFormatException e) { + // 数値項目の非数値 → COBOL S0C7 相当。ABEND 扱い。 + sqlcode = -1; + sqlErrMsg = e.getMessage(); + dbErrPara(); + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * レコード処理→次レコード読込。 + */ + private void majPara() { + // *** レコード処理 + procPara(); + + // *** EVALUATE TRUE による分岐 + if (CST_STAT_1.equals(wsStatus) && wsApplId == 0) { + insertPara(); // WHEN STATUS='1' AND APPL-ID=0 + } else if (CST_STAT_1.equals(wsStatus) && wsApplId > 0) { + updatePara(); // WHEN STATUS='1' AND APPL-ID>0 + } else if (CST_STAT_9.equals(wsStatus)) { + deletePara(); // WHEN STATUS='9' + } else { + // WHEN OTHER: CONTINUE(何も処理しない) + } + + // *** 次レコード読込 + readR01(); + } + + /** + * 段落 2100PROCSOR:R01各項目→ホスト変数(WS-*) MOVE。 + * 数値項目は int→CHAR(8/4) 変換(padLeftZero で先頭ゼロ維持)。 + */ + private void procPara() { + wsApplId = r01.getApplId(); + wsEmpId = r01.getEmpId(); + wsLeaveType = r01.getLeaveType(); + wsStartDate = ConvUtil.padLeftZero(r01.getStartDate(), 8); // CHAR(8) YYYYMMDD + wsStartTime = ConvUtil.padLeftZero(r01.getStartTime(), 4); // CHAR(4) HHMM + wsEndDate = ConvUtil.padLeftZero(r01.getEndDate(), 8); + wsEndTime = ConvUtil.padLeftZero(r01.getEndTime(), 4); + wsStatus = r01.getStatus(); + } + + /** + * 段落 2110INSERTSOR:新規登録(INSERT)。 + * APPLICATION_ID = 生成ID(WS-NEXT-APPL-ID) を明示挿入(COBOL/使用資源一覧準拠)。 + */ + private void insertPara() { + String sql = "INSERT INTO LEAVE_RECORDS " + + "(APPLICATION_ID, EMP_ID, LEAVE_TYPE, START_DATE, START_TIME, " + + " END_DATE, END_TIME, STATUS) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + execUpdate(sql, wsNextApplId, wsEmpId, wsLeaveType, + wsStartDate, wsStartTime, wsEndDate, wsEndTime, wsStatus); + wsNextApplId++; // ADD 1 TO WS-NEXT-APPL-ID + cunDbxIns++; // ADD 1 TO CUN-DBXINS + } + + /** + * 段落 2120UPDATESOR:変更(DELETE+INSERT)。 + * APPLICATION_ID = レコード自身の WS-APPL-ID で DELETE 後、同一 ID で INSERT。 + */ + private void updatePara() { + execUpdate("DELETE FROM LEAVE_RECORDS WHERE APPLICATION_ID = ?", wsApplId); + String sql = "INSERT INTO LEAVE_RECORDS " + + "(APPLICATION_ID, EMP_ID, LEAVE_TYPE, START_DATE, START_TIME, " + + " END_DATE, END_TIME, STATUS) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + execUpdate(sql, wsApplId, wsEmpId, wsLeaveType, + wsStartDate, wsStartTime, wsEndDate, wsEndTime, wsStatus); + cunDbxUpd++; // ADD 1 TO CUN-DBXUPD + } + + /** + * 段落 2130DELETESOR:取消(DELETE)。 + */ + private void deletePara() { + execUpdate("DELETE FROM LEAVE_RECORDS WHERE APPLICATION_ID = ?", wsApplId); + cunDbxDel++; // ADD 1 TO CUN-DBXDEL + } + + /** + * EXEC SQL INSERT/DELETE の実行+SQLCODE監視。 + * SQLCODE≠0 → DBエラー処理(9100DBERRSOR)(内部で ROLLBACK+ABEND するため復帰しない)。 + */ + private void execUpdate(final String sql, final Object... params) { + try (PreparedStatement ps = conn.prepareStatement(sql)) { + for (int i = 0; i < params.length; i++) { + ps.setObject(i + 1, params[i]); // APPLICATION_ID=Integer / 他=String(CHAR) + } + ps.executeUpdate(); + } catch (SQLException e) { + sqlcode = e.getErrorCode(); + sqlErrMsg = e.getMessage(); + dbErrPara(); // 復帰しない(ABENDで終了) + } + } + + /** + * 初期処理の SELECT MAX(APPLICATION_ID)+1。 + * SQLCODE≠0 の場合、COBOL どおり 0 を返す(ABEND しない)。 + */ + private int selectMaxApplId() { + try (PreparedStatement ps = conn.prepareStatement( + "SELECT COALESCE(MAX(APPLICATION_ID), 0) + 1 FROM LEAVE_RECORDS")) { + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getInt(1); + } + } + } catch (SQLException e) { + sqlcode = e.getErrorCode(); + return 0; // IF SQLCODE NOT = 0 MOVE ZERO TO WS-NEXT-APPL-ID + } + return 0; + } + + /** + * 段落 3000STPSOR:終了処理。 + * COMMIT→CLOSE→件数メッセージ×5→終了メッセージ。 + */ + private void termPara() { + // *** COMMIT EXEC SQL COMMIT WORK + try { + if (conn != null) { + conn.commit(); + } + } catch (SQLException e) { + sqlcode = e.getErrorCode(); + sqlErrMsg = e.getMessage(); + dbErrPara(); + return; + } + + // *** 入出力ファイル CLOSE R01INNFIL W01OUTFIL. + closeFiles(); + + // *** 件数メッセージ出力(COBOL の MSGCOD 割当てに準拠) + msgOutPara(CST_MSGIINKES, "KIN01W01", ConvUtil.padLeftZero(cunR01Inn, 9)); + msgOutPara(CST_MSGIINKES, "INS", ConvUtil.padLeftZero(cunDbxIns, 9)); + msgOutPara(CST_MSGIINKES, "UPD", ConvUtil.padLeftZero(cunDbxUpd, 9)); + msgOutPara(CST_MSGOUTKES, "DEL", ConvUtil.padLeftZero(cunDbxDel, 9)); + msgOutPara(CST_MSGOUTKES, "KIN02W01", ConvUtil.padLeftZero(cunW01Out, 9)); + + // *** 終了メッセージ出力 + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 9100DBERRSOR:DBエラー処理。 + * ROLLBACK→ERROR-LOG出力(W01OUTFIL)→エラーメッセージ→ABEND(9999ABDSOR)。 + */ + private void dbErrPara() { + // *** ROLLBACK EXEC SQL ROLLBACK WORK + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException ignore) { + // ROLLBACK失敗は無視(原程序も未管理) + } + } + + // *** ERROR-LOG出力(W01OUTFIL が OPEN されている場合のみ) + if (w01OutFil != null) { + w01.reset(); // INITIALIZE W01OUTREC. + w01.setErrCategory(1); // MOVE '01' TO W01ERR-CATEGORY. + w01.setErrDetail("KIN02UPD SQLCODE=" + sqlcodeDisp() + + " APPL-ID=" + wsApplId); // STRING ... INTO W01ERR-DETAIL. + writeW01(); // WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. + } + + // *** エラーメッセージ出力(4000MSGOUTSOR 経由) + msgOutPara(CST_MSGSUBEEK, "KIN02UPD SQL ERROR", + sqlcodeDisp(), Integer.toString(wsApplId)); + + // *** ABEND処理SUB呼出(9999ABDSOR) + abendPara(); + } + + /** WRK-SQLCODE-DISP PIC +9(009) 相当:符号付9桁ゼロ埋め。 */ + private String sqlcodeDisp() { + String sign = sqlcode < 0 ? "-" : "+"; + return sign + ConvUtil.padLeftZero(Math.abs((long) sqlcode), 9); + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); // MOVE CNS-ABD999 TO E01ABDCOD. + Sub03EndSub.execute(e01AbdPar); // CALL 'SUB03END'(System.exit(999)) + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + msgOutPara(msgCod, p1, p2, null); + } + + private void msgOutPara(final int msgCod, final String p1, final String p2, final String p3) { + m00MhoPar.reset(); // INITIALIZE M00MHOPAR. + m00MhoPar.setM00MsgCod(msgCod); // MOVE ... TO M00MSGCOD. + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); // MOVE ... TO M00UMKDATS22-01. + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); // MOVE ... TO M00UMKDATS22-02. + } + if (p3 != null) { + m00MhoPar.setM00Umkdats2203(p3); // MOVE ... TO M00UMKDATS22-03. + } + // MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1).(参照修改→先頭1桁上書き、残り29桁は維持) + String p3cur = m00MhoPar.getM00Umkdats2203(); + String newP3 = (p3cur.length() == 0) ? ConvUtil.padRight(Integer.toString(CST_KN0002), 30) + : ("2" + p3cur.substring(1)); + m00MhoPar.setM00Umkdats2203(newP3); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); // MOVE CNS-PRGIDX TO ...-05. + Sub02MsgSub.execute(m00MhoPar); // CALL 'SUB02MSG' USING M00MHOPAR. + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL.(autoFlush で ABEND 前でも書込が失われない) */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), "UTF-8")); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), "UTF-8"), true); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. */ + private void writeW01() { + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** CLOSE R01INNFIL W01OUTFIL.(DB接続は finally で別途 close) */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w01OutFil); + r01InnFil = null; + w01OutFil = null; + closeQuiet(conn); + conn = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } else if (handle instanceof Connection) { + ((Connection) handle).close(); + } + } catch (Exception e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } +} diff --git a/JavaSrc/src/Kin03ExpMain.java b/JavaSrc/src/Kin03ExpMain.java new file mode 100644 index 0000000..9b49ad7 --- /dev/null +++ b/JavaSrc/src/Kin03ExpMain.java @@ -0,0 +1,417 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.TreeSet; + +import dto.Kin02Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KIN03EXP 休暇日別展開処理 + * ============================================================ + * PROGRAM-ID. KIN03EXP → 类 Kin03ExpMain(PGMパターン:キーブレイク集計) + * + * 使用DB(SQLite: data/kin.db): + * C1: LEAVE_RECORDS(STATUS='1') → 日別展開元 + * C2: HOLIDAY_CALENDAR → 休日テーブル + * 使用文件(SELECT ASSIGN → 路径常量): + * W01OUTFIL(KIN02W01.DAT LEAVE-DAILY F80B) → W01OUT_FILE(COPY KIN02REC → dto.Kin02Rec) + * 使用SUB(CALL → 类#execute): + * SUB01DAT→Sub01DatSub / SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100C1INITSOR→initC1() + * 1100C1FETCHSOR→fetchC1() 1200HDINNSOR→loadHolidays() 2000MAJSOR→majPara() + * 2100-PROCESS-EMP→majPara内 2200-EXPAND-DATE→expandDate() 2300-DATE-ADD-1→LocalDate.plusDays + * 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * ※COBOLはカーソル/SQLエラーを EOF 扱い(ABENDしない)。ABENDは SUB01DAT失敗・W01 OPEN失敗のみ。 + * ※INTEGER-OF-DATE/MOD 7 の曜日判定は Java LocalDate.getDayOfWeek() に読替(土日=週末除外、意味等価)。 + * ※SEARCH ALL(ASC KEY) は TreeSet.contains に読替(昇順ロード維持、結果等価)。 + * ※2300-DATE-ADD-1 の月末/閏年計算は LocalDate.plusDays(1) に読替(意味等価)。 + */ +public class Kin03ExpMain { + + // ==================== コンスタント領域 CNSARA ==================== + + private static final String CST_PRGIDX = "KIN03EXP"; + private static final int CST_MSGSTR = 1; + private static final int CST_MSGFIN = 2; + private static final int CST_MSGSUBEEK = 5; + private static final int CST_MSGIINKES = 6; + private static final int CST_MSGOUTKES = 7; + private static final int CST_MSGKEYINF = 33; + private static final int CST_KN0002 = 2; + private static final int CST_ABD999 = 999; + + // ==================== ファイル・DB定義 ==================== + + /** SELECT W01OUTFIL ASSIGN TO "KIN02W01.DAT". */ + private static final String W01OUT_FILE = "data/KIN02W01.DAT"; + /** EXEC SQL CONNECT TO 'data/kin.db'. */ + private static final String DB_URL = "jdbc:sqlite:data/kin.db"; + + private static final DateTimeFormatter YMD = DateTimeFormatter.ofPattern("yyyyMMdd"); + + // ==================== カウンタ領域 ==================== + + private long cunR01Inn; + private long cunW01Out; + + // ==================== 作業領域 ==================== + + private String wrkR01Eof = "0"; + /** 03 WRK-BFR-EMP-ID PIC X(008). キーブレイク用 */ + private String wrkBfrEmpId = ""; + /** 03 CUN-EMP-SUB PIC S9(009) COMP-3. 社員別小計 */ + private long empSub; + /** 直近SQLCODE(デバッグ用、ABEND非対象) */ + private int sqlcode; + + // ==================== DBホスト変数(C1 FETCH 結果) ==================== + + private int sqlApplId; + private String sqlEmpId = ""; + private String sqlLeaveType = ""; + private String sqlStartDate = ""; + private String sqlStartTime = ""; + private String sqlEndDate = ""; + private String sqlEndTime = ""; + + // ==================== 休日テーブル ==================== + + /** WRK-HOLIDAY-TABLE(OCCURS 1 TO 366, ASCENDING KEY)。SEARCH ALL → TreeSet。 */ + private final TreeSet hdSet = new TreeSet(); + /** 03 WRK-HD-COUNT PIC 9(004). */ + private int hdCount; + + // ==================== サブプログラム連絡領域 ==================== + + private final ZandatacPar d01UbsPar = new ZandatacPar(); + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 入出力レコード ==================== + + /** 01 W01OUTREC.(FD W01OUTFIL)COPY KIN02REC → W01 */ + private final Kin02Rec w01 = new Kin02Rec(); + + // ==================== ハンドル ==================== + + private Connection conn; + private PreparedStatement psC1; + private ResultSet rsC1; + private PreparedStatement psC2; + private ResultSet rsC2; + private PrintWriter w01OutFil; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(String[] args) { + new Kin03ExpMain().run(); + } + + private void run() { + try { + initPara(); + while (!"1".equals(wrkR01Eof)) { // PERFORM 2000MAJSOR UNTIL WRK-R01EOF='1' + majPara(); + } + termPara(); + } finally { + closeAll(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始/コンパイル時刻→WRK初期化→DB接続→運用日付→休日読込→W01 OPEN→C1初回FETCH。 + */ + private void initPara() { + msgOutPara(CST_MSGSTR, null, null); + + String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + resetWork(); + + // DB接続(失敗時は conn=null → 以降のカーソルが EOF 扱い、ABENDしない) + try { + Class.forName("org.sqlite.JDBC"); + conn = DriverManager.getConnection(DB_URL); + conn.setAutoCommit(false); + } catch (ClassNotFoundException | SQLException e) { + conn = null; // COBOL同様、CONNECT/カーソル失敗は EOF として処理 + } + + // 運用日付取得 CALL 'SUB01DAT' + d01UbsPar.reset(); + Sub01DatSub.execute(d01UbsPar); + if (d01UbsPar.getD01FkiCod() != 0) { + // IF D01FKICOD NOT = ZERO → メッセージ + ABEND(999) + msgOutPara(CST_MSGSUBEEK, "SUB01DAT", + ConvUtil.padLeftZero(d01UbsPar.getD01FkiCod(), 4)); + abendPara(); + } + + loadHolidays(); // C2: HOLIDAY_CALENDAR 全件ロード + openW01(); // W01 OPEN(失敗時は ABEND) + initC1(); // C1 DECLARE/OPEN/初回FETCH + } + + private void resetWork() { + cunR01Inn = 0; + cunW01Out = 0; + wrkBfrEmpId = ""; + empSub = 0; + sqlcode = 0; + hdCount = 0; + } + + /** + * 段落 1200HDINNSOR:C2 HOLIDAY_CALENDAR 読込。 + * PERFORM UNTIL SQLCODE NOT = 0 → WRK-HD-COUNT++、WRK-HD-DATE格納。 + * エラー時は件数0(COBOLはループ終了のみ)。 + */ + private void loadHolidays() { + if (conn == null) { + return; + } + try (PreparedStatement ps = conn.prepareStatement( + "SELECT HOLIDAY_DATE FROM HOLIDAY_CALENDAR ORDER BY HOLIDAY_DATE")) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + hdSet.add(rs.getString("HOLIDAY_DATE")); + hdCount++; + } + } + } catch (SQLException e) { + // エラー時は取得済み分まで(COBOLは PERFORM UNTIL SQLCODE NOT=0 で終了) + sqlcode = e.getErrorCode(); + } + } + + /** W01 OPEN(FILE STATUS 異常時は ABEND 999)。 */ + private void openW01() { + try { + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), "UTF-8"), true); + } catch (IOException e) { + abendPara(); // IF WS-W01-STATUS NOT = '00' → ABEND + } + } + + /** + * 段落 1100C1INITSOR:C1 DECLARE/OPEN/初回FETCH。 + * SQLCODE=0 → CUN-R01INN++、 ELSE → WRK-R01EOF='1'。 + */ + private void initC1() { + if (conn == null) { + wrkR01Eof = "1"; + return; + } + try { + psC1 = conn.prepareStatement( + "SELECT APPLICATION_ID, EMP_ID, LEAVE_TYPE, START_DATE, " + + " START_TIME, END_DATE, END_TIME" + + " FROM LEAVE_RECORDS WHERE STATUS = '1'" + + " ORDER BY EMP_ID, START_DATE"); + rsC1 = psC1.executeQuery(); + fetchC1(); + } catch (SQLException e) { + sqlcode = e.getErrorCode(); + wrkR01Eof = "1"; // カーソルエラーは EOF 扱い + } + } + + /** + * 段落 1100C1FETCHSOR / 1100C1INITSOR 内 FETCH。 + * SQLCODE=0 → ホスト変数格納 + CUN-R01INN++、 ELSE → WRK-R01EOF='1'。 + */ + private void fetchC1() { + if (rsC1 == null) { + wrkR01Eof = "1"; + return; + } + try { + if (rsC1.next()) { + sqlApplId = rsC1.getInt("APPLICATION_ID"); + sqlEmpId = rsC1.getString("EMP_ID"); + sqlLeaveType = rsC1.getString("LEAVE_TYPE"); + sqlStartDate = rsC1.getString("START_DATE"); + sqlStartTime = rsC1.getString("START_TIME"); + sqlEndDate = rsC1.getString("END_DATE"); + sqlEndTime = rsC1.getString("END_TIME"); + cunR01Inn++; + } else { + wrkR01Eof = "1"; + } + } catch (SQLException e) { + sqlcode = e.getErrorCode(); + wrkR01Eof = "1"; // フェッチエラーは EOF 扱い(COBOL準拠) + } + } + + /** + * 段落 2000MAJSOR:主処理(社員番号キーブレイク)。 + * SQL-EMP-ID→WRK-BFR-EMP-ID、1社員分処理(2100)→社員別小計出力。 + */ + private void majPara() { + wrkBfrEmpId = sqlEmpId; // キー保存 + empSub = 0; // CUN-EMP-SUB 初期化 + + // 2100-PROCESS-EMP:EOF またはキー変化までループ + while (!"1".equals(wrkR01Eof) && sqlEmpId.equals(wrkBfrEmpId)) { + expandDate(); // 2200-EXPAND-DATE + fetchC1(); // 次レコード FETCH + } + + // 社員別小計出力(キーブレイク) + msgOutPara(CST_MSGKEYINF, wrkBfrEmpId, Long.toString(empSub), "EMP SUB"); + } + + /** + * 段落 2200-EXPAND-DATE:開始日〜終了日を日別展開。 + * 週末(Sat/Sun)・休日を除外し、非週末かつ非休日で W01 出力。 + */ + private void expandDate() { + LocalDate cur; + LocalDate end; + try { + cur = LocalDate.parse(sqlStartDate, YMD); + end = LocalDate.parse(sqlEndDate, YMD); + } catch (DateTimeParseException e) { + return; // 日付解析不可は当該申請をスキップ(COBOLは 0/SPACE で CONTINUE 相当) + } + + while (!cur.isAfter(end)) { + DayOfWeek dow = cur.getDayOfWeek(); + if (dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY) { + // 非週末:休日テーブル検索 + String ymd = cur.format(YMD); + if (!hdSet.contains(ymd)) { + w01.reset(); + w01.setEmpId(wrkBfrEmpId); // MOVE WRK-BFR-EMP-ID + w01.setDate(Integer.parseInt(ymd)); // MOVE WRK-DATE-ALPHA + w01.setLeaveType(sqlLeaveType); // MOVE SQL-LEAVE-TYPE + w01.setStartTime(Integer.parseInt(sqlStartTime)); // MOVE SQL-START-TIME + w01.setEndTime(Integer.parseInt(sqlEndTime)); // MOVE SQL-END-TIME + w01.setApplId(sqlApplId); // MOVE NUMVAL(SQL-APPL-ID) + writeW01(); + empSub++; + } + } + cur = cur.plusDays(1); // 2300-DATE-ADD-1(LocalDate が月末/閏年を処理) + } + } + + /** + * 段落 3000STPSOR:終了処理。 + * CURSOR CLOSE→W01 CLOSE→件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + // *** 件数メッセージ出力 + msgOutPara(CST_MSGIINKES, "LEAVE_RECORDS", Long.toString(cunR01Inn)); + msgOutPara(CST_MSGOUTKES, "KIN02W01", Long.toString(cunW01Out)); + msgOutPara(CST_MSGKEYINF, "HOLIDAYS LOADED", Long.toString(hdCount)); + + // *** 終了メッセージ出力 + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + msgOutPara(msgCod, p1, p2, null); + } + + private void msgOutPara(final int msgCod, final String p1, final String p2, final String p3) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + if (p3 != null) { + m00MhoPar.setM00Umkdats2203(p3); + } + String p3cur = m00MhoPar.getM00Umkdats2203(); + String newP3 = (p3cur.length() == 0) ? ConvUtil.padRight(Integer.toString(CST_KN0002), 30) + : ("2" + p3cur.substring(1)); + m00MhoPar.setM00Umkdats2203(newP3); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理(ABENDコード 999)。 + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); // System.exit(999) + } + + // ==================== ファイル操作補助 ==================== + + /** WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. */ + private void writeW01() { + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** CURSOR / W01 / DB 接続のクローズ(finally からも呼ばれ、冪等)。 */ + private void closeAll() { + closeQuiet(rsC1); + closeQuiet(psC1); + closeQuiet(rsC2); + closeQuiet(psC2); + closeQuiet(w01OutFil); + closeQuiet(conn); + rsC1 = null; + psC1 = null; + rsC2 = null; + psC2 = null; + w01OutFil = null; + conn = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof ResultSet) { + ((ResultSet) handle).close(); + } else if (handle instanceof PreparedStatement) { + ((PreparedStatement) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } else if (handle instanceof Connection) { + ((Connection) handle).close(); + } + } catch (Exception e) { + // CLOSE異常は無視 + } + } +} diff --git a/JavaSrc/src/Kin04ChkMain.java b/JavaSrc/src/Kin04ChkMain.java new file mode 100644 index 0000000..e1fdf68 --- /dev/null +++ b/JavaSrc/src/Kin04ChkMain.java @@ -0,0 +1,521 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import dto.Kin04Rec; +import dto.Kin05Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanchkacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KIN04CHK 打刻項目チェック処理 + * ============================================================ + * PROGRAM-ID. KIN04CHK → 类 Kin04ChkMain(PGMパターン:項目チェック(重複なし)) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R01INNFIL(KIN04R01 CSV入力 F80B) → R01INN_FILE + * W01OUTFIL(KIN04W01 EDITED-PUNCH F80B) → W01OUT_FILE(COPY KIN04REC → dto.Kin04Rec) + * W02OUTFIL(KIN04W02 ERROR-LOG V200B) → W02OUT_FILE(COPY KIN05REC → dto.Kin05Rec) + * 使用SUB(CALL → 类#execute): + * SUB01DAT→Sub01DatSub / SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub / SUB04CHK→Sub04ChkSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R01INNSOR→readR01Para() + * 2000MAJSOR→majPara() 2010CSVSOR→csvPara() 2021EMPIDSOR→empIdPara() + * 2020VALIDATESOR→validatePara() 2050NORMSOR→normPara() 2090ERRORSOR→errorPara() + * 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 与原代码的差异(详见 doc\conv\KIN04CHK_对照表.md 疑问·回答记录): + * #Q1 WRK-COMMA-CNT は UNSTRING TALLYING(受信フィールド数)=フィールド数と解釈。 + * 設計書「フィールド数=6」に従い tokens.length==6 で判定、CNT=は実フィールド数を表示。 + * #Q2 FUNCTION WHEN-COMPILED → 程序启动时刻代替。 + */ +public class Kin04ChkMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KIN04CHK'. */ + private static final String CST_PRGIDX = "KIN04CHK"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. サブエラー */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数 */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数 */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-ERR-PUNCH PIC 9(002) VALUE 02. 打刻エラー区分 */ + private static final int CST_ERR_PUNCH = 2; + /** 03 CNS-FIELD-COUNT-6 PIC 9(002) VALUE 6. 期待フィールド数 */ + private static final int CST_FIELD_COUNT_6 = 6; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KIN04R01.(CSV入力) */ + private static final String R01INN_FILE = "data/KIN04R01.csv"; + /** SELECT W01OUTFIL ASSIGN TO KIN04W01.(EDITED-PUNCH出力) */ + private static final String W01OUT_FILE = "data/KIN04W01.dat"; + /** SELECT W02OUTFIL ASSIGN TO KIN04W02.(ERROR-LOG出力) */ + private static final String W02OUT_FILE = "data/KIN04W02.dat"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + /** 03 CUN-W02OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW02Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-R01EOF PIC X(001). 読込フラグ */ + private String wrkR01Eof = "0"; + /** 03 WRK-COMMA-CNT PIC 9(002) COMP. UNSTRING TALLYING(フィールド数) */ + private int wrkCommaCnt; + /** 03 WRK-CSV-EMP-ID PIC X(008). */ + private String wrkCsvEmpId = ""; + /** 03 WRK-CSV-DATE PIC X(008). */ + private String wrkCsvDate = ""; + /** 03 WRK-CSV-TIME-IN PIC X(004). */ + private String wrkCsvTimeIn = ""; + /** 03 WRK-CSV-TIME-OUT PIC X(004). */ + private String wrkCsvTimeOut = ""; + /** 03 WRK-CSV-DEPT-ID PIC X(004). */ + private String wrkCsvDeptId = ""; + /** 03 WRK-CSV-TERMINAL PIC X(006). */ + private String wrkCsvTerminal = ""; + /** 03 WRK-TIME-IN-NUM PIC 9(004). */ + private int wrkTimeInNum; + /** 03 WRK-TIME-OUT-NUM PIC 9(004). */ + private int wrkTimeOutNum; + /** 03 WRK-COMMA-DISP PIC 9(002). エラー表示用 */ + private int wrkCommaDisp; + /** 03 WRK-EMPID-OK PIC X(001). 社員番号チェック結果 */ + private String wrkEmpIdOk = "0"; + + /** 88 WRK-EMPID-IS-OK VALUE '1'. */ + private boolean isEmpIdOk() { + return "1".equals(wrkEmpIdOk); + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANDATAC.(SUB01DAT用) */ + private final ZandatacPar d01UbsPar = new ZandatacPar(); + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + /** COPY ZANCHKAC.(SUB04CHK用) */ + private final ZanchkacPar c01ChkPar = new ZanchkacPar(); + + // ==================== 出力レコード(FILE SECTION → DTO) ==================== + + /** 01 W01OUTREC.(FD W01OUTFIL) */ + private final Kin04Rec w01 = new Kin04Rec(); + /** 01 W02OUTREC.(FD W02OUTFIL) */ + private final Kin05Rec w02 = new Kin05Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private BufferedReader r01InnFil; + private PrintWriter w01OutFil; + private PrintWriter w02OutFil; + + /** FD R01INNFIL の 01 R01INNREC. 03 R01LINE PIC X(80).(現在レコードバッファ) */ + private String r01Line = ""; + + /** 入力ファイルパス。デフォルトは SELECT ASSIGN 対応の R01INN_FILE; + * コマンドライン引数で上書き可(テスト時等に任意パスを指定)。 */ + private final String r01InnFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(String[] args) { + // 入力ファイルパス:引数指定があれば上書き、なければ R01INN_FILE(COBOL ASSIGN 映射を維持) + final String inputFile = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + new Kin04ChkMain(inputFile).run(); // PERFORM 0000MAJCOLSOR 相当 + } + + /** デフォルトコンストラクタ:入力パス=R01INN_FILE(SELECT ASSIGN 対応)。 */ + public Kin04ChkMain() { + this(R01INN_FILE); + } + + /** 引数付コンストラクタ:入力ファイルパスを指定(テスト等で使用)。 */ + public Kin04ChkMain(final String r01InnFile) { + this.r01InnFile = r01InnFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!"1".equals(wrkR01Eof)) { // PERFORM 2000MAJSOR UNTIL WRK-R01EOF = '1' + majPara(); + } + termPara(); + } finally { + // 确保所有路径(正常+异常)均关闭文件,防止资源泄漏 + closeFiles(); + } + // GOBACK → main 正常返回 + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→運用日付取得→OPEN→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※疑问#Q2:WHEN-COMPILED→启动时刻代替 + String compiled = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + initWrk(); + + // *** 運用日付取得 CALL 'SUB01DAT' USING D01UBSPAR. + d01UbsPar.reset(); // INITIALIZE D01UBSPAR. + Sub01DatSub.execute(d01UbsPar); + if (d01UbsPar.getD01FkiCod() != 0) { + // IF D01FKICOD NOT = ZERO → メッセージ出力後 ABEND + msgOutPara(CST_MSGSUBEEK, "SUB01DAT", + ConvUtil.padLeftZero(d01UbsPar.getD01FkiCod(), 4)); // S9(4) COMP→4桁表示 + abendPara(); + } + + // *** 入出力ファイル OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. + openFiles(); + + // *** #R01#を読み込み(1回目) + readR01Para(); + } + + /** INITIALIZE WRKARA 相当。 */ + private void initWrk() { + wrkR01Eof = "0"; + wrkCommaCnt = 0; + wrkCsvEmpId = ""; + wrkCsvDate = ""; + wrkCsvTimeIn = ""; + wrkCsvTimeOut = ""; + wrkCsvDeptId = ""; + wrkCsvTerminal = ""; + wrkTimeInNum = 0; + wrkTimeOutNum = 0; + wrkCommaDisp = 0; + wrkEmpIdOk = "0"; + } + + /** + * 段落 1100R01INNSOR:##R01##読込処理。 + * READ AT END / NOT AT END のマッピング。 + */ + private void readR01Para() { + try { + String line = r01InnFil.readLine(); + if (line == null) { + // AT END: MOVE '1' TO WRK-R01EOF. + wrkR01Eof = "1"; + } else { + // NOT AT END: ADD 1 TO CUN-R01INN.(レコードバッファへ格納) + r01Line = line; + cunR01Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * CSV分解→社員番号チェック→IF多重ネスト(句)4段階チェック→次レコード読込。 + */ + private void majPara() { + // *** CSV分解 + csvPara(); + + // *** 社員番号チェック + empIdPara(); + + // *** 項目チェック(IF多重ネスト 句) + validatePara(); + + // *** 次のレコード読込 + readR01Para(); + } + + /** + * 段落 2010CSVSOR:CSV分解処理。 + * UNSTRING DELIMITED BY ',' INTO 6項目 TALLYING IN WRK-COMMA-CNT. + */ + private void csvPara() { + // MOVE ZERO TO WRK-COMMA-CNT./INITIALIZE 各CSV項目. + wrkCommaCnt = 0; + wrkCsvEmpId = ""; + wrkCsvDate = ""; + wrkCsvTimeIn = ""; + wrkCsvTimeOut = ""; + wrkCsvDeptId = ""; + wrkCsvTerminal = ""; + + // UNSTRING R01INNREC DELIMITED BY ',' INTO ... (split(-1)で空項目保持) + String[] tokens = r01Line.split(",", -1); + // 先頭6項目を受信領域へ(PIC X(n) 左詰め・右空白埋め=UNSTRING転送規則) + wrkCsvEmpId = ConvUtil.padRight(tok(tokens, 0), 8); + wrkCsvDate = ConvUtil.padRight(tok(tokens, 1), 8); + wrkCsvTimeIn = ConvUtil.padRight(tok(tokens, 2), 4); + wrkCsvTimeOut = ConvUtil.padRight(tok(tokens, 3), 4); + wrkCsvDeptId = ConvUtil.padRight(tok(tokens, 4), 4); + wrkCsvTerminal = ConvUtil.padRight(tok(tokens, 5), 6); + // TALLYING IN WRK-COMMA-CNT:受信フィールド数=tokens.length(疑问#Q1)。 + wrkCommaCnt = tokens.length; + } + + /** UNSTRING受信領域への転送補助(項目不足時は空文字=未転送扱い)。 */ + private static String tok(final String[] tokens, final int idx) { + return idx < tokens.length ? tokens[idx] : ""; + } + + /** + * 段落 2021EMPIDSOR:社員番号チェック処理。 + * SUB04CHK('EMPID')により社員番号の妥当性チェックを実施する。 + */ + private void empIdPara() { + // MOVE '0' TO WRK-EMPID-OK. + wrkEmpIdOk = "0"; + // IF WRK-COMMA-CNT = CNS-FIELD-COUNT-6(フィールド数=6)のときのみ実施 + if (wrkCommaCnt == CST_FIELD_COUNT_6) { + // INITIALIZE C01CHKPAR./MOVE ... TO C01CHKDAT./MOVE 'EMPID' TO C01CHKTYP. + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvEmpId, 80)); + c01ChkPar.setC01ChkTyp("EMPID"); + Sub04ChkSub.execute(c01ChkPar); // CALL 'SUB04CHK' USING C01CHKPAR. + if (c01ChkPar.getC01ChkRrc() == 0) { + wrkEmpIdOk = "1"; // MOVE '1' TO WRK-EMPID-OK. + } + } + } + + /** + * 段落 2020VALIDATESOR:項目チェック(IF多重ネスト 句)。 + * 第1段階: フィールド数 + 社員番号 → 第2段階: 日付 → 第3段階: 出勤時刻/退勤時刻 + * → 第4段階: 出勤<退勤 の4段階ネストチェックを句で記述。 + */ + private void validatePara() { + // *** 第1段階: フィールド数 + 社員番号必須 + if (wrkCommaCnt == CST_FIELD_COUNT_6 && isEmpIdOk()) { + + // *** 第2段階: 日付チェック(SUB04CHK) + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvDate, 80)); + c01ChkPar.setC01ChkTyp("DATE"); + Sub04ChkSub.execute(c01ChkPar); + if (c01ChkPar.getC01ChkRrc() == 0) { + + // *** 第3段階: 出勤時刻チェック(SUB04CHK) + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvTimeIn, 80)); + c01ChkPar.setC01ChkTyp("TIME"); + Sub04ChkSub.execute(c01ChkPar); + if (c01ChkPar.getC01ChkRrc() == 0) { + + // *** 第3段階: 退勤時刻チェック(逆順)→ここで退勤時刻をチェック + c01ChkPar.reset(); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(wrkCsvTimeOut, 80)); + c01ChkPar.setC01ChkTyp("TIME"); + Sub04ChkSub.execute(c01ChkPar); + if (c01ChkPar.getC01ChkRrc() == 0) { + + // *** 第4段階: 出勤<退勤チェック + // MOVE WRK-CSV-TIME-IN TO WRK-TIME-IN-NUM./MOVE WRK-CSV-TIME-OUT TO WRK-TIME-OUT-NUM. + wrkTimeInNum = Integer.parseInt(wrkCsvTimeIn); + wrkTimeOutNum = Integer.parseInt(wrkCsvTimeOut); + if (wrkTimeInNum < wrkTimeOutNum) { + normPara(); // PERFORM 2050NORMSOR. + } else { + errorPara(); // PERFORM 2090ERRORSOR. + } + } else { + errorPara(); + } + } else { + errorPara(); + } + } else { + errorPara(); + } + } else { + errorPara(); + } + } + + /** + * 段落 2050NORMSOR:正常出力処理。 + * WORKING-STORAGEに編集し、WRITE FROMでW01(EDITED-PUNCH)に出力。 + */ + private void normPara() { + // *** WORKING-STORAGEに編集(O1* 項目へ MOVE) + // ここでは直接 W01OUTREC へ編集(WRITE FROM WS-W01REC と等価)。 + w01.reset(); // INITIALIZE W01OUTREC. + w01.setEmpId(wrkCsvEmpId); // MOVE ... TO O1EMP-ID. + w01.setWorkDate(wrkCsvDate); // MOVE ... TO O1WORK-DATE. + w01.setStrTime(wrkCsvTimeIn); // MOVE ... TO O1STR-TIME. + w01.setEndTime(wrkCsvTimeOut); // MOVE ... TO O1END-TIME. + w01.setDeptId(wrkCsvDeptId); // MOVE ... TO O1DEPT-ID. + w01.setTerminal(wrkCsvTerminal); // MOVE ... TO O1TERMINAL. + writeW01(); // WRITE W01OUTREC FROM WS-W01REC. + } + + /** + * 段落 2090ERRORSOR:エラー処理。 + * エラーレコードをW02(ERROR-LOG)に出力(ERR-CATEGORY='02')。 + */ + private void errorPara() { + // *** W02レコード初期化 INITIALIZE W02OUTREC. + w02.reset(); + w02.setErrCategory(CST_ERR_PUNCH); // MOVE CNS-ERR-PUNCH TO W02ERR-CATEGORY. + + // *** エラー種別判定 + if (wrkCommaCnt != CST_FIELD_COUNT_6) { + // フィールド数不一致 + wrkCommaDisp = wrkCommaCnt; // MOVE WRK-COMMA-CNT TO WRK-COMMA-DISP. + w02.setErrDetail("FIELD COUNT ERR CNT=" + + ConvUtil.padLeftZero(wrkCommaDisp, 2)); + } else if (wrkCsvEmpId.trim().isEmpty()) { + // 社員番号が空 + w02.setErrDetail("EMP-ID EMPTY"); + } else { + // その他バリデーションエラー + w02.setErrDetail("VALIDATION ERR EMP=" + wrkCsvEmpId + + " DATE=" + wrkCsvDate + + " TI=" + wrkCsvTimeIn + + " TO=" + wrkCsvTimeOut); + } + + writeW02(); // WRITE W02OUTREC./ADD 1 TO CUN-W02OUT. + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→入出力件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + // *** 入出力ファイル CLOSE R01INNFIL W01OUTFIL W02OUTFIL. + closeFiles(); + + // *** 入出力ファイル件数出力(数値→X(30)転送は9桁ゼロ埋め表示形) + msgOutPara(CST_MSGIINKES, "KIN04R01", ConvUtil.padLeftZero(cunR01Inn, 9)); + msgOutPara(CST_MSGOUTKES, "KIN04W01", ConvUtil.padLeftZero(cunW01Out, 9)); + msgOutPara(CST_MSGOUTKES, "KIN04W02", ConvUtil.padLeftZero(cunW02Out, 9)); + + // *** 終了メッセージ出力 + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); // INITIALIZE M00MHOPAR. + m00MhoPar.setM00MsgCod(msgCod); // MOVE ... TO M00MSGCOD. + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); // MOVE ... TO M00UMKDATS22-01. + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); // MOVE ... TO M00UMKDATS22-02. + } + // MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1).(参照修改→先頭1桁上書き、残り29桁はSPACE) + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); // MOVE CNS-PRGIDX TO ...-05. + Sub02MsgSub.execute(m00MhoPar); // CALL 'SUB02MSG' USING M00MHOPAR. + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); // MOVE CNS-ABD999 TO E01ABDCOD. + Sub03EndSub.execute(e01AbdPar); // CALL 'SUB03END'. + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), "UTF-8")); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), "UTF-8")); + w02OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W02OUT_FILE), "UTF-8")); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. */ + private void writeW01() { + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** WRITE W02OUTREC./ADD 1 TO CUN-W02OUT. */ + private void writeW02() { + w02OutFil.println(w02.toRecordLine()); + cunW02Out++; + } + + /** CLOSE R01INNFIL W01OUTFIL W02OUTFIL. */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w01OutFil); + closeQuiet(w02OutFil); + r01InnFil = null; + w01OutFil = null; + w02OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } +} diff --git a/JavaSrc/src/Kin05MatMain.java b/JavaSrc/src/Kin05MatMain.java new file mode 100644 index 0000000..113eed2 --- /dev/null +++ b/JavaSrc/src/Kin05MatMain.java @@ -0,0 +1,566 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import dto.Kin02Rec; +import dto.Kin03Rec; +import dto.Kin04Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KIN05MAT 打刻休暇照合処理 + * ============================================================ + * PROGRAM-ID. KIN05MAT → 类 Kin05MatMain(PGMパターン:マッチング(1:N)) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R01INNFIL(KIN04W01 EDITED-PUNCH F80B) → R01INN_FILE(COPY KIN04REC → dto.Kin04Rec) + * R02INNFIL(KIN02W01 LEAVE-DAILY F80B) → R02INN_FILE(COPY KIN02REC → dto.Kin02Rec) + * W01OUTFIL(KIN05W01 KIN-LEAVE F160B)→ W01OUT_FILE(COPY KIN03REC → dto.Kin03Rec) + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R01INNSOR→readR01Para() + * 1200R02INNSOR→readR02Para() 2000MAJSOR→majPara() 2020MATCHSOR→matchPara() + * 2030PRIOSOR→prioSor() 2100R01ONLYSOR→r01OnlyPara() 2200R02ONLYSOR→r02OnlyPara() + * 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 与原代码的差异(详见 doc\conv\KIN05MAT_对照表.md 疑问·回答记录): + * #Q1 照合キー(EMP-ID+DATE)はグループ項目のバイト比較と等価なため、文字列連結で比較。 + * #Q2 優先順位選定(2030PRIOSOR)は COBOL の EVALUATE TRUE を if/else で忠実に再現。 + * #Q3 COBOL は WRK-BEST-* を 2020MATCHSOR と 1000ITTSOR のみ初期化し、 + * R01単独出力(2100R01ONLYSOR)ではリセットしないため、前件マッチの休暇情報が + * 継続する不具合がある(設計書 2-1/3-5「R01単独→LEAVE-TYPE='99'」と矛盾)。 + * ⇒ Java は【設計書準拠】とし、每条 R01 処理前(majPara先頭)に WRK-BEST-* を + * '99'/0 に初期化する(resetBest())。COBOL の不整合は本ファイル内コメントで指摘。 + * #Q4 FUNCTION WHEN-COMPILED → 程序启动时刻代替。 + */ +public class Kin05MatMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KIN05MAT'. */ + private static final String CST_PRGIDX = "KIN05MAT"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数 */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数 */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-NO-LEAVE PIC X(002) VALUE '99'. 休暇なし */ + private static final String CST_NO_LEAVE = "99"; + /** 03 CNS-LEAVE-01 PIC X(002) VALUE '01'. */ + private static final String CST_LEAVE_01 = "01"; + /** 03 CNS-LEAVE-02 PIC X(002) VALUE '02'. */ + private static final String CST_LEAVE_02 = "02"; + /** 03 CNS-LEAVE-03 PIC X(002) VALUE '03'. */ + private static final String CST_LEAVE_03 = "03"; + /** 03 CNS-LEAVE-04 PIC X(002) VALUE '04'. */ + private static final String CST_LEAVE_04 = "04"; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KIN04W01.(EDITED-PUNCH入力) */ + private static final String R01INN_FILE = "data/KIN04W01.dat"; + /** SELECT R02INNFIL ASSIGN TO KIN02W01.(LEAVE-DAILY入力) */ + private static final String R02INN_FILE = "data/KIN02W01.dat"; + /** SELECT W01OUTFIL ASSIGN TO KIN05W01.(KIN-LEAVE出力) */ + private static final String W01OUT_FILE = "data/KIN05W01.dat"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-R02INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR02Inn; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WS-R01-STATUS PIC X(002).(OPENチェック用、Javaは例外で代替) */ + private String wsR01Status = "00"; + /** 03 WS-R02-STATUS PIC X(002). */ + private String wsR02Status = "00"; + /** 03 WS-W01-STATUS PIC X(002). */ + private String wsW01Status = "00"; + /** 03 WRK-R01EOF PIC X(001). 読込フラグ */ + private String wrkR01Eof = "0"; + /** 03 WRK-R02EOF PIC X(001). 読込フラグ */ + private String wrkR02Eof = "0"; + /** 03 WRK-R01KEY(EMP-ID 8 + DATE 8) */ + private String wrkR01Key = ""; + /** 03 WRK-R02KEY(EMP-ID 8 + DATE 8) */ + private String wrkR02Key = ""; + /** 03 WRK-BEST-TYPE PIC X(002). 選定休暇種別 */ + private String wrkBestType = CST_NO_LEAVE; + /** 03 WRK-BEST-STR-TIME PIC 9(004). */ + private int wrkBestStrTime; + /** 03 WRK-BEST-END-TIME PIC 9(004). */ + private int wrkBestEndTime; + /** 03 WRK-BEST-APPL-ID PIC 9(009). */ + private int wrkBestApplId; + /** 03 WRK-BEST-FOUND PIC X(001). 選定済フラグ */ + private String wrkBestFound = "0"; + + /** 88 WRK-BEST-IS-FOUND VALUE '1'. */ + private boolean isBestFound() { + return "1".equals(wrkBestFound); + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 入出力レコード(FILE SECTION → DTO) ==================== + + /** FD R01INNFIL の 01 R01INNREC.(現在レコードバッファ) */ + private final Kin04Rec r01 = new Kin04Rec(); + /** FD R02INNFIL の 01 R02OUTREC.(現在レコードバッファ) */ + private final Kin02Rec r02 = new Kin02Rec(); + /** FD W01OUTFIL の 01 W01OUTREC. */ + private final Kin03Rec w01 = new Kin03Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private BufferedReader r01InnFil; + private BufferedReader r02InnFil; + private PrintWriter w01OutFil; + + /** 入力ファイルパス(R01/R02/W01)。引数で上書き可。 */ + private final String r01InnFile; + private final String r02InnFile; + private final String w01OutFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(String[] args) { + final String r1 = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + final String r2 = (args != null && args.length > 1) ? args[1] : R02INN_FILE; + final String w1 = (args != null && args.length > 2) ? args[2] : W01OUT_FILE; + new Kin05MatMain(r1, r2, w1).run(); // PERFORM 0000MAJCOLSOR 相当 + } + + /** 引数付コンストラクタ:各ファイルパスを指定(テスト等で使用)。 */ + public Kin05MatMain(final String r01InnFile, final String r02InnFile, final String w01OutFile) { + this.r01InnFile = r01InnFile; + this.r02InnFile = r02InnFile; + this.w01OutFile = w01OutFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL R01-EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!"1".equals(wrkR01Eof)) { // PERFORM 2000MAJSOR UNTIL WRK-R01-EOF + majPara(); + } + termPara(); + } finally { + closeFiles(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→BEST領域初期化→OPEN→OPENチェック→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※疑问#Q4:WHEN-COMPILED→启动时刻代替 + String compiled = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + wrkR01Eof = "0"; + wrkR02Eof = "0"; + wrkR01Key = ""; + wrkR02Key = ""; + // *** BEST領域初期化(R01のみ出力時に使用) + wrkBestFound = "0"; // MOVE '0' TO WRK-BEST-FOUND. + wrkBestType = CST_NO_LEAVE; // MOVE CNS-NO-LEAVE TO WRK-BEST-TYPE. + wrkBestStrTime = 0; // MOVE ZERO TO WRK-BEST-STR-TIME. + wrkBestEndTime = 0; // MOVE ZERO TO WRK-BEST-END-TIME. + wrkBestApplId = 0; // MOVE ZERO TO WRK-BEST-APPL-ID. + + // *** 入出力ファイル OPEN INPUT R01/R02 / OUTPUT W01. + openFiles(); + + // *** OPENチェック(STATUS≠'00' → ABEND 999) + if (!"00".equals(wsR01Status) || !"00".equals(wsR02Status) || !"00".equals(wsW01Status)) { + abendPara(); + } + + // *** R01/R02を読み込み + readR01Para(); + readR02Para(); + } + + /** + * 段落 1100R01INNSOR:##R01##読込処理。 + * READ AT END / NOT AT END のマッピング。 + */ + private void readR01Para() { + try { + String line = r01InnFil.readLine(); + if (line == null) { + wrkR01Eof = "1"; // AT END: MOVE '1' TO WRK-R01EOF. + } else { + r01.fromRecordLine(line); // FD R01INNREC への転送. + cunR01Inn++; // ADD 1 TO CUN-R01INN. + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 1200R02INNSOR:##R02##読込処理。 + * READ AT END / NOT AT END のマッピング. AT END→EOF、NOT AT END→件数++. + */ + private void readR02Para() { + try { + String line = r02InnFil.readLine(); + if (line == null) { + wrkR02Eof = "1"; // AT END: MOVE '1' TO WRK-R02EOF. + } else { + r02.fromRecordLine(line); // FD R02OUTREC への転送. + cunR02Inn++; // ADD 1 TO CUN-R02INN. + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理(マッチング制御)。 + * R01キー設定→R02キー設定(有効時)→EVALUATE TRUE で3分岐。 + */ + private void majPara() { + // *** 設計書準拠:每条打刻(R01)は独立に判定。COBOL(KIN05MAT.cbl)は + // WRK-BEST-* を 2020MATCHSOR と 1000ITTSOR のみ初期化し、 + // R01単独出力(2100R01ONLYSOR)ではリセットしないため、前件マッチの + // 休暇情報が継続する不具合がある。Java は設計書意図(='99')どおり、 + // 每条 R01 処理前に WRK-BEST-* を初期化する。 + resetBest(); + + // *** R01キー設定 + setR01Key(); + // *** R02キー設定(有効な場合) + if (!"1".equals(wrkR02Eof)) { + setR02Key(); + } + + // *** EVALUATE TRUE マッチング3分岐(COBOL の WHEN 順序を if/else で再現) + if ("1".equals(wrkR02Eof)) { + // WHEN WRK-R02-EOF → R01のみ出力+R01次読込 + r01OnlyPara(); + readR01Para(); + } else if (wrkR01Key.compareTo(wrkR02Key) < 0) { + // WHEN WRK-R01KEY < WRK-R02KEY → R01のみ出力+R01次読込 + r01OnlyPara(); + readR01Para(); + } else if (wrkR01Key.compareTo(wrkR02Key) == 0) { + // WHEN WRK-R01KEY = WRK-R02KEY → マッチング処理+R01次読込 + matchPara(); + readR01Para(); + } else { + // WHEN WRK-R01KEY > WRK-R02KEY → R02のみスキップ+R02次読込 + r02OnlyPara(); + readR02Para(); + } + } + + /** WRK-R01KEY = R01EMP-ID(8) + R01WORK-DATE(8)(グループ比較と等価)。 */ + private void setR01Key() { + wrkR01Key = r01.getEmpId() + r01.getWorkDate(); + } + + /** WRK-R02KEY = R02EMP-ID(8) + R02DATE(8)(グループ比較と等価)。 */ + private void setR02Key() { + wrkR02Key = r02.getEmpId() + ConvUtil.padLeftZero(r02.getDate(), 8); + } + + /** + * WRK-BEST-* 初期化('99'/0)。 + * 設計書(KIN05MAT)準拠:每条打刻(R01)の処理前に実施し、前件マッチの + * 休暇情報が継続しないよう保証する。COBOL は本初期化を 2020MATCHSOR と + * 1000ITTSOR のみに記述し R01単独出力で漏れたため、無休假打刻に前件の + * 休暇種別が残る不具合がある(#Q3)。 + */ + private void resetBest() { + wrkBestFound = "0"; // MOVE '0' TO WRK-BEST-FOUND. + wrkBestType = CST_NO_LEAVE; // MOVE CNS-NO-LEAVE TO WRK-BEST-TYPE. + wrkBestStrTime = 0; // MOVE ZERO TO WRK-BEST-STR-TIME. + wrkBestEndTime = 0; // MOVE ZERO TO WRK-BEST-END-TIME. + wrkBestApplId = 0; // MOVE ZERO TO WRK-BEST-APPL-ID. + } + + /** + * 段落 2020MATCHSOR:マッチング処理(N:1ループレ)。 + * 一致キーの全R02を読込み、最適休暇を選定した上で KIN-LEAVE 出力。 + * ※ majPara() 先頭の resetBest() で既に '99'/0 初期化済だが、COBOL 互換の + * 位置でも再初期化(冪等)。設計書準拠の挙動は majPara() 側で保証される。 + */ + private void matchPara() { + // *** 選定領域初期化(majPara() での初期化と同一内容) + resetBest(); + + // *** N:1ループ 一致するR02を全件処理 + while (true) { + if ("1".equals(wrkR02Eof)) { + break; // WRK-R02-EOF + } + if (!wrkR01Key.equals(wrkR02Key)) { + break; // WRK-R01KEY < WRK-R02KEY OR WRK-R01KEY > WRK-R02KEY + } + // *** ベスト選定 + if (!isBestFound()) { + // 初回 → ベストとして保持 + wrkBestFound = "1"; // MOVE '1' TO WRK-BEST-FOUND. + wrkBestType = r02.getLeaveType(); + wrkBestStrTime = r02.getStartTime(); + wrkBestEndTime = r02.getEndTime(); + wrkBestApplId = r02.getApplId(); + } else { + prioSor(r02.getLeaveType(), r02.getStartTime(), + r02.getEndTime(), r02.getApplId()); + } + // *** R02次読込 + readR02Para(); + // *** R02キー再設定(有効な場合) + if (!"1".equals(wrkR02Eof)) { + setR02Key(); + } + } + + // *** KIN-LEAVE出力(R01のみ出力サブルーチンを共用) + r01OnlyPara(); + } + + /** + * 段落 2030PRIOSOR:優先順位判定。 + * 優先順位 01 > 02 > 03 > 04。同種別は APPL-ID の小さい方を採用。 + * COBOL の EVALUATE TRUE を if/else で忠実に再現(WHEN OTHER→CONTINUE)。 + */ + private void prioSor(final String r02Type, final int r02Str, + final int r02End, final int r02Appl) { + // EVALUATE TRUE(R02LEAVE-TYPE で分岐) + if (CST_LEAVE_01.equals(r02Type)) { + if (!CST_LEAVE_01.equals(wrkBestType)) { + wrkBestType = r02Type; + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } else if (r02Appl < wrkBestApplId) { + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } + } else if (CST_LEAVE_02.equals(r02Type)) { + if (CST_LEAVE_01.equals(wrkBestType)) { + // CONTINUE(01 が優先) + } else if (CST_LEAVE_02.equals(wrkBestType)) { + if (r02Appl < wrkBestApplId) { + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } + } else { + wrkBestType = r02Type; + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } + } else if (CST_LEAVE_03.equals(r02Type)) { + if (CST_LEAVE_01.equals(wrkBestType)) { + // CONTINUE + } else if (CST_LEAVE_02.equals(wrkBestType)) { + // CONTINUE + } else if (CST_LEAVE_03.equals(wrkBestType)) { + if (r02Appl < wrkBestApplId) { + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } + } else { + wrkBestType = r02Type; + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } + } else if (CST_LEAVE_04.equals(r02Type)) { + if (CST_LEAVE_01.equals(wrkBestType)) { + // CONTINUE + } else if (CST_LEAVE_02.equals(wrkBestType)) { + // CONTINUE + } else if (CST_LEAVE_03.equals(wrkBestType)) { + // CONTINUE + } else if (CST_LEAVE_04.equals(wrkBestType)) { + if (r02Appl < wrkBestApplId) { + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } + } else { + wrkBestType = r02Type; + wrkBestStrTime = r02Str; + wrkBestEndTime = r02End; + wrkBestApplId = r02Appl; + } + } else { + // WHEN OTHER → CONTINUE(未知種別は無視) + } + } + + /** + * 段落 2100R01ONLYSOR:R01のみ出力(またはマッチ結果出力)。 + * 打刻情報(1-80B)+休暇情報(81-160B、WRK-BEST-* を使用)を KIN-LEAVE に出力。 + */ + private void r01OnlyPara() { + // *** KIN-LEAVEレコード編集 + w01.reset(); // INITIALIZE W01OUTREC. + // *** 打刻情報(1-80B) + w01.setEmpId(r01.getEmpId()); // MOVE R01EMP-ID TO W01EMP-ID. + w01.setWorkDate(r01.getWorkDate()); // MOVE R01WORK-DATE TO W01WORK-DATE. + w01.setStrTime(r01.getStrTime()); // MOVE R01STR-TIME TO W01STR-TIME. + w01.setEndTime(r01.getEndTime()); // MOVE R01END-TIME TO W01END-TIME. + w01.setDeptId(r01.getDeptId()); // MOVE R01DEPT-ID TO W01DEPT-ID. + w01.setTerminal(r01.getTerminal()); // MOVE R01TERMINAL TO W01TERMINAL. + // *** 休暇情報(81-160B) + w01.setLeaveType(wrkBestType); // MOVE WRK-BEST-TYPE TO W01LEAVE-TYPE. + w01.setLeaveStrTime(wrkBestStrTime); // MOVE WRK-BEST-STR-TIME TO W01LEAVE-STR-TIME. + w01.setLeaveEndTime(wrkBestEndTime); // MOVE WRK-BEST-END-TIME TO W01LEAVE-END-TIME. + w01.setLeaveHours(0); // MOVE ZERO TO W01LEAVE-HOURS. + w01.setApplicationId(wrkBestApplId); // MOVE WRK-BEST-APPL-ID TO W01APPLICATION-ID. + + writeW01(); // WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. + } + + /** + * 段落 2200R02ONLYSOR:R02のみスキップ(該当打刻のない休暇レコードは出力しない)。 + */ + private void r02OnlyPara() { + // PERFORM 1200R02INNSOR は呼出元(majPara)で実施済(CONTINUE)。 + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→入出力件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + // *** 入出力ファイル CLOSE + closeFiles(); + + // *** 入出力ファイル件数出力(数値→X(30)転送は9桁ゼロ埋め表示形) + msgOutPara(CST_MSGIINKES, "KIN04W01", ConvUtil.padLeftZero(cunR01Inn, 9)); + msgOutPara(CST_MSGIINKES, "KIN02W01", ConvUtil.padLeftZero(cunR02Inn, 9)); + msgOutPara(CST_MSGOUTKES, "KIN05W01", ConvUtil.padLeftZero(cunW01Out, 9)); + + // *** 終了メッセージ出力 + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); // INITIALIZE M00MHOPAR. + m00MhoPar.setM00MsgCod(msgCod); // MOVE ... TO M00MSGCOD. + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); // MOVE ... TO M00UMKDATS22-01. + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); // MOVE ... TO M00UMKDATS22-02. + } + // MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1).(参照修改→先頭1桁上書き、残り29桁はSPACE) + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); // MOVE CNS-PRGIDX TO ...-05. + Sub02MsgSub.execute(m00MhoPar); // CALL 'SUB02MSG' USING M00MHOPAR. + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); // MOVE CNS-ABD999 TO E01ABDCOD. + Sub03EndSub.execute(e01AbdPar); // CALL 'SUB03END'. + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R01INNFIL/R02INNFIL / OUTPUT W01OUTFIL(失敗時は STATUS='99'→ABEND)。 */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), "UTF-8")); + r02InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r02InnFile), "UTF-8")); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(w01OutFile), "UTF-8")); + } catch (IOException e) { + // OPEN失敗相当:FILE STATUS ≠ '00'(後続の OPENチェックで ABEND 999) + wsR01Status = "99"; + wsR02Status = "99"; + wsW01Status = "99"; + } + } + + /** WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. */ + private void writeW01() { + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** CLOSE R01INNFIL R02INNFIL W01OUTFIL. */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(r02InnFil); + closeQuiet(w01OutFil); + r01InnFil = null; + r02InnFil = null; + w01OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } +} diff --git a/JavaSrc/src/Kin06CldMain.java b/JavaSrc/src/Kin06CldMain.java new file mode 100644 index 0000000..c95e0c3 --- /dev/null +++ b/JavaSrc/src/Kin06CldMain.java @@ -0,0 +1,371 @@ +import dto.Kin06Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +/** + * ============================================================ + * 【COBOL→Java】KIN06CLD 出勤日カレンダー生成処理 + * ============================================================ + * PROGRAM-ID. KIN06CLD → 类 Kin06CldMain + * PGMパターン:レイアウト編集のみ(GETPUT) + * 機能概要:PARM(YEARMONTH=YYYYMM) 指定年月の出勤日カレンダーを生成する。 + * 在籍社員(EMP_MASTER.STATUS='1') × 当月1日〜月末日をループし、 + * 休日判定(祝日テーブル線形探索 + 曜日判定)を行い、出勤日のみ + * WORK-DAY-FILE(KIN06W01.DAT) に出力する。 + * + * ※COBOL構造は main で受け取る引数(args)を YEARMONTH= 解析に使用。 + * COBOL「ACCEPT WRK-CMDLINE FROM COMMAND-LINE」はコマンドライン全体を取得するが、 + * 本Java実装では args を空白連結した文字列から "YEARMONTH=" をスキャンする。 + */ +public class Kin06CldMain { + + /** CNS-PRGIDX VALUE 'KIN06CLD'. */ + private static final String CNS_PRGIDX = "KIN06CLD"; + /** CNS-MSGSTR VALUE 001(開始). */ + private static final int CNS_MSGSTR = 1; + /** CNS-MSGFIN VALUE 002(終了). */ + private static final int CNS_MSGFIN = 2; + /** CNS-MSGOUTKES VALUE 007(出力件数). */ + private static final int CNS_MSGOUTKES = 7; + /** CNS-MSGKEYINF VALUE 033(コンパイル日時). */ + private static final int CNS_MSGKEYINF = 33; + /** CNS-KN0002 VALUE 2(メッセージ置換項目定数). */ + private static final String CNS_KN0002 = "2"; + /** CNS-ABD999 VALUE 999. */ + private static final int CNS_ABD999 = 999; + + /** SELECT OUTRECFIL ASSIGN TO "data/KIN06W01.DAT". */ + private static final String OUTREC_FILE = "data/KIN06W01.DAT"; + + /** 月日数テーブル(MONTH-DAYS / MONTH-DAY OCCURS 12、0-indexed). */ + private static final int[] MONTH_DAYS = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + }; + + // ---- ワークエリア(WRKARA)相当 ---- + private int wrkYear; + private int wrkMonth; + private int wrkLastDay; + + /** CUN-OUTREC PIC S9(009) COMP-3. */ + private long cunOutrec; + + /** WRK-YEARMONTH(PARM解析値 YYYYMM、祝日SELECTのLIKEフィルタに使用). */ + private String wrkYearMonth; + + /** 休日セット(HOLIDAY-TABLE 相当、祝日文字列 YYYYMMDD を保持). */ + private final Set holidaySet = new HashSet<>(); + + /** メッセージ編集出力SR用連絡領域(ZANMSGAC). */ + private final ZanmsgacPar msgPar = new ZanmsgacPar(); + /** ABEND処理SR用連絡領域(ZANENDAC). */ + private final ZanendacPar endPar = new ZanendacPar(); + + /** 出力ファイルライター. */ + private BufferedWriter outrecFil; + + /** DB接続. */ + private Connection conn; + + public static void main(final String[] args) { + new Kin06CldMain().run(args); + } + + private void run(final String[] args) { + initProc(args); + majProc(); + stpProc(); + } + + // ***************************************************************** + // 初期処理 1000ITTSOR + // ***************************************************************** + private void initProc(final String[] args) { + // 開始メッセージ出力(MSG 001) + msgOutPara(CNS_MSGSTR, "", "", ""); + + // コンパイル日時出力(MSG 033) + msgOutPara(CNS_MSGKEYINF, "COMPILED", "", ""); + + // PARM解析(1100PARMSOR) + parmProc(args); + + // DB接続 + try { + Class.forName("org.sqlite.JDBC"); + conn = DriverManager.getConnection("jdbc:sqlite:data/kin.db"); + } catch (Exception e) { + throw new RuntimeException("DB接続失敗: " + e.getMessage(), e); + } + + // 休日データ読込(1200HOLIDAYSOR) + holidayProc(); + + // 出力ファイルOPEN + try { + outrecFil = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(OUTREC_FILE), StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException("出力ファイルOPEN失敗: " + e.getMessage(), e); + } + + // 社員CURSOR OPEN(1300EMPOPNSOR) + empOpenProc(); + } + + // ***************************************************************** + // PARM解析 1100PARMSOR + // ***************************************************************** + private void parmProc(final String[] args) { + // COBOL: ACCEPT WRK-CMDLINE FROM COMMAND-LINE → 本実装は args 連結文字列を使用 + StringBuilder cmdLine = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + cmdLine.append(' '); + } + cmdLine.append(args[i]); + } + String line = cmdLine.toString(); + + // "YEARMONTH=" をスキャン(COBOL: PERFORM VARYING で先頭10桁一致を検索) + int pos = line.indexOf("YEARMONTH="); + boolean found = pos >= 0; + + String yearMonthStr = ""; + if (found) { + int valStart = pos + "YEARMONTH=".length(); + if (valStart + 6 <= line.length()) { + yearMonthStr = line.substring(valStart, valStart + 6); + } else { + yearMonthStr = line.substring(valStart); + } + } + + // PARM必須チェック(COBOL: IF NOT WRK-PARM-FOUND → ABEND 999) + if (!found || yearMonthStr.length() != 6) { + abend(); + return; + } + + // 年・月分解(COBOL: WRK-YEARMONTH(1:4)=YEAR, (5:2)=MONTH) + int yearMonth; + try { + yearMonth = Integer.parseInt(yearMonthStr); + } catch (NumberFormatException e) { + // 非数値 → 月チェックで ABEND 相当 + abend(); + return; + } + wrkYear = yearMonth / 100; + wrkMonth = yearMonth % 100; + wrkYearMonth = yearMonthStr; + + // 月チェック(1-12)(COBOL: IF WRK-MONTH < 1 OR > 12 → ABEND 999) + if (wrkMonth < 1 || wrkMonth > 12) { + abend(); + return; + } + + // 月の日数計算(COBOL: MOVE MONTH-DAY(WRK-MONTH) TO WRK-LAST-DAY) + wrkLastDay = MONTH_DAYS[wrkMonth - 1]; + + // 閏年判定(2月のみ)(COBOL: MOD(YEAR,400)=0 OR (MOD(YEAR,4)=0 AND MOD(YEAR,100)!=0)) + if (wrkMonth == 2) { + if ((wrkYear % 400 == 0) || (wrkYear % 4 == 0 && wrkYear % 100 != 0)) { + wrkLastDay = 29; + } + } + } + + // ***************************************************************** + // 休日データ読込 1200HOLIDAYSOR + // ***************************************************************** + private void holidayProc() { + // 設計書: SELECT HOLIDAY_DATE FROM HOLIDAY_CALENDAR + // WHERE HOLIDAY_DATE LIKE 'YYYYMM%'(対象月のみ) + // ガード: 対象月祝日が50件未満の場合のみ ADD(テーブル溢れ防止) + String sql = "SELECT HOLIDAY_DATE FROM HOLIDAY_CALENDAR " + + "WHERE HOLIDAY_DATE LIKE ? ORDER BY HOLIDAY_DATE"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, wrkYearMonth + "%"); + try (ResultSet rs = ps.executeQuery()) { + int count = 0; + while (rs.next()) { + if (count >= 50) { + break; + } + String holidayDate = rs.getString("HOLIDAY_DATE"); + if (holidayDate != null) { + holidaySet.add(holidayDate.trim()); + count++; + } + } + } + } catch (Exception e) { + throw new RuntimeException("休日読込失敗: " + e.getMessage(), e); + } + } + + // ***************************************************************** + // 社員CURSOR OPEN 1300EMPOPNSOR + // ***************************************************************** + private void empOpenProc() { + // COBOL: SELECT EMP_ID FROM EMP_MASTER WHERE STATUS='1' ORDER BY EMP_ID + // 初回FETCH → SQLCODE != 0 なら EMP-EOF='1' + String sql = "SELECT EMP_ID FROM EMP_MASTER WHERE STATUS = '1' ORDER BY EMP_ID"; + try { + empStmt = conn.prepareStatement(sql); + empRs = empStmt.executeQuery(); + if (!empRs.next()) { + empEof = true; + } + } catch (Exception e) { + throw new RuntimeException("社員CURSOR OPEN失敗: " + e.getMessage(), e); + } + } + + // 社員CURSOR用ステートメント・結果セット + private PreparedStatement empStmt; + private ResultSet empRs; + /** WRK-EMP-EOF PIC X(001) VALUE '1' → empEof=true. */ + private boolean empEof; + + // ***************************************************************** + // 主処理 2000MAJSOR(社員EOFまで繰り返す) + // ***************************************************************** + private void majProc() { + while (!empEof) { + final String hvEmpId; + try { + hvEmpId = empRs.getString("EMP_ID"); + } catch (Exception e) { + throw new RuntimeException("社員取得失敗: " + e.getMessage(), e); + } + + // 当月1日〜月末日をループ + for (int wrkDay = 1; wrkDay <= wrkLastDay; wrkDay++) { + // 日付文字列構築(COBOL: WRK-DATE-STR = YEAR*10000 + MONTH*100 + DAY) + int wrkDateStr = wrkYear * 10000 + wrkMonth * 100 + wrkDay; + String wrkDateStrX = ConvUtil.padLeftZero(wrkDateStr, 8); + + // 曜日計算(COBOL: INTEGER-OF-DATE / MOD(7) → 本実装は LocalDate.getDayOfWeek で等価) + // COBOL: 0=日,1=月…6=土 → OUTDAY-OF-WEEK: 0→7, 1..6 そのまま + // Java : DayOfWeek.getValue() = 1=月…7=日(上記変換と同一) + LocalDate ld = LocalDate.of(wrkYear, wrkMonth, wrkDay); + int dayOfWeek = ld.getDayOfWeek().getValue(); + DayOfWeek dow = ld.getDayOfWeek(); + + // 休日判定(COBOL: PERFORM VARYING 線形探索 → Set.contains で等価) + boolean isHoliday = holidaySet.contains(wrkDateStrX); + + // 出勤日のみ出力(土曜=6 または 日曜=7 または 祝日 → スキップ) + if (isHoliday || dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) { + // CONTINUE(スキップ) + } else { + Kin06Rec outRec = new Kin06Rec(); + outRec.setEmpId(hvEmpId); + outRec.setDate(wrkDateStr); + outRec.setDayOfWeek(dayOfWeek); + outRec.setFiller(""); + writeOutrec(outRec.toRecordLine()); + cunOutrec++; + } + } + + // 次の社員をFETCH(COBOL: FETCH C_EMP INTO :HV-EMP-ID) + try { + if (!empRs.next()) { + empEof = true; + } + } catch (Exception e) { + throw new RuntimeException("社員FETCH失敗: " + e.getMessage(), e); + } + } + } + + // ***************************************************************** + // 終了処理 3000STPSOR + // ***************************************************************** + private void stpProc() { + // カーソルCLOSE + try { + if (empRs != null) { + empRs.close(); + } + if (empStmt != null) { + empStmt.close(); + } + } catch (Exception ignore) { + // クローズ失敗は無視(COBOL同様) + } + + // DB切断 + try { + if (conn != null) { + conn.close(); + } + } catch (Exception ignore) { + // 無視 + } + + // 出力ファイルCLOSE + try { + if (outrecFil != null) { + outrecFil.close(); + } + } catch (Exception ignore) { + // 無視 + } + + // 出力件数メッセージ(MSG 007: P1=KIN06W01, P2=件数) + msgOutPara(CNS_MSGOUTKES, "KIN06W01", ConvUtil.padLeftZero(cunOutrec, 9), ""); + + // 終了メッセージ出力(MSG 002) + msgOutPara(CNS_MSGFIN, "", "", ""); + } + + // ***************************************************************** + // メッセージ編集出力 4000MSGOUTSOR(SUB02MSG 呼出) + // ***************************************************************** + private void msgOutPara(final int msgCod, final String p1, final String p2, final String p3) { + msgPar.reset(); + msgPar.setM00MsgCod(msgCod); + // COBOL: MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1) / -04(1:1) / PRGIDX TO -05 + msgPar.setM00Umkdats2203(CNS_KN0002); + msgPar.setM00Umkdats2204(CNS_KN0002); + msgPar.setM00Umkdats2205(CNS_PRGIDX); + msgPar.setM00Umkdats2201(p1); + msgPar.setM00Umkdats2202(p2); + Sub02MsgSub.execute(msgPar); + } + + /** WRITE OUTREC. */ + private void writeOutrec(final String line) { + try { + outrecFil.write(line); + outrecFil.newLine(); + } catch (Exception e) { + throw new RuntimeException("出力失敗: " + e.getMessage(), e); + } + } + + /** ABEND処理(COBOL: MOVE 999 TO E01ABDCOD, CALL 'SUB03END'). */ + private void abend() { + endPar.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(endPar); + } +} diff --git a/JavaSrc/src/Kin07DaiMain.java b/JavaSrc/src/Kin07DaiMain.java new file mode 100644 index 0000000..df29765 --- /dev/null +++ b/JavaSrc/src/Kin07DaiMain.java @@ -0,0 +1,450 @@ +import dto.Kin02Rec; +import dto.Kin03Rec; +import dto.Kin05Rec; +import dto.Kin06Rec; +import dto.Kin07Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import dto.ZantimacPar; +import util.ConvUtil; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * ============================================================ + * 【COBOL→Java】KIN07DAI 日別勤怠計算処理 + * ============================================================ + * PROGRAM-ID. KIN07DAI → 类 Kin07DaiMain + * PGMパターン:マッチング(1:N) + EVALUATE 4パターン分岐 + * 機能概要:WORK-DAY-FILE(R01)を主駆動に、KIN-LEAVE(R02)とLEAVE-DAILY(R03)を + * 社員番号+日付キーで照合し、4パターン(A/B/C/D)に分岐して + * 日別勤怠記録(DAILY-RECORD / KIN07W01.DAT)を出力する。 + * 休暇時間はランチ除外 + 0.1h切上丸め(SUB05TIM)。 + * + * ※入力ファイルは「社員番号+日付 昇順」でソート済みであること(設計書前提)。 + */ +public class Kin07DaiMain { + + /** CNS-PRGIDX VALUE 'KIN07DAI'. */ + private static final String CNS_PRGIDX = "KIN07DAI"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final String CNS_KN0002 = "2"; + /** CNS-ABD999 VALUE 999. */ + private static final int CNS_ABD999 = 999; + + /** 休暇種別定数 */ + private static final String CNS_LEAVE_01 = "01"; + private static final String CNS_LEAVE_02 = "02"; + private static final String CNS_LEAVE_03 = "03"; + private static final String CNS_LEAVE_04 = "04"; + + /** 時刻定数(分) */ + private static final int CNS_LUNCH_START_MIN = 720; // 12:00 + private static final int CNS_LUNCH_END_MIN = 780; // 13:00 + /** CNS-DAILY-HOURS 8.0h → 0.1h単位整数 = 80 */ + private static final int CNS_DAILY_HOURS_TENTH = 80; + /** CNS-RND-MODE-UP = 1(0.1h切上) */ + private static final int CNS_RND_MODE_UP = 1; + + /** 入力ファイル(R01=WORK-DAY-FILE=KIN06CLD出力 / R02=KIN-LEAVE / R03=LEAVE-DAILY). */ + private static final String R01_FILE = "data/KIN06W01.DAT"; + private static final String R02_FILE = "data/KIN07R02.DAT"; + private static final String R03_FILE = "data/KIN07R03.DAT"; + /** 出力ファイル(W01=DAILY-RECORD 200B). */ + private static final String W01_FILE = "data/KIN07W01.DAT"; + /** 出力ファイル(W02=ERROR-LOG / KIN05REC / VB 200B). */ + private static final String W02_FILE = "data/KIN07E01.DAT"; + + // ---- カウンタ ---- + private long cunR01Inn; + private long cunR02Inn; + private long cunR03Inn; + private long cunW01Out; + private long cunW02Out; + + private final ZanmsgacPar msgPar = new ZanmsgacPar(); + private final ZanendacPar endPar = new ZanendacPar(); + + private BufferedWriter w01OutFil; + private BufferedWriter w02OutFil; + + /** 主処理で使用する入力リスト(initProc で一括読込)。 */ + private List r02List; + private List r03List; + + public static void main(final String[] args) { + new Kin07DaiMain().run(); + } + + private void run() { + List r01List = initProc(); + majProc(r01List); + stpProc(); + } + + // ***************************************************************** + // 初期処理 1000ITTSOR + // ***************************************************************** + private List initProc() { + // 開始メッセージ + msgOutPara(CNS_MSGSTR, "", "", ""); + // コンパイル日時 + msgOutPara(CNS_MSGKEYINF, "COMPILED", "", ""); + + // 入力ファイル読込(R01/R02/R03) + List r01List = readR01(); + r02List = readR02(); + r03List = readR03(); + + // 出力ファイルOPEN + try { + w01OutFil = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(W01_FILE), StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException("出力ファイルOPEN失敗: " + e.getMessage(), e); + } + + // W02(ERROR-LOG) OPEN(設計書使用ファイル一覧 No.5) + try { + w02OutFil = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(W02_FILE), StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException("W02出力ファイルOPEN失敗: " + e.getMessage(), e); + } + + return r01List; + } + + // ***************************************************************** + // 主処理 2000MAJSOR(R01駆動マージ) + // ***************************************************************** + private void majProc(final List r01List) { + int r02Idx = 0; + int r03Idx = 0; + int r02Size = r02List.size(); + int r03Size = r03List.size(); + + for (final Kin06Rec r01 : r01List) { + cunR01Inn++; + String r01Key = buildKey(r01.getEmpId(), r01.getDate()); + + // 積算・編集領域初期化 + int wrkTimeIn = 0; + int wrkTimeOut = 0; + int annualH = 0; + int personalH = 0; + int officialH = 0; + int sickH = 0; + int pattern = 4; // PATTERN-D + + // ---- R02: 追越レコードをスキップ ---- + while (r02Idx < r02Size) { + String r02Key = buildKey(r02List.get(r02Idx).getEmpId(), + parseIntDate(r02List.get(r02Idx).getWorkDate())); + if (r02Key.compareTo(r01Key) >= 0) { + break; + } + r02Idx++; + cunR02Inn++; + } + // ---- R02 マッチ判定 ---- + if (r02Idx < r02Size) { + String r02Key = buildKey(r02List.get(r02Idx).getEmpId(), + parseIntDate(r02List.get(r02Idx).getWorkDate())); + if (r02Key.equals(r01Key)) { + Kin03Rec r02 = r02List.get(r02Idx); + wrkTimeIn = ConvUtil.parseIntSafe(r02.getStrTime(), 0); + wrkTimeOut = ConvUtil.parseIntSafe(r02.getEndTime(), 0); + pattern = 2; // PATTERN-B + r02Idx++; + cunR02Inn++; + } + } + + // ---- R03: 追越レコードをスキップ ---- + while (r03Idx < r03Size) { + String r03Key = buildKey(r03List.get(r03Idx).getEmpId(), + r03List.get(r03Idx).getDate()); + if (r03Key.compareTo(r01Key) >= 0) { + break; + } + r03Idx++; + cunR03Inn++; + } + // ---- R03 マッチループ(0〜N件の全休暇を処理)---- + if (r03Idx < r03Size) { + String r03Key = buildKey(r03List.get(r03Idx).getEmpId(), + r03List.get(r03Idx).getDate()); + if (r03Key.equals(r01Key)) { + while (r03Idx < r03Size) { + Kin02Rec r03 = r03List.get(r03Idx); + String k3 = buildKey(r03.getEmpId(), r03.getDate()); + if (!k3.equals(r01Key)) { + break; + } + int lvH = calcLeaveHours(r03.getStartTime(), r03.getEndTime()); + String lt = r03.getLeaveType(); + if (CNS_LEAVE_01.equals(lt)) { + annualH += lvH; + } else if (CNS_LEAVE_02.equals(lt)) { + personalH += lvH; + } else if (CNS_LEAVE_03.equals(lt)) { + officialH += lvH; + } else if (CNS_LEAVE_04.equals(lt)) { + sickH += lvH; + } else { + // 未知休暇種別 → 設計書に規定なし。COBOL 同様に無視(W02 非出力、休暇時間へ計上せず)。 + } + r03Idx++; + cunR03Inn++; + } + if (pattern == 2) { + pattern = 1; // PATTERN-A(打刻+休暇) + } else { + pattern = 3; // PATTERN-C(休暇のみ) + } + } + } + + // ---- EVALUATE 4パターン分岐 → 出力 ---- + switch (pattern) { + case 1: + writeW01(r01, wrkTimeIn, wrkTimeOut, annualH, personalH, officialH, sickH, 0); + break; + case 2: + writeW01(r01, wrkTimeIn, wrkTimeOut, 0, 0, 0, 0, 0); + break; + case 3: + // 打刻なし、休暇時間は8h上限 + if (annualH > CNS_DAILY_HOURS_TENTH) { + annualH = CNS_DAILY_HOURS_TENTH; + } + if (personalH > CNS_DAILY_HOURS_TENTH) { + personalH = CNS_DAILY_HOURS_TENTH; + } + if (officialH > CNS_DAILY_HOURS_TENTH) { + officialH = CNS_DAILY_HOURS_TENTH; + } + if (sickH > CNS_DAILY_HOURS_TENTH) { + sickH = CNS_DAILY_HOURS_TENTH; + } + writeW01(r01, 0, 0, annualH, personalH, officialH, sickH, 0); + break; + case 4: + default: + // 打刻なし、未申請欠勤=8.0h + writeW01(r01, 0, 0, 0, 0, 0, 0, CNS_DAILY_HOURS_TENTH); + break; + } + } + } + + // ***************************************************************** + // 終了処理 3000STPSOR + // ***************************************************************** + private void stpProc() { + try { + if (w01OutFil != null) { + w01OutFil.close(); + } + } catch (Exception ignore) { + // 無視 + } + + try { + if (w02OutFil != null) { + w02OutFil.close(); + } + } catch (Exception ignore) { + // 無視 + } + + // 入力件数出力(MSG 006) + msgOutPara(CNS_MSGIINKES, "KIN07R01", ConvUtil.padLeftZero(cunR01Inn, 9), ""); + msgOutPara(CNS_MSGIINKES, "KIN07R02", ConvUtil.padLeftZero(cunR02Inn, 9), ""); + msgOutPara(CNS_MSGIINKES, "KIN07R03", ConvUtil.padLeftZero(cunR03Inn, 9), ""); + // 出力件数出力(MSG 007) + msgOutPara(CNS_MSGOUTKES, "KIN07W01", ConvUtil.padLeftZero(cunW01Out, 9), ""); + // W02 出力件数(エラー件数) + msgOutPara(CNS_MSGOUTKES, "KIN07E01", ConvUtil.padLeftZero(cunW02Out, 9), ""); + // 終了メッセージ + msgOutPara(CNS_MSGFIN, "", "", ""); + } + + // ***************************************************************** + // 休暇時間計算 5000CALCLVSOR + // 開始/終了時刻(HHMM) → ランチ除外 → 0.1h切上丸め(SUB05TIM) + // 戻り値: 0.1h単位整数(tenth) + // ***************************************************************** + private int calcLeaveHours(final int startTime, final int endTime) { + int startHour = startTime / 100; + int startMin = startTime % 100; + int endHour = endTime / 100; + int endMin = endTime % 100; + + int startMinTotal = startHour * 60 + startMin; + int endMinTotal = endHour * 60 + endMin; + + int totalMin = endMinTotal - startMinTotal; + + // ランチ除外(12:00-13:00 = 720-780分) + int lunchOverlap = 0; + if (endMinTotal > CNS_LUNCH_START_MIN && startMinTotal < CNS_LUNCH_END_MIN) { + int lo = Math.min(endMinTotal, CNS_LUNCH_END_MIN); + int hi = Math.max(startMinTotal, CNS_LUNCH_START_MIN); + lunchOverlap = lo - hi; + } + totalMin -= lunchOverlap; + + // 時間(小数)に変換(9(4)V9(1) への COMPUTE は 0.1h で切捨) + int tenth = totalMin / 6; // floor(totalMin / 60 * 10) = floor(totalMin / 6) + + // SUB05TIM 呼出(0.1h切上) + ZantimacPar tp = new ZantimacPar(); + tp.setT01Timhrs(tenth / 10.0); + tp.setT01Timrrc(CNS_RND_MODE_UP); + Sub05TimSub.execute(tp); + return (int) Math.round(tp.getT01Timout() * 10.0); + } + + // ***************************************************************** + // DAILY-RECORD 出力 7000W01OUTSOR + // ***************************************************************** + private void writeW01(final Kin06Rec r01, final int timeIn, final int timeOut, + final int annualH, final int personalH, final int officialH, + final int sickH, final int absentH) { + Kin07Rec out = new Kin07Rec(); + out.setEmpId(r01.getEmpId()); + out.setDate(r01.getDate()); + out.setTimeIn(timeIn); + out.setTimeOut(timeOut); + out.setAnnualH(annualH); + out.setPersonalH(personalH); + out.setOfficialH(officialH); + out.setSickH(sickH); + out.setAbsentH(absentH); + out.setFiller(""); + try { + w01OutFil.write(out.toRecordLine()); + w01OutFil.newLine(); + cunW01Out++; + } catch (Exception e) { + throw new RuntimeException("出力失敗: " + e.getMessage(), e); + } + } + + // ***************************************************************** + // ユーティリティ + // ***************************************************************** + private String buildKey(final String empId, final int date) { + return (empId == null ? "" : empId) + ConvUtil.padLeftZero(date, 8); + } + + private int parseIntDate(final String dateStr) { + return ConvUtil.parseIntSafe(dateStr, 0); + } + + private List readR01() { + List list = new ArrayList<>(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(R01_FILE), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty()) { + continue; + } + Kin06Rec rec = new Kin06Rec(); + rec.fromRecordLine(line); + list.add(rec); + } + } catch (Exception e) { + throw new RuntimeException("R01読込失敗(" + R01_FILE + "): " + e.getMessage(), e); + } + return list; + } + + private List readR02() { + List list = new ArrayList<>(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(R02_FILE), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty()) { + continue; + } + Kin03Rec rec = new Kin03Rec(); + rec.fromRecordLine(line); + list.add(rec); + } + } catch (Exception e) { + throw new RuntimeException("R02読込失敗(" + R02_FILE + "): " + e.getMessage(), e); + } + return list; + } + + private List readR03() { + List list = new ArrayList<>(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(R03_FILE), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty()) { + continue; + } + Kin02Rec rec = new Kin02Rec(); + rec.fromRecordLine(line); + list.add(rec); + } + } catch (Exception e) { + throw new RuntimeException("R03読込失敗(" + R03_FILE + "): " + e.getMessage(), e); + } + return list; + } + + /** メッセージ編集出力 4000MSGOUTSOR(SUB02MSG 呼出). */ + private void msgOutPara(final int msgCod, final String p1, final String p2, final String p3) { + msgPar.reset(); + msgPar.setM00MsgCod(msgCod); + msgPar.setM00Umkdats2203(CNS_KN0002); + msgPar.setM00Umkdats2204(CNS_KN0002); + msgPar.setM00Umkdats2205(CNS_PRGIDX); + msgPar.setM00Umkdats2201(p1); + msgPar.setM00Umkdats2202(p2); + Sub02MsgSub.execute(msgPar); + } + + // ***************************************************************** + // W02 ERROR-LOG 出力(設計書: W02ERR-CATEGORY='99' / W02ERR-DETAIL=詳細) + // 呼出元: SUB05TIM異常時・未知休暇種別等のエラー検出時 + // ***************************************************************** + private void writeW02(final String detail) { + Kin05Rec err = new Kin05Rec(); + err.setErrCategory(99); + err.setErrDetail(detail); + try { + w02OutFil.write(err.toRecordLine()); + w02OutFil.newLine(); + cunW02Out++; + } catch (Exception e) { + throw new RuntimeException("W02出力失敗: " + e.getMessage(), e); + } + } + + /** ABEND処理(9999ABDSOR 相当)。 */ + private void abend() { + endPar.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(endPar); + } +} diff --git a/JavaSrc/src/Kin08DbuMain.java b/JavaSrc/src/Kin08DbuMain.java new file mode 100644 index 0000000..322b36a --- /dev/null +++ b/JavaSrc/src/Kin08DbuMain.java @@ -0,0 +1,467 @@ +import dto.Kin07Rec; +import dto.Kin08Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; + +/** + * ============================================================ + * 【COBOL→Java】KIN08DBU 勤怠DB更新処理 + * ============================================================ + * PROGRAM-ID. KIN08DBU → 类 Kin08DbuMain + * PGMパターン:DB更新 + SYSIN読込(P28) + * 機能概要:DAILY-RECORD(R01/KIN07REC)を読込み DAILY_RECORDS へ INSERT。 + * 社員別月次集計(AGGREGATION-TABLE)後、MONTHLY_ABSENCE へ UPSERT。 + * 集計結果を ABSENCE-SUMMARY(W01/KIN08REC) に出力。 + * SYSIN制御カード(T/P/M)で対象社員・対象年月・モードを指定。 + */ +public class Kin08DbuMain { + + private static final String CNS_PRGIDX = "KIN08DBU"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final String CNS_KN0002 = "2"; + private static final int CNS_ABD999 = 999; + + /** SELECT R01INNFIL ASSIGN TO "KIN08R01". */ + private static final String R01_FILE = "data/KIN08R01.DAT"; + /** SELECT SYSINFILE ASSIGN TO "KIN08S01". */ + private static final String SYSIN_FILE = "data/KIN08S01.DAT"; + /** SELECT W01OUTFIL ASSIGN TO "KIN08W01". */ + private static final String W01_FILE = "data/KIN08W01.DAT"; + + // ---- カウンタ ---- + private long cunR01Inn; + private long cunDbxIns; + private long cunDbxUpd; + private long cunDbxDel; + private long cunW01Out; + + // ---- SYSIN 解析結果 ---- + /** Tカード対象社員(指定なし=全社員)。 */ + private final List targetList = new ArrayList<>(); + private boolean allEmp = true; + /** Pカード対象年月。 */ + private String yearMonth = ""; + private boolean periodFound = false; + /** Mカード RESET モード。 */ + private boolean resetMode = false; + + // ---- 集計テーブル(AGGREGATION-TABLE 相当、tenth 単位で保持)---- + private static class AggRow { + String empId; + String yearMonth; + int annualH; + int personalH; + int officialH; + int sickH; + int absentH; + /** WS-TOTAL-HOURS(設計書: COMPUTE ROUNDED + ON SIZE ERROR、出力には非含). */ + int totalH; + } + + private final List aggTable = new ArrayList<>(); + + private final ZanmsgacPar msgPar = new ZanmsgacPar(); + private final ZanendacPar endPar = new ZanendacPar(); + + private BufferedWriter w01OutFil; + private Connection conn; + + public static void main(final String[] args) { + new Kin08DbuMain().run(); + } + + private void run() { + initProc(); + majProc(); + stpProc(); + } + + // ***************************************************************** + // 初期処理 1000ITTSOR + // ***************************************************************** + private void initProc() { + msgOutPara(CNS_MSGSTR, "", "", ""); + msgOutPara(CNS_MSGKEYINF, "COMPILED", "", ""); + + // ワーク初期化 + allEmp = true; + resetMode = false; + + // DB接続 + try { + Class.forName("org.sqlite.JDBC"); + conn = DriverManager.getConnection("jdbc:sqlite:data/kin.db"); + conn.setAutoCommit(false); + } catch (Exception e) { + throw new RuntimeException("DB接続失敗: " + e.getMessage(), e); + } + + // SYSIN読込 + readSysin(); + + // 必須パラメータ(対象年月)未設定チェック + if (!periodFound) { + abend(); + } + + // 入出力ファイルOPEN + try { + w01OutFil = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(W01_FILE), StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException("出力ファイルOPEN失敗: " + e.getMessage(), e); + } + } + + // ***************************************************************** + // SYSIN 読込・解析(1199-SYSIN-LOOP / 1110/1120/1130) + // ***************************************************************** + private void readSysin() { + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(SYSIN_FILE), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty()) { + continue; + } + char card = line.charAt(0); + String body = line.length() > 2 ? line.substring(2) : ""; + if (card == 'T') { + // Tカード: 対象社員一覧(カンマ区切り) + targetList.clear(); + String[] emps = body.split(","); + for (String e : emps) { + String t = e.trim(); + if (!t.isEmpty()) { + targetList.add(t); + } + } + allEmp = false; + } else if (card == 'P') { + // Pカード: YEARMONTH=YYYYMM + String[] kv = body.split("="); + String val = kv.length > 1 ? kv[1].trim() : ""; + if (val.length() >= 6) { + yearMonth = val.substring(0, 6); + periodFound = true; + } + } else if (card == 'M') { + // Mカード: MODE=RESET|NORMAL + String mode = body.replace("MODE=", "").trim(); + resetMode = "RESET".equals(mode); + } else if (card == '*') { + // コメント行は無視 + } else { + System.out.println("WARNING: Unknown card type [" + card + "]"); + } + } + } catch (Exception e) { + throw new RuntimeException("SYSIN読込失敗: " + e.getMessage(), e); + } + } + + // ***************************************************************** + // 主処理 2000MAJSOR + // ***************************************************************** + private void majProc() { + // 設計書(使用DBテーブル): RESET = DELETE + INSERT + // → DAILY_RECORDS 挿入前に既存レコードを削除(stpProc 後だと PK 違反) + if (resetMode) { + resetDelete(); + } + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(R01_FILE), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty()) { + continue; + } + cunR01Inn++; + Kin07Rec rec = new Kin07Rec(); + rec.fromRecordLine(line); + + // 社員番号有効性チェック(空ならスキップ) + if (rec.getEmpId() == null || rec.getEmpId().trim().isEmpty()) { + System.out.println("WARNING: Empty EMP-ID record " + cunR01Inn); + continue; + } + + String empId = rec.getEmpId(); + String targetDate = String.format("%08d", rec.getDate()); + String timeIn = ConvUtil.padLeftZero(rec.getTimeIn(), 4); + String timeOut = ConvUtil.padLeftZero(rec.getTimeOut(), 4); + double annualH = rec.getAnnualH() / 10.0; + double personalH = rec.getPersonalH() / 10.0; + double officialH = rec.getOfficialH() / 10.0; + double sickH = rec.getSickH() / 10.0; + double absentH = rec.getAbsentH() / 10.0; + + // DAILY_RECORDS INSERT + String insSql = "INSERT INTO DAILY_RECORDS " + + "(EMP_ID, TARGET_DATE, TIME_IN, TIME_OUT, ANNUAL_LEAVE_H, " + + "PERSONAL_LEAVE_H, OFFICIAL_LEAVE_H, SICK_LEAVE_H, " + + "UNAPPROVED_ABSENT_H, UPDATED_AT) VALUES " + + "(?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)"; + try (PreparedStatement ps = conn.prepareStatement(insSql)) { + ps.setString(1, empId); + ps.setString(2, targetDate); + ps.setString(3, timeIn); + ps.setString(4, timeOut); + ps.setDouble(5, annualH); + ps.setDouble(6, personalH); + ps.setDouble(7, officialH); + ps.setDouble(8, sickH); + ps.setDouble(9, absentH); + ps.executeUpdate(); + cunDbxIns++; + } catch (Exception e) { + dbError(e); + return; + } + + // 対象社員フィルタリング(Tカード指定時) → 集計のみスキップ + if (!allEmp && !targetList.contains(empId)) { + continue; + } + + // 年月抽出(YYYYMMDD→YYYYMM) + String ym = targetDate.substring(0, 6); + + // 集計テーブル加算 + AggRow row = findAgg(empId, ym); + if (row == null) { + if (aggTable.size() < 100) { + row = new AggRow(); + row.empId = empId; + row.yearMonth = ym; + row.annualH = rec.getAnnualH(); + row.personalH = rec.getPersonalH(); + row.officialH = rec.getOfficialH(); + row.sickH = rec.getSickH(); + row.absentH = rec.getAbsentH(); + aggTable.add(row); + } else { + System.out.println("WARNING: Agg table full " + empId); + } + } else { + // ANNUAL は COMPUTE ROUNDED(入力は0.1h単位のため ADD と同一結果)、 + // 他は ADD(V9(1) 切捨だが値は既に0.1h単位)→ 全て単純加算 + row.annualH += rec.getAnnualH(); + row.personalH += rec.getPersonalH(); + row.officialH += rec.getOfficialH(); + row.sickH += rec.getSickH(); + row.absentH += rec.getAbsentH(); + } + + // 設計書: WS-TOTAL-HOURS ROUNDED = ANNUAL+PERSONAL+OFFICIAL+SICK+ABSENT + // ON SIZE ERROR → 0 クリア + 警告 + if (row != null) { + row.totalH = computeTotalHours(empId, ym, row.annualH, row.personalH, + row.officialH, row.sickH, row.absentH); + } + } + } catch (Exception e) { + throw new RuntimeException("R01読込失敗: " + e.getMessage(), e); + } + } + + private AggRow findAgg(final String empId, final String ym) { + for (AggRow r : aggTable) { + if (r.empId.equals(empId) && r.yearMonth.equals(ym)) { + return r; + } + } + return null; + } + + /** RESET 時の既存レコード削除(DAILY_RECORDS + MONTHLY_ABSENCE、対象年月のみ). */ + private void resetDelete() { + try (PreparedStatement ps = conn.prepareStatement( + "DELETE FROM DAILY_RECORDS WHERE TARGET_DATE LIKE ?")) { + ps.setString(1, yearMonth + "%"); + int n = ps.executeUpdate(); + cunDbxDel += n; + } catch (Exception e) { + dbError(e); + return; + } + try (PreparedStatement ps = conn.prepareStatement( + "DELETE FROM MONTHLY_ABSENCE WHERE YEAR_MONTH = ?")) { + ps.setString(1, yearMonth); + int n = ps.executeUpdate(); + cunDbxDel += n; + } catch (Exception e) { + dbError(e); + } + } + + /** + * 設計書 2-3: WS-TOTAL-HOURS ROUNDED = ANNUAL+PERSONAL+OFFICIAL+SICK+ABSENT。 + * 9(4)V9(1) → tenth 単位上限 99999。溢れ(ON SIZE ERROR)時は 0 クリア+警告。 + */ + private int computeTotalHours(final String empId, final String ym, + final int annualH, final int personalH, final int officialH, + final int sickH, final int absentH) { + long sum = (long) annualH + personalH + officialH + sickH + absentH; + if (sum > 99999 || sum < -9999) { + System.out.println("WARNING: Total hours overflow emp=" + empId + " ym=" + ym); + return 0; + } + return (int) sum; + } + + // ***************************************************************** + // 終了処理 3000STPSOR + // ***************************************************************** + private void stpProc() { + try { + // 集計結果 → MONTHLY_ABSENCE UPSERT + for (AggRow row : aggTable) { + String cntSql = "SELECT COUNT(*) FROM MONTHLY_ABSENCE " + + "WHERE EMP_ID = ? AND YEAR_MONTH = ?"; + int cnt; + try (PreparedStatement ps = conn.prepareStatement(cntSql)) { + ps.setString(1, row.empId); + ps.setString(2, row.yearMonth); + try (ResultSet rs = ps.executeQuery()) { + cnt = rs.next() ? rs.getInt(1) : 0; + } + } + + if (cnt > 0) { + String updSql = "UPDATE MONTHLY_ABSENCE SET " + + "ANNUAL_LEAVE_H=?, PERSONAL_LEAVE_H=?, OFFICIAL_LEAVE_H=?, " + + "SICK_LEAVE_H=?, UNAPPROVED_ABSENT_H=?, UPDATED_AT=CURRENT_TIMESTAMP " + + "WHERE EMP_ID=? AND YEAR_MONTH=?"; + try (PreparedStatement ps = conn.prepareStatement(updSql)) { + ps.setDouble(1, row.annualH / 10.0); + ps.setDouble(2, row.personalH / 10.0); + ps.setDouble(3, row.officialH / 10.0); + ps.setDouble(4, row.sickH / 10.0); + ps.setDouble(5, row.absentH / 10.0); + ps.setString(6, row.empId); + ps.setString(7, row.yearMonth); + ps.executeUpdate(); + cunDbxUpd++; + } + } else { + String insSql = "INSERT INTO MONTHLY_ABSENCE " + + "(EMP_ID, YEAR_MONTH, ANNUAL_LEAVE_H, PERSONAL_LEAVE_H, " + + "OFFICIAL_LEAVE_H, SICK_LEAVE_H, UNAPPROVED_ABSENT_H, UPDATED_AT) " + + "VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP)"; + try (PreparedStatement ps = conn.prepareStatement(insSql)) { + ps.setString(1, row.empId); + ps.setString(2, row.yearMonth); + ps.setDouble(3, row.annualH / 10.0); + ps.setDouble(4, row.personalH / 10.0); + ps.setDouble(5, row.officialH / 10.0); + ps.setDouble(6, row.sickH / 10.0); + ps.setDouble(7, row.absentH / 10.0); + ps.executeUpdate(); + cunDbxIns++; + } + } + } + + // ABSENCE_SUMMARY 出力 + for (AggRow row : aggTable) { + Kin08Rec out = new Kin08Rec(); + out.setEmpId(row.empId); + out.setYearMonth(row.yearMonth); + out.setAnnualH(row.annualH); + out.setPersonalH(row.personalH); + out.setOfficialH(row.officialH); + out.setSickH(row.sickH); + out.setAbsentH(row.absentH); + out.setFiller(""); + w01OutFil.write(out.toRecordLine()); + w01OutFil.newLine(); + cunW01Out++; + } + + // COMMIT + conn.commit(); + } catch (Exception e) { + dbError(e); + return; + } + + // ファイルCLOSE + try { + if (w01OutFil != null) { + w01OutFil.close(); + } + } catch (Exception ignore) { + // 無視 + } + try { + if (conn != null) { + conn.close(); + } + } catch (Exception ignore) { + // 無視 + } + + // 件数メッセージ + msgOutPara(CNS_MSGIINKES, "KIN08R01", ConvUtil.padLeftZero(cunR01Inn, 9), ""); + msgOutPara(CNS_MSGIINKES, "INS", ConvUtil.padLeftZero(cunDbxIns, 9), ""); + msgOutPara(CNS_MSGIINKES, "UPD", ConvUtil.padLeftZero(cunDbxUpd, 9), ""); + if (cunDbxDel > 0) { + msgOutPara(CNS_MSGOUTKES, "DEL", ConvUtil.padLeftZero(cunDbxDel, 9), ""); + } + msgOutPara(CNS_MSGOUTKES, "KIN08W01", ConvUtil.padLeftZero(cunW01Out, 9), ""); + msgOutPara(CNS_MSGFIN, "", "", ""); + } + + // ***************************************************************** + // DBエラー処理 9100DBERRSOR(ROLLBACK + ABEND) + // ***************************************************************** + private void dbError(final Exception e) { + System.out.println("SQL ERROR: " + e.getMessage() + " PGM=KIN08DBU"); + try { + if (conn != null) { + conn.rollback(); + } + } catch (Exception ignore) { + // 無視 + } + abend(); + } + + /** メッセージ編集出力 4000MSGOUTSOR(SUB02MSG 呼出). */ + private void msgOutPara(final int msgCod, final String p1, final String p2, final String p3) { + msgPar.reset(); + msgPar.setM00MsgCod(msgCod); + msgPar.setM00Umkdats2203(CNS_KN0002); + msgPar.setM00Umkdats2204(CNS_KN0002); + msgPar.setM00Umkdats2205(CNS_PRGIDX); + msgPar.setM00Umkdats2201(p1); + msgPar.setM00Umkdats2202(p2); + Sub02MsgSub.execute(msgPar); + } + + /** ABEND処理(9999ABDSOR 相当)。 */ + private void abend() { + endPar.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(endPar); + } +} diff --git a/JavaSrc/src/Kin09CsvMain.java b/JavaSrc/src/Kin09CsvMain.java new file mode 100644 index 0000000..72236af --- /dev/null +++ b/JavaSrc/src/Kin09CsvMain.java @@ -0,0 +1,222 @@ +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class Kin09CsvMain { + + private static final int REC_LEN = 200; + + private static final String DAILY_HEADER = + "社員番号,日付,出勤時刻,退勤時刻,休暇種別,年休時間,事假時間,因公特批假時間,病欠時間,未申請欠勤時間"; + private static final String SUMMARY_HEADER = + "社員番号,年月,年休計,事假計,因公特批假計,病欠計,未申請欠勤計"; + + private static final String OUTPUT_FILE = "data/KIN09W01.DAT"; + + public static void main(String[] args) throws Exception { + System.out.println("KIN09CSV 開始"); + + String[] parsed = parseParm(args); + String yearMonth = parsed[0]; + String mode = parsed[1]; + boolean shortMode = mode.equalsIgnoreCase("SHORT"); + + if (yearMonth == null || yearMonth.trim().isEmpty()) { + System.err.println("ABEND: YEARMONTH は必須です (CNS-ABD999)"); + System.exit(999); + return; + } + + String dateFrom = yearMonth + "01"; + String dateTo = yearMonth + "31"; + + Class.forName("org.sqlite.JDBC"); + try (Connection conn = DriverManager.getConnection("jdbc:sqlite:data/kin.db")) { + + try (BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(OUTPUT_FILE), "UTF-8"))) { + + long w01out = 0; + + boolean fullMode = !shortMode; + + if (fullMode) { + writeRec(out, DAILY_HEADER); + w01out++; + + int lineCount = 0; + + String dSql = "SELECT EMP_ID, TARGET_DATE, TIME_IN, TIME_OUT, " + + "ANNUAL_LEAVE_H, PERSONAL_LEAVE_H, OFFICIAL_LEAVE_H, " + + "SICK_LEAVE_H, UNAPPROVED_ABSENT_H " + + "FROM DAILY_RECORDS " + + "WHERE TARGET_DATE >= ? AND TARGET_DATE <= ? " + + "ORDER BY EMP_ID, TARGET_DATE"; + try (PreparedStatement ps = conn.prepareStatement(dSql)) { + ps.setString(1, dateFrom); + ps.setString(2, dateTo); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String empId = sanitize(getStr(rs, "EMP_ID")); + String date = getStr(rs, "TARGET_DATE"); + String timeIn = getStr(rs, "TIME_IN"); + String timeOut = getStr(rs, "TIME_OUT"); + double annual = rs.getDouble("ANNUAL_LEAVE_H"); + double personal = rs.getDouble("PERSONAL_LEAVE_H"); + double official = rs.getDouble("OFFICIAL_LEAVE_H"); + double sick = rs.getDouble("SICK_LEAVE_H"); + double absent = rs.getDouble("UNAPPROVED_ABSENT_H"); + + String leaveType = "99"; + if (annual > 0) leaveType = "01"; + else if (personal > 0) leaveType = "02"; + else if (official > 0) leaveType = "03"; + else if (sick > 0) leaveType = "04"; + + String line = empId + "," + date + "," + timeIn + "," + timeOut + "," + + leaveType + "," + + editHours(annual) + "," + editHours(personal) + "," + + editHours(official) + "," + editHours(sick) + "," + + editHours(absent); + + writeRec(out, line); + w01out++; + + lineCount++; + if (lineCount > 100) { + writeAdvancing(out, 2, DAILY_HEADER); + w01out++; + lineCount = 0; + } + } + } + } + } + + // 月次サマリ(FULL / SHORT 両方出力)。FULL は日別との区切り空白を挿入。 + if (fullMode) { + writeAdvancing(out, 2, ""); + w01out++; + } + + writeRec(out, SUMMARY_HEADER); + w01out++; + + String mSql = "SELECT EMP_ID, YEAR_MONTH, ANNUAL_LEAVE_H, " + + "PERSONAL_LEAVE_H, OFFICIAL_LEAVE_H, SICK_LEAVE_H, " + + "UNAPPROVED_ABSENT_H " + + "FROM MONTHLY_ABSENCE WHERE YEAR_MONTH = ? ORDER BY EMP_ID"; + try (PreparedStatement ps = conn.prepareStatement(mSql)) { + ps.setString(1, yearMonth); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String empId = sanitize(getStr(rs, "EMP_ID")); + String ym = getStr(rs, "YEAR_MONTH"); + double annual = rs.getDouble("ANNUAL_LEAVE_H"); + double personal = rs.getDouble("PERSONAL_LEAVE_H"); + double official = rs.getDouble("OFFICIAL_LEAVE_H"); + double sick = rs.getDouble("SICK_LEAVE_H"); + double absent = rs.getDouble("UNAPPROVED_ABSENT_H"); + + String line = empId + "," + ym + "," + + editHours(annual) + "," + editHours(personal) + "," + + editHours(official) + "," + editHours(sick) + "," + + editHours(absent); + + writeRec(out, line); + w01out++; + } + } + } + + System.out.println("CSV-OUTPUT 出力件数: " + w01out); + } + } + + System.out.println("KIN09CSV 終了"); + } + + private static String[] parseParm(String[] args) { + String cmdLine = String.join(",", args); + String parm1 = ""; + String parm2 = ""; + int commaIdx = cmdLine.indexOf(','); + if (commaIdx >= 0) { + parm1 = cmdLine.substring(0, commaIdx); + parm2 = cmdLine.substring(commaIdx + 1); + } else { + parm1 = cmdLine; + } + + String[] kv1 = parm1.split("=", 2); + String[] kv2 = parm2.split("=", 2); + String key1 = kv1.length > 0 ? kv1[0].trim() : ""; + String val1 = kv1.length > 1 ? kv1[1] : ""; + String key2 = kv2.length > 0 ? kv2[0].trim() : ""; + String val2 = kv2.length > 1 ? kv2[1] : ""; + + String yearMonth = ""; + if (key1.equals("YEARMONTH")) { + yearMonth = val1.length() >= 6 ? val1.substring(0, 6) : val1; + } else if (key2.equals("YEARMONTH")) { + yearMonth = val2.length() >= 6 ? val2.substring(0, 6) : val2; + } + + String mode = "FULL"; + if (key1.equals("MODE")) { + mode = val1.length() >= 5 ? val1.substring(0, 5) : val1; + } else if (key2.equals("MODE")) { + mode = val2.length() >= 5 ? val2.substring(0, 5) : val2; + } + if (mode.trim().isEmpty()) { + mode = "FULL"; + } + + return new String[]{yearMonth, mode.toUpperCase()}; + } + + private static String sanitize(String s) { + if (s == null) return ""; + return s.replace(",", ";").replace("\r", " ").replace("\n", " "); + } + + private static String getStr(ResultSet rs, String col) throws Exception { + String v = rs.getString(col); + return v == null ? "" : v; + } + + private static String editHours(double d) { + int tenth = (int) Math.round(d * 10); + int intPart = tenth / 10; + int dec = Math.abs(tenth % 10); + return intPart + "." + dec; + } + + private static void writeRec(BufferedWriter out, String content) throws Exception { + out.write(padRight(content, REC_LEN)); + out.newLine(); + } + + private static void writeAdvancing(BufferedWriter out, int linesBefore, String content) throws Exception { + for (int i = 0; i < linesBefore; i++) { + out.newLine(); + } + writeRec(out, content); + } + + private static String padRight(String s, int len) { + if (s == null) s = ""; + StringBuilder sb = new StringBuilder(s); + while (sb.length() < len) { + sb.append(' '); + } + if (sb.length() > len) { + sb.setLength(len); + } + return sb.toString(); + } +} diff --git a/JavaSrc/src/Kyu01CvtMain.java b/JavaSrc/src/Kyu01CvtMain.java new file mode 100644 index 0000000..60cdce7 --- /dev/null +++ b/JavaSrc/src/Kyu01CvtMain.java @@ -0,0 +1,468 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +import dto.Kyu01Rec; +import dto.Kyu99Rec; +import dto.ZanendacPar; +import dto.ZanchkacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU01CVT 社員マスタCSV→FB変換処理 + * ============================================================ + * PROGRAM-ID. KYU01CVT → 类 Kyu01CvtMain(PGMパターン:CSV→FB変換) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R01INNFIL(KYU01R01 CSV入力 V500B) → R01INN_FILE + * W01OUTFIL(KYU01W01 EMP-RECORD F80B)→ W01OUT_FILE(COPY KYU01REC → dto.Kyu01Rec) + * W02OUTFIL(KYU01W02 ERROR-LOG VB200B)→ W02OUT_FILE(COPY KYU99REC → dto.Kyu99Rec) + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub / SUB04CHK→Sub04ChkSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R01INNSOR→readR01Para() + * 2000MAJSOR→majPara() 2100CSVUNSOR→splitCsv() 2200ERROUTSOR→errorCsvPara() + * 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 处理要点(詳細設計書_KYU01CVT 準拠): + * - コメント行(先頭'#')は読飛ばし。 + * - 引用符内改行:WRK-IN-QUOTE フラグ追跡し、奇数個の'"'で状態反転、 + * 引用符内の物理行を WRK-CSV-ACCUM に蓄積、完全1レコード分が揃ったら + * INSPECT CONVERTING(CR/LF→SPACE)で改行→スペース変換。 + * - CSV分解は9項目を期待(設計「項目数≠9 は ERROR-LOG」)。 + * ※COBOL の UNSTRING TALLYING は区切り数(8)をカウントし CNS-FLD-COUNT(=9)と比較して + *   常に不一致となる off-by-one と同種の不備があるが、設計書を優先しフィールド数=9 で判定。 + * - 社員番号空チェック・SUB04CHK(EMPID/NUM)で異常時は ERROR-LOG 出力。 + */ +public class Kyu01CvtMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU01CVT'. */ + private static final String CST_PRGIDX = "KYU01CVT"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数 */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数 */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-FLD-COUNT PIC 9(002) VALUE 9. 期待フィールド数(設計準拠) */ + private static final int CST_FLD_COUNT = 9; + + /** 各CSV項目のPIC幅(UNSTRING受信領域の桁数、超過は切捨)。 */ + private static final int[] FLD_WIDTH = {8, 40, 2, 2, 3, 9, 7, 2, 1}; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KYU01R01.(CSV入力) */ + private static final String R01INN_FILE = "data/KYU01R01.csv"; + /** SELECT W01OUTFIL ASSIGN TO KYU01W01.(EMP-RECORD出力) */ + private static final String W01OUT_FILE = "data/KYU01W01.txt"; + /** SELECT W02OUTFIL ASSIGN TO KYU01W02.(ERROR-LOG出力) */ + private static final String W02OUT_FILE = "data/KYU01W02.txt"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + /** 03 CUN-W02OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW02Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-EOF PIC X(001). 読込フラグ(88 WRK-EOF-Y VALUE '1') */ + private String wrkEof = "0"; + /** 03 WRK-IN-QUOTE PIC X(001). 引用符状態(88 WRK-QUOTE-ACTIVE VALUE '1') */ + private boolean inQuote; + /** 03 WRK-CSV-ACCUM PIC X(500) + WRK-ACCUM-LEN. 引用符内蓄積バッファ */ + private final StringBuilder accum = new StringBuilder(); + /** FD R01INNFIL の 01 R01INNREC PIC X(500).(現在の物理行バッファ) */ + private String wrkCsvLine = ""; + + /** 88 WRK-EOF-Y VALUE '1'. */ + private boolean isEof() { + return "1".equals(wrkEof); + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + /** COPY ZANCHKAC.(SUB04CHK用) */ + private final ZanchkacPar c01ChkPar = new ZanchkacPar(); + + // ==================== 出力レコード(FILE SECTION → DTO) ==================== + + /** 01 W01OUTREC.(FD W01OUTFIL) */ + private final Kyu01Rec w01 = new Kyu01Rec(); + /** 01 W02OUTREC.(FD W02OUTFIL) */ + private final Kyu99Rec w02 = new Kyu99Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private BufferedReader r01InnFil; + private PrintWriter w01OutFil; + private PrintWriter w02OutFil; + + /** 入力ファイルパス。デフォルトは SELECT ASSIGN 対応の R01INN_FILE。 */ + private final String r01InnFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + final String inputFile = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + new Kyu01CvtMain(inputFile).run(); + } + + public Kyu01CvtMain() { + this(R01INN_FILE); + } + + public Kyu01CvtMain(final String r01InnFile) { + this.r01InnFile = r01InnFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!isEof()) { // PERFORM 2000MAJSOR UNTIL WRK-EOF = '1' + majPara(); + } + termPara(); + } finally { + closeFiles(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→OPEN→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※FUNCTION WHEN-COMPILED→起動時刻で代用 + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + cunR01Inn = 0; + cunW01Out = 0; + cunW02Out = 0; + inQuote = false; + accum.setLength(0); + wrkCsvLine = ""; + + // *** 入出力ファイル OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. + openFiles(); + + // *** 初回READ + readR01Para(); + } + + /** + * 段落 1100R01INNSOR:R01読込処理。 + * READ AT END / NOT AT END のマッピング。 + */ + private void readR01Para() { + try { + final String line = r01InnFil.readLine(); + if (line == null) { + // AT END: MOVE '1' TO WRK-EOF. + wrkEof = "1"; + } else { + // NOT AT END: ADD 1 TO CUN-R01INN.(バッファへ格納) + wrkCsvLine = line; + cunR01Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * コメント行スキップ→引用符内改行処理(蓄積/完了判定)→processRecord へ。 + */ + private void majPara() { + final String line = wrkCsvLine; + + // *** コメント行(先頭'#')はスキップ(COBOLは引用符状態に関わらずここで判定) + if (!line.isEmpty() && line.charAt(0) == '#') { + readR01Para(); + return; + } + + if (inQuote) { + // *** 引用符内:蓄積継続または完了判定 + final int q = countQuotes(line); + if ((q % 2) == 1) { + // 奇数個 → 引用符クローズ(完全1レコード分が揃った) + inQuote = false; + accum.append(line); + final String logical = accum.toString() + .replace("\r", " ").replace("\n", " "); + processRecord(logical); + accum.setLength(0); + readR01Para(); // 次の物理行へ + } else { + // 偶数個 → 蓄積継続 + accum.append(line); + readR01Para(); + } + return; + } + + // *** 引用符外:奇数個の'"'で開始 + final int q = countQuotes(line); + if ((q % 2) == 1) { + inQuote = true; + accum.setLength(0); + accum.append(line); + readR01Para(); + return; + } + + // *** 通常行(引用符なし)→ 直接 CSV 処理 + processRecord(line.replace("\r", " ").replace("\n", " ")); + readR01Para(); + } + + /** + * 段落 2100CSVUNSOR + 2-3~2-8:CSV分解→項目数/空チェック→SUB04CHK→W01出力。 + * @param logical 完全な1論理レコード(改行→スペース変換済) + */ + private void processRecord(final String logical) { + // *** 項目数≠9 → ERROR-LOG出力(設計「項目数≠9」準拠。 + // ※UNSTRING の TALLYING は区切り数(8)を数えるため、RAW トークン数で判定) + final String[] tokens = logical.split(",", -1); + if (tokens.length != CST_FLD_COUNT) { + errorCsvPara(logical); + return; + } + + // *** CSV分解(UNSTRING DELIMITED BY ',' → 9項目、受信領域は PIC 幅で切捨) + final String[] f = splitCsv(logical); + + // *** 社員番号空チェック(INSPECT TALLYING FOR LEADING SPACES = 8 → 全桁SPACE) + final String empId = f[0]; + if (leadingSpaces(empId) == 8) { + w02.reset(); + w02.setErrCategory("EMPID-ERR"); + w02.setErrDetail("EMPTY EMP-ID AT LINE " + cunR01Inn); + writeW02(); + return; + } + + // *** SUB04CHK:社員番号(EMPID)数値妥当性チェック + if (!chkPara("EMPID", empId)) { + errorCsvPara(logical); + return; + } + + // *** SUB04CHK:時給(NUM)数値妥当性チェック + if (!chkPara("NUM", f[6])) { + errorCsvPara(logical); + return; + } + + // *** 社員名英字チェック(IF NOT ALPHABETIC → CONTINUE のため無動作) + + // *** W01OUTREC編集(MOVE CSV各項目→W01OUTREC) + w01.reset(); + w01.setEmpId(empId); + w01.setEmpName(f[1]); + w01.setDeptCode(f[2]); + w01.setRegionCode(f[3]); + w01.setCategoryCode(f[4]); + w01.setBaseSalary(ConvUtil.parseIntSafe(f[5], 0)); + w01.setHourlyRate(ConvUtil.parseIntSafe(f[6], 0)); + w01.setDependentCount(ConvUtil.parseIntSafe(f[7], 0)); + w01.setStatus(f[8]); + writeW01(); + } + + /** + * 段落 2100CSVUNSOR:UNSTRING DELIMITED BY ',' INTO 各フィールド(TALLYING相当は判定に使用)。 + * COBOL と同様に受信領域の PIC 幅で切捨;不足項目は SPACE(数値は0)で補完。 + */ + private String[] splitCsv(final String line) { + final String[] tokens = line.split(",", -1); + final String[] out = new String[CST_FLD_COUNT]; + for (int i = 0; i < CST_FLD_COUNT; i++) { + if (i < tokens.length) { + final String t = tokens[i]; + if (t.isEmpty()) { + // 空項目は UNSTRING で受信されなかったのと同等(INITIALIZE 済の SPACE) + out[i] = ConvUtil.padRight("", FLD_WIDTH[i]); + } else if (t.length() > FLD_WIDTH[i]) { + out[i] = t.substring(0, FLD_WIDTH[i]); // 受信領域 PIC 幅で切捨 + } else { + out[i] = t; + } + } else { + // UNSTRING で受信されなかった項目は INITIALIZE 済(SPACE)のまま + out[i] = ConvUtil.padRight("", FLD_WIDTH[i]); + } + } + return out; + } + + /** 行内の '"' 出現回数(引用符状態反転判定用)。 */ + private static int countQuotes(final String s) { + int n = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '"') { + n++; + } + } + return n; + } + + /** 先行スペース数(COBOL INSPECT TALLYING FOR LEADING SPACES 相当)。 */ + private static int leadingSpaces(final String s) { + int n = 0; + while (n < s.length() && s.charAt(n) == ' ') { + n++; + } + return n; + } + + /** + * 段落 2200ERROUTSOR:CSV系エラー出力処理。 + * MOVE 'CSV-ERR' TO W02ERR-CATEGORY./MOVE WRK-CSV-ACCUM(1:80) TO W02ERR-DETAIL./WRITE W02OUTREC. + */ + private void errorCsvPara(final String logical) { + w02.reset(); + w02.setErrCategory("CSV-ERR "); + final String detail = (logical.length() > 80) ? logical.substring(0, 80) : logical; + w02.setErrDetail(detail); + writeW02(); + } + + /** SUB04CHK 呼出(C01CHKTYP/C01CHKDAT 設定→execute→RC=0 判定)。 */ + private boolean chkPara(final String typ, final String dat) { + c01ChkPar.reset(); + c01ChkPar.setC01ChkTyp(typ); + c01ChkPar.setC01ChkDat(ConvUtil.padRight(dat, 80)); + Sub04ChkSub.execute(c01ChkPar); + return c01ChkPar.getC01ChkRrc() == 0; + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→入出力件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + closeFiles(); + msgOutPara(CST_MSGIINKES, "KYU01R01", ConvUtil.padLeftZero(cunR01Inn, 9)); + msgOutPara(CST_MSGOUTKES, "KYU01W01", ConvUtil.padLeftZero(cunW01Out, 9)); + msgOutPara(CST_MSGOUTKES, "KYU01W02", ConvUtil.padLeftZero(cunW02Out, 9)); + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), StandardCharsets.UTF_8)); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), StandardCharsets.UTF_8)); + w02OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W02OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. */ + private void writeW01() { + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** WRITE W02OUTREC./ADD 1 TO CUN-W02OUT. */ + private void writeW02() { + w02OutFil.println(w02.toRecordLine()); + cunW02Out++; + } + + /** CLOSE R01INNFIL W01OUTFIL W02OUTFIL. */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w01OutFil); + closeQuiet(w02OutFil); + r01InnFil = null; + w01OutFil = null; + w02OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } +} diff --git a/JavaSrc/src/Kyu02RegMain.java b/JavaSrc/src/Kyu02RegMain.java new file mode 100644 index 0000000..70fa04b --- /dev/null +++ b/JavaSrc/src/Kyu02RegMain.java @@ -0,0 +1,502 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import dto.Kyu01Rec; +import dto.Kyu99Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU02REG 社員マスタDB2登録処理 + * ============================================================ + * PROGRAM-ID. KYU02REG → 类 Kyu02RegMain(PGMパターン:DB更新) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R02INNFIL(KYU02R02 EMP-RECORD FB80B) → R02INN_FILE(COPY KYU01REC → dto.Kyu01Rec 再利用) + * W03OUTFIL(KYU02W03 ERROR-LOG VB200B) → W03OUT_FILE(COPY KYU99REC → dto.Kyu99Rec 再利用) + * 使用DB2:EMP-MASTER(EXEC SQL INSERT/UPDATE、重複-803時UPDATE) + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 使用COPY(連絡領域):ZANMSGAC→ZanmsgacPar / ZANENDAC→ZanendacPar + * ※SQLCA は JDBC の SQLException に映射(SQLCODE = e.getErrorCode()) + * + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R02INNSOR→readR02Para() + * 2000MAJSOR→majPara() 2100ERROUTSOR→errorOutPara() + * 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() + * 9999ABDSOR→abendPara()(本程序未 CALL、死代码、忠实残留) + * + * 处理要点(詳細設計書_KYU02REG 準拠): + * - EMP-RECORD(80B固定長)を 1 行毎読み、DB2 ホスト変数へ MOVE。 + * - INSERT 試行。SQLCODE=0 → CUN-DB-INS 加算。SQLCODE=-803 → UPDATE へ切替 + * (成功→CUN-DB-UPD 加算、失敗→ERROR-LOG)。その他 SQL エラー → ERROR-LOG。 + * - ERROR-LOG は 'DB-ERR' + エラー社員番号を出力し、処理継続(ABEND しない)。 + * - DB 接続は DbSample 標準例に倣い setAutoCommit(false)、正常終了時に commit、 + * SQLException 時は rollback(詳細設計書に明示的 COMMIT は無いが、 + * 本プロジェクトの DB 標準例に準拠して追加。※COBOL との差異は報告書に記載)。 + * - CONNECT 先は COBOL の 'data/SALARY.db' だが、作業指示により + * DB2 URL 'jdbc:db2://localhost:50000/TESTDB'(ユーザ/パスはプレースホルダ)を使用。 + */ +public class Kyu02RegMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU02REG'. */ + private static final String CST_PRGIDX = "KYU02REG"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. (宣言のみ、本程序未使用) */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数メッセージ */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数メッセージ */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時メッセージ */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + + /** DB 接続先(作業指示:DB2 URL。ユーザ/パスはプレースホルダで実値を保持しない。テスト時は -Dkyu.db.url で上書き) */ + private static final String DB_URL = System.getProperty("kyu.db.url", "jdbc:db2://localhost:50000/TESTDB"); + private static final String DB_USER = "db2inst1"; + private static final String DB_PASS = "password"; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R02INNFIL ASSIGN TO EXTERNAL KYU02R02.(EMP-RECORD FB80B 入力) */ + private static final String R02INN_FILE = "data/KYU02R02.txt"; + /** SELECT W03OUTFIL ASSIGN TO EXTERNAL KYU02W03.(ERROR-LOG VB200B 出力) */ + private static final String W03OUT_FILE = "data/KYU02W03.txt"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R02INN PIC S9(009) COMP-3 VALUE ZERO. 入力読込件数 */ + private long cunR02Inn; + /** 03 CUN-W03OUT PIC S9(009) COMP-3 VALUE ZERO. エラーログ出力件数 */ + private long cunW03Out; + /** 03 CUN-DB-INS PIC S9(009) COMP-3 VALUE ZERO. INSERT 件数 */ + private long cunDbIns; + /** 03 CUN-DB-UPD PIC S9(009) COMP-3 VALUE ZERO. UPDATE 件数 */ + private long cunDbUpd; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-EOF PIC X(001).(宣言のみ。実際の EOF 判定は 88 WRK-EOF-Y を使用) */ + private String wrkEof = ""; + /** 03 WS-PAD-CHAR PIC X(001) VALUE SPACE. */ + private String wsPadChar = " "; + /** 03 WS-FILE-STATUS PIC X(002). 88 WRK-EOF-Y VALUE '1'. */ + private String wsFileStatus = ""; + /** 88 WRK-EOF-Y VALUE '1'.(SET WRK-EOF-Y TO TRUE → wsFileStatus='1') */ + private boolean isWrkEofY() { + return "1".equals(wsFileStatus); + } + + /** 現在読込中の R02 物理行(FD R02INNFIL 01 R02INNREC の生バッファ) */ + private String r02Line = ""; + + // ==================== DB2 ホスト変数 DBVARA ==================== + + /** 03 DBV-EMP-ID PIC X(008). */ + private String dbvEmpId = ""; + /** 03 DBV-EMP-NAME PIC X(040). */ + private String dbvEmpName = ""; + /** 03 DBV-DEPT-CODE PIC X(002). */ + private String dbvDeptCode = ""; + /** 03 DBV-REGION-CODE PIC X(002). */ + private String dbvRegionCode = ""; + /** 03 DBV-CATEGORY-CODE PIC X(003). */ + private String dbvCategoryCode = ""; + /** 03 DBV-BASE-SALARY PIC 9(009). */ + private int dbvBaseSalary; + /** 03 DBV-HOURLY-RATE PIC 9(007). */ + private int dbvHourlyRate; + /** 03 DBV-DEPENDENT-COUNT PIC 9(002). */ + private int dbvDependentCount; + /** 03 DBV-STATUS PIC X(001). */ + private String dbvStatus = ""; + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 入出力レコード(FILE SECTION → DTO) ==================== + + /** 01 R02INNREC.(FD R02INNFIL / COPY KYU01REC REPLACING ==(A)== BY ==R02==) */ + private final Kyu01Rec r02 = new Kyu01Rec(); + /** 01 W03OUTREC.(FD W03OUTFIL / COPY KYU99REC REPLACING ==(A)== BY ==W03==) */ + private final Kyu99Rec w03 = new Kyu99Rec(); + + // ==================== ファイルハンドル / DB 接続 ==================== + + /** R02INNFIL 読取ハンドル(OPEN/CLOSE はフィールド保持、finally でクローズ) */ + private BufferedReader r02InnFil; + /** W03OUTFIL 書込ハンドル(同上) */ + private PrintWriter w03OutFil; + /** DB2 接続(EXEC SQL CONNECT TO … END-EXEC 相当) */ + private Connection conn; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + new Kyu02RegMain().run(); + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL WRK-EOF-Y → PERFORM 3000STPSOR → GOBACK. + * DbSample 標準例に倣い、正常時は commit、SQLException 時は rollback。 + */ + private void run() { + try { + initPara(); + while (!isWrkEofY()) { // PERFORM 2000MAJSOR UNTIL WRK-EOF-Y + majPara(); + } + if (conn != null) { + // EXEC SQL COMMIT END-EXEC(正常終了時コミット:DbSample パターン) + conn.commit(); + System.out.println("[db] NORMAL END (COMMIT)"); + } + termPara(); + } catch (SQLException e) { + // EXEC SQL WHENEVER SQLERROR GO TO ERROR-PROC 相当(接続/コミット等の致命エラー) + System.out.println("[db] SQLERROR code=" + e.getErrorCode() + " msg=" + e.getMessage()); + rollbackQuiet(conn); + } finally { + closeFiles(); + closeQuiet(conn); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→OPEN→DB接続→初回読込。 + */ + private void initPara() throws SQLException { + // *** 開始メッセージ出力(INITIALIZE M00MHOPAR / MOVE CNS-MSGSTR TO M00MSGCOD) + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力(FUNCTION WHEN-COMPILED → 起動時刻で代用) + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + wrkEof = ""; + wsPadChar = " "; + wsFileStatus = ""; + cunR02Inn = 0; + cunW03Out = 0; + cunDbIns = 0; + cunDbUpd = 0; + r02Line = ""; + + // *** OPEN INPUT R02INNFIL / OUTPUT W03OUTFIL. + openFiles(); + + // *** DB接続 EXEC SQL CONNECT TO 'data/SALARY.db' END-EXEC + // (作業指示により DB2 URL を使用。ユーザ/パスはプレースホルダ) + conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); + conn.setAutoCommit(false); // バッチ標準:明示的トランザクション制御 + + // *** 初回 READ(1100R02INNSOR) + readR02Para(); + } + + /** + * 段落 1100R02INNSOR:R02読込処理。 + * READ AT END(SET WRK-EOF-Y TO TRUE)/ NOT AT END(ADD 1 TO CUN-R02INN)のマッピング。 + */ + private void readR02Para() { + try { + final String line = r02InnFil.readLine(); + if (line == null) { + // AT END: SET WRK-EOF-Y TO TRUE(→ wsFileStatus='1') + wsFileStatus = "1"; + } else { + // NOT AT END: 物理行をバッファへ格納し ADD 1 TO CUN-R02INN. + r02Line = line; + cunR02Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * DB2ホスト変数へ MOVE → INSERT 試行 → SQLCODE 判定(-803 は UPDATE へ切替)。 + * ※いずれの分岐も最後に次行 READ を実施(COBOL の PERFORM 1100R02INNSOR 相当)。 + */ + private void majPara() { + // *** DB2ホスト変数にMOVE:R02INNREC(80B固定長) → DBVARA + parseR02Line(r02Line); + moveToDbv(); + + // *** INSERT試行(EXEC SQL INSERT INTO EMP-MASTER ... VALUES ... :ホスト変数) + try { + final int insCnt = insertEmp(); // SQLCODE=0 → 1行挿入 + if (insCnt >= 1) { + // IF SQLCODE = 0 → ADD 1 TO CUN-DB-INS + cunDbIns++; + } + } catch (SQLException e) { + if (isDuplicateKey(e)) { + // *** -803 = 重複 → UPDATE + try { + final int updCnt = updateEmp(); + if (updCnt >= 1) { + // UPDATE 成功 → ADD 1 TO CUN-DB-UPD + cunDbUpd++; + } else { + // UPDATE だが 0 件(想定外)→ ERROR-LOG + errorOutPara(); + } + } catch (SQLException e2) { + // UPDATE 異常 → ERROR-LOG + errorOutPara(); + } + } else { + // *** その他 SQL エラー → ERROR-LOG + errorOutPara(); + } + } + + // *** 次行 READ(各分岐共通:PERFORM 1100R02INNSOR) + readR02Para(); + } + + /** + * 段落 2100ERROUTSOR:エラー出力処理。 + * MOVE 'DB-ERR' TO W03ERR-CATEGORY./MOVE DBV-EMP-ID TO W03ERR-DETAIL./WRITE W03OUTREC./ADD 1 TO CUN-W03OUT. + */ + private void errorOutPara() { + w03.reset(); + w03.setErrCategory("DB-ERR"); // MOVE 'DB-ERR' TO W03ERR-CATEGORY(PIC X(008)) + w03.setErrDetail(dbvEmpId); // MOVE DBV-EMP-ID TO W03ERR-DETAIL(PIC X(192)) + w03OutFil.println(w03.toRecordLine()); + cunW03Out++; + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→入出力件数メッセージ×4(入力/DB-INSERT/DB-UPDATE/ERROR-LOG)→終了メッセージ。 + */ + private void termPara() { + closeFiles(); + + // *** 入力件数メッセージ(CNS-MSGIINKES=6 / 'KYU02R02' / CUN-R02INN) + msgOutPara(CST_MSGIINKES, "KYU02R02", ConvUtil.padLeftZero(cunR02Inn, 9)); + // *** INSERT 件数メッセージ(CNS-MSGOUTKES=7 / 'DB-INSERT' / CUN-DB-INS) + msgOutPara(CST_MSGOUTKES, "DB-INSERT", ConvUtil.padLeftZero(cunDbIns, 9)); + // *** UPDATE 件数メッセージ(CNS-MSGOUTKES=7 / 'DB-UPDATE' / CUN-DB-UPD) + msgOutPara(CST_MSGOUTKES, "DB-UPDATE", ConvUtil.padLeftZero(cunDbUpd, 9)); + // *** ERROR-LOG 件数メッセージ(CNS-MSGOUTKES=7 / 'KYU02W03' / CUN-W03OUT) + msgOutPara(CST_MSGOUTKES, "KYU02W03", ConvUtil.padLeftZero(cunW03Out, 9)); + // *** 終了メッセージ(CNS-MSGFIN=2) + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + // *** MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1) / -04(1:1)(先頭1文字='2'、残りSPACE) + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理(本程序からは CALL されていない死代码、忠实残留)。 + * MOVE CNS-ABD999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== DB 操作補助(EXEC SQL INSERT/UPDATE 相当) ==================== + + /** + * EXEC SQL INSERT INTO EMP-MASTER ... :DBV-* 相当。 + * @return 挿入件数(SQLCODE=0 で 1) + */ + private int insertEmp() throws SQLException { + final String sql = "INSERT INTO \"EMP-MASTER\" " + + "(\"EMP-ID\", \"EMP-NAME\", \"DEPT-CODE\", \"REGION-CODE\", \"CATEGORY-CODE\", " + + " \"BASE-SALARY\", \"HOURLY-RATE\", \"DEPENDENT-COUNT\", \"STATUS\", \"UPDATED-AT\") " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + // MOVE 済の DBV ホスト変数 → setXxx(定位置プレースホルダ) + ps.setString(1, dbvEmpId); // :DBV-EMP-ID + ps.setString(2, dbvEmpName); // :DBV-EMP-NAME + ps.setString(3, dbvDeptCode); // :DBV-DEPT-CODE + ps.setString(4, dbvRegionCode); // :DBV-REGION-CODE + ps.setString(5, dbvCategoryCode); // :DBV-CATEGORY-CODE + ps.setLong(6, dbvBaseSalary); // :DBV-BASE-SALARY + ps.setLong(7, dbvHourlyRate); // :DBV-HOURLY-RATE + ps.setInt(8, dbvDependentCount); // :DBV-DEPENDENT-COUNT + ps.setString(9, dbvStatus); // :DBV-STATUS + return ps.executeUpdate(); + } + } + + /** + * EXEC SQL UPDATE EMP-MASTER SET ... WHERE EMP-ID = :DBV-EMP-ID 相当(-803 時)。 + * @return 更新件数(成功で 1) + */ + private int updateEmp() throws SQLException { + final String sql = "UPDATE \"EMP-MASTER\" SET " + + "\"EMP-NAME\" = ?, \"DEPT-CODE\" = ?, \"REGION-CODE\" = ?, \"CATEGORY-CODE\" = ?, " + + "\"BASE-SALARY\" = ?, \"HOURLY-RATE\" = ?, \"DEPENDENT-COUNT\" = ?, \"STATUS\" = ?, " + + "\"UPDATED-AT\" = CURRENT_TIMESTAMP " + + "WHERE \"EMP-ID\" = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, dbvEmpName); // :DBV-EMP-NAME + ps.setString(2, dbvDeptCode); // :DBV-DEPT-CODE + ps.setString(3, dbvRegionCode); // :DBV-REGION-CODE + ps.setString(4, dbvCategoryCode); // :DBV-CATEGORY-CODE + ps.setLong(5, dbvBaseSalary); // :DBV-BASE-SALARY + ps.setLong(6, dbvHourlyRate); // :DBV-HOURLY-RATE + ps.setInt(7, dbvDependentCount); // :DBV-DEPENDENT-COUNT + ps.setString(8, dbvStatus); // :DBV-STATUS + ps.setString(9, dbvEmpId); // :DBV-EMP-ID(WHERE) + return ps.executeUpdate(); + } + } + + // ==================== レコード編集補助 ==================== + + /** + * 80B 固定長 R02 物理行を R02INNREC(COPY KYU01REC)に分割格納。 + * 位置は 05 層レイアウトに相当(empId 8 / empName 40 / dept 2 / region 2 / + * category 3 / baseSalary 9 / hourlyRate 7 / dependentCount 2 / status 1 / filler 6 = 80)。 + */ + private void parseR02Line(final String line) { + final String l = (line == null) ? "" : line; + r02.setEmpId(ConvUtil.safeSub(l, 0, 8)); // R02EMP-ID (1:8) + r02.setEmpName(ConvUtil.safeSub(l, 8, 48)); // R02EMP-NAME (9:48) + r02.setDeptCode(ConvUtil.safeSub(l, 48, 50)); // R02DEPT-CODE (49:50) + r02.setRegionCode(ConvUtil.safeSub(l, 50, 52)); // R02REGION-CODE (51:52) + r02.setCategoryCode(ConvUtil.safeSub(l, 52, 55)); // R02CATEGORY-CODE (53:55) + r02.setBaseSalary(ConvUtil.parseIntSafe(ConvUtil.safeSub(l, 55, 64), 0)); // R02BASE-SALARY (56:64) + r02.setHourlyRate(ConvUtil.parseIntSafe(ConvUtil.safeSub(l, 64, 71), 0)); // R02HOURLY-RATE (65:71) + r02.setDependentCount(ConvUtil.parseIntSafe(ConvUtil.safeSub(l, 71, 73), 0)); // R02DEPENDENT-COUNT (72:73) + r02.setStatus(ConvUtil.safeSub(l, 73, 74)); // R02STATUS (74:74) + // filler (75:80) は名称項目なしのため省略 + } + + /** MOVE R02* → DBV*(2000MAJSOR 先頭の 9 項目 MOVE 群相当)。 */ + private void moveToDbv() { + dbvEmpId = r02.getEmpId(); + dbvEmpName = r02.getEmpName(); + dbvDeptCode = r02.getDeptCode(); + dbvRegionCode = r02.getRegionCode(); + dbvCategoryCode = r02.getCategoryCode(); + dbvBaseSalary = r02.getBaseSalary(); + dbvHourlyRate = r02.getHourlyRate(); + dbvDependentCount = r02.getDependentCount(); + dbvStatus = r02.getStatus(); + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R02INNFIL / OUTPUT W03OUTFIL. */ + private void openFiles() { + try { + r02InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(R02INN_FILE), StandardCharsets.UTF_8)); + w03OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W03OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** ファイルハンドル close(finally から呼出)。 */ + private void closeFiles() { + closeQuiet(r02InnFil); + closeQuiet(w03OutFil); + r02InnFil = null; + w03OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序も FILE STATUS 未管理) + } + } + + /** EXEC SQL ROLLBACK END-EXEC(異常時)。 */ + private static void rollbackQuiet(final Connection c) { + if (c != null) { + try { + c.rollback(); + System.out.println("[db] ROLLBACK DONE"); + } catch (SQLException e) { + System.out.println("[db] ROLLBACK FAILED: " + e.getMessage()); + } + } + } + + private static void closeQuiet(final Connection c) { + if (c != null) { + try { + c.close(); + } catch (SQLException e) { + System.out.println("[db] CLOSE FAILED: " + e.getMessage()); + } + } + } + + /** DB2(-803) と SQLite(UNIQUE constraint / SQLState 23000) の両方で重複キーを判定。 */ + private static boolean isDuplicateKey(final SQLException e) { + if (e.getErrorCode() == -803) { + return true; + } + if ("23000".equals(e.getSQLState())) { + return true; + } + final String m = e.getMessage(); + return m != null && m.contains("UNIQUE constraint failed"); + } +} diff --git a/JavaSrc/src/Kyu03AggMain.java b/JavaSrc/src/Kyu03AggMain.java new file mode 100644 index 0000000..d9bf698 --- /dev/null +++ b/JavaSrc/src/Kyu03AggMain.java @@ -0,0 +1,387 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; + +import dto.Kyu02Rec; +import dto.Kyu99Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU03AGG 欠勤集約処理 + * ============================================================ + * PROGRAM-ID. KYU03AGG → 类 Kyu03AggMain(PGMパターン:キーブレイク集約、メイン) + * + * 使用ファイル(SELECT ASSIGN → 路径常量): + * R01INNFIL(KYU03R01 ABSENCE-SUMMARY FB80B) → R01INN_FILE(自前定義80B) + * W01OUTFIL(KYU03W01 ABSENCE-AGGREGATE FB80B) → W01OUT_FILE(COPY KYU02REC → dto.Kyu02Rec 再利用) + * W02OUTFIL(KYU03W02 ERROR-LOG VB200B) → W02OUT_FILE(COPY KYU99REC → dto.Kyu99Rec) + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 使用DB2テーブル:なし(DB操作なし) + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R01INNSOR→readR01Para() + * 2000MAJSOR→majPara() 2100WRITOUTSOR→writeOutPara() 3000STPSOR→termPara() + * 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 处理要点(詳細設計書_KYU03AGG 準拠): + * - JCL SORT済み欠勤データ(社員番号昇順)を読み、同一社員単位に + * 欠勤時間合計(WRK-TOTAL-HOURS)・欠勤回数(WRK-ABSENT-COUNT)を集約し + * 社員単位1件の集約レコードをW01へ出力する。 + * - キーブレイク:WRK-PREV-EMP-ID と WRK-EMP-ID の比較により、 + * 同一社員は加算継続、異社員出現時に前社員の集約結果を出力。 + * - W02(ERROR-LOG)は現時点出力なし(将来拡張、COBOLも WRITE なし)。 + */ +public class Kyu03AggMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU03AGG'. */ + private static final String CST_PRGIDX = "KYU03AGG"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. (使用資源一覧/手順で未参照) */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数 */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数 */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. (M00UMKDATS22-03/04 先頭1桁) */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. ABENDコード */ + private static final int CST_ABD999 = 999; + /** 03 CNS-ONE PIC 9(002) VALUE 1. 初回/異社員時の欠勤回数初期値 */ + private static final int CST_ONE = 1; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KYU03R01.(ABSENCE-SUMMARY FB80B 入力) */ + private static final String R01INN_FILE = "data/KYU03R01.txt"; + /** SELECT W01OUTFIL ASSIGN TO KYU03W01.(ABSENCE-AGGREGATE FB80B 出力) */ + private static final String W01OUT_FILE = "data/KYU03W01.txt"; + /** SELECT W02OUTFIL ASSIGN TO KYU03W02.(ERROR-LOG VB200B 出力、現時点出力なし) */ + private static final String W02OUT_FILE = "data/KYU03W02.txt"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + /** 03 CUN-W02OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW02Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-ABSENCE-REC 内 05 WRK-EMP-ID PIC X(008).(READ ... INTO 受信) */ + private String wrkEmpId = ""; + /** 03 WRK-ABSENCE-REC 内 05 WRK-YEAR-MONTH PIC X(006). */ + private String wrkYearMonth = ""; + /** 03 WRK-ABSENCE-REC 内 05 WRK-ABSENT-HOURS PIC 9(004)V9(001). */ + private BigDecimal wrkAbsentHours = BigDecimal.ZERO; + /** 03 WRK-PREV-EMP-ID PIC X(008). 前社員キー保持 */ + private String wrkPrevEmpId = ""; + /** 03 WRK-PREV-KEY-SET PIC X(001).(88 WRK-PREV-Y VALUE '1') */ + private String wrkPrevKeySet = "0"; + /** 03 WRK-TOTAL-HOURS PIC 9(004)V9(001). 同一社員欠勤時間合計 */ + private BigDecimal wrkTotalHours = BigDecimal.ZERO; + /** 03 WRK-ABSENT-COUNT PIC 9(002). 同一社員欠勤回数 */ + private int wrkAbsentCount; + /** 03 WRK-EOF PIC X(001).(※88 WRK-EOF-Y は WS-FILE-STATUS 配下に定義される)未使用 */ + private String wrkEof = ""; + /** 03 WS-PAD-CHAR PIC X(001) VALUE SPACE.(PADDING CHARACTER 指定用) */ + private String wsPadChar = " "; + /** 03 WS-FILE-STATUS PIC X(002).(88 WRK-EOF-Y VALUE '1' → SET 時に '1' となる) */ + private String wsFileStatus = "0"; + + /** 88 WRK-PREV-Y VALUE '1'. */ + private boolean isPrevY() { + return "1".equals(wrkPrevKeySet); + } + + /** 88 WRK-EOF-Y VALUE '1'.(WS-FILE-STATUS 配下の 88) */ + private boolean isEofY() { + return "1".equals(wsFileStatus); + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 出力レコード(FILE SECTION → DTO) ==================== + + /** 01 W01OUTREC.(FD W01OUTFIL / COPY KYU02REC REPLACING ==(A)== BY ==W01== → dto.Kyu02Rec を再利用) */ + private final Kyu02Rec w01 = new Kyu02Rec(); + /** 01 W02OUTREC.(FD W02OUTFIL / COPY KYU99REC)※現時点出力なし */ + private final Kyu99Rec w02 = new Kyu99Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private BufferedReader r01InnFil; + private PrintWriter w01OutFil; + private PrintWriter w02OutFil; + + /** 入力ファイルパス。デフォルトは SELECT ASSIGN 対応の R01INN_FILE。 */ + private final String r01InnFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + final String inputFile = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + new Kyu03AggMain(inputFile).run(); + } + + public Kyu03AggMain() { + this(R01INN_FILE); + } + + public Kyu03AggMain(final String r01InnFile) { + this.r01InnFile = r01InnFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR(初回READ) → PERFORM 2000MAJSOR UNTIL WRK-EOF-Y → PERFORM 3000STPSOR → GOBACK. + * ※初回READで EOF(空入力)の場合は 0 件処理とする(事前判定ループ)。 + */ + private void run() { + try { + initPara(); + while (!isEofY()) { // 初回READでEOF(空入力)なら 0 件 + majPara(); + } + termPara(); + } finally { + closeFiles(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→OPEN→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※FUNCTION WHEN-COMPILED→起動時刻で代用 + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + resetWrkara(); + + // *** 入出力ファイル OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. + openFiles(); + + // *** 初回READ + readR01Para(); + } + + /** INITIALIZE WRKARA 相当:数値→0、文字→SPACE。 */ + private void resetWrkara() { + wrkEmpId = ""; + wrkYearMonth = ""; + wrkAbsentHours = BigDecimal.ZERO; + wrkPrevEmpId = ""; + wrkPrevKeySet = "0"; + wrkTotalHours = BigDecimal.ZERO; + wrkAbsentCount = 0; + wrkEof = ""; + wsPadChar = " "; + wsFileStatus = "0"; + } + + /** + * 段落 1100R01INNSOR:R01読込処理。 + * READ R01INNFIL INTO WRK-ABSENCE-REC AT END SET WRK-EOF-Y TO TRUE + * / NOT AT END ADD 1 TO CUN-R01INN. + */ + private void readR01Para() { + try { + final String line = r01InnFil.readLine(); + if (line == null) { + // AT END: SET WRK-EOF-Y TO TRUE → WS-FILE-STATUS = '1' + wsFileStatus = "1"; + } else { + // NOT AT END: ADD 1 TO CUN-R01INN.(および READ INTO WRK-ABSENCE-REC の各項目格納) + cunR01Inn++; + // MOVE 位置指定で WRK-ABSENCE-REC 各項目へ(80B 固定長の位置割当) + wrkEmpId = ConvUtil.safeSub(line, 0, 8); // 01-08 EMP-ID + wrkYearMonth = ConvUtil.safeSub(line, 8, 14); // 09-14 YEAR-MONTH + final String ahStr = ConvUtil.safeSub(line, 14, 19); // 15-19 ABSENT-HOURS(9(4)V9(1)=5桁) + wrkAbsentHours = ConvUtil.toDecimal(ahStr, 1); + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理(キーブレイク集約)。 + * 初回設定→同一社員は加算継続→異社員出現時に前社員集約出力、その後前レコード更新。 + */ + private void majPara() { + if (isPrevY()) { + if (wrkPrevEmpId.equals(wrkEmpId)) { + // *** 同一社員 → 集約加算 + // ADD 1 TO WRK-ABSENT-COUNT + wrkAbsentCount++; + // ADD WRK-ABSENT-HOURS TO WRK-TOTAL-HOURS + wrkTotalHours = wrkTotalHours.add(wrkAbsentHours); + } else { + // *** 異社員 → 前社員の集約出力 + writeOutPara(); + // MOVE WRK-EMP-ID TO WRK-PREV-EMP-ID + wrkPrevEmpId = wrkEmpId; + // INITIALIZE WRK-TOTAL-HOURS WRK-ABSENT-COUNT REPLACING NUMERIC DATA BY ZERO + wrkTotalHours = BigDecimal.ZERO; + wrkAbsentCount = 0; + // MOVE 1 TO WRK-ABSENT-COUNT + wrkAbsentCount = CST_ONE; + // MOVE WRK-ABSENT-HOURS TO WRK-TOTAL-HOURS + wrkTotalHours = wrkAbsentHours; + } + } else { + // *** 初回 + // MOVE WRK-EMP-ID TO WRK-PREV-EMP-ID + wrkPrevEmpId = wrkEmpId; + // SET WRK-PREV-Y TO TRUE + wrkPrevKeySet = "1"; + // MOVE CNS-ONE TO WRK-ABSENT-COUNT + wrkAbsentCount = CST_ONE; + // MOVE WRK-ABSENT-HOURS TO WRK-TOTAL-HOURS + wrkTotalHours = wrkAbsentHours; + } + + // *** PERFORM 1100R01INNSOR(次レコード読込) + readR01Para(); + } + + /** + * 段落 2100WRITOUTSOR:集約レコード出力処理。 + * INITIALIZE W01OUTREC → MOVE 各項目 → WRITE W01OUTREC → ADD 1 TO CUN-W01OUT. + */ + private void writeOutPara() { + // INITIALIZE W01OUTREC. + w01.reset(); + // MOVE WRK-PREV-EMP-ID TO W01EMP-ID. + w01.setEmpId(wrkPrevEmpId); + // MOVE WRK-YEAR-MONTH TO W01YEAR-MONTH. + w01.setYearMonth(wrkYearMonth); + // MOVE WRK-TOTAL-HOURS TO W01TOTAL-HOURS. + w01.setTotalHours(wrkTotalHours); + // MOVE WRK-ABSENT-COUNT TO W01ABSENT-COUNT. + w01.setAbsentCount(wrkAbsentCount); + // WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** + * 段落 3000STPSOR:終了処理。 + * 最終社員出力→CLOSE→入出力件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + // *** 最終社員の集約出力(CLOSE の前に出力すること) + if (isPrevY()) { + writeOutPara(); + } + + // CLOSE R01INNFIL W01OUTFIL W02OUTFIL. + closeFiles(); + + // *** 入出力件数メッセージ + msgOutPara(CST_MSGIINKES, "KYU03R01", ConvUtil.padLeftZero(cunR01Inn, 9)); + msgOutPara(CST_MSGOUTKES, "KYU03W01", ConvUtil.padLeftZero(cunW01Out, 9)); + msgOutPara(CST_MSGOUTKES, "KYU03W02", ConvUtil.padLeftZero(cunW02Out, 9)); + + // *** 終了メッセージ + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + * ※本プログラム手順からは呼出されない(COBOL定義のみ)が1:1再現のため残置。 + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), StandardCharsets.UTF_8)); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), StandardCharsets.UTF_8)); + w02OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W02OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** CLOSE R01INNFIL W01OUTFIL W02OUTFIL. */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w01OutFil); + closeQuiet(w02OutFil); + r01InnFil = null; + w01OutFil = null; + w02OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } +} diff --git a/JavaSrc/src/Kyu04CalMain.java b/JavaSrc/src/Kyu04CalMain.java new file mode 100644 index 0000000..00d9ba3 --- /dev/null +++ b/JavaSrc/src/Kyu04CalMain.java @@ -0,0 +1,634 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import dto.Kyu99Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * Sub01DatSub / Sub02MsgSub / Sub03EndSub は既定パッケージ(src/)のため import 不要。 + * 同一パッケージ(既定)から単純名で参照可能。 + * ============================================================ + * 【COBOL→Java】KYU04CAL 給与計算処理 + * ============================================================ + * PROGRAM-ID. KYU04CAL → 类 Kyu04CalMain(PGMパターン:1:N+キーブレイク) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R02INNFIL(KYU04R02 ABSENCE-AGGREGATE 入力 80B) → R02INN_FILE(COPY KYU02REC → dto.Kyu02Rec) + * R03INNFIL(KYU04R03 PAY-PARAM 入力 80B CSV) → R03INN_FILE(自前定義/UNSTRING) + * W03OUTFIL(KYU04W03 SALARY-DETAIL 出力 240B) → W03OUT_FILE(COPY KYU03REC → dto.Kyu03Rec) + * W04OUTFIL(KYU04W04 ERROR-LOG 出力 200B VB) → W04OUT_FILE(COPY KYU99REC → dto.Kyu99Rec 再利用) + * 使用DB(EXEC SQL → JDBC): + * EMP-MASTER(SELECT)/OVT-MONTHLY(SELECT SUM) + * 使用SUB(CALL → 类#execute): + * SUB01DAT→Sub01DatSub / SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R02INNSOR→readR02Para() + * 2000MAJSOR→majPara() 2100CALCSOR→calcPara() 2110WRITOUTSOR→writeOutPara() + * 2200ERROUTSOR→errorOutPara() 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() + * 9999ABDSOR→abendPara() + * + * 处理要点(詳細設計書_KYU04CAL 準拠): + * - R02(ABSENCE-AGGREGATE)を全件読み、EMP-MASTER/OVT-MONTHLY を参照し給与計算。 + * - 欠勤控除額→時間外手当→EVALUATE ALSO(勤怠区分×手当種別)→支給総額→SALARY-DETAIL出力。 + * - 金額計算は PIC 9(009) の桁溢れを COBOL の ON SIZE ERROR 相当として 0 クリア。 + * - ROUNDED は BigDecimal.setScale(0, RoundingMode.HALF_UP) で実現。 + */ +public class Kyu04CalMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU04CAL'. */ + private static final String CST_PRGIDX = "KYU04CAL"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. サブエラーメッセージ */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数メッセージ */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数メッセージ */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-OVERTIME-RATE PIC 9(002)V9(002) VALUE 01.25. */ + private static final BigDecimal CST_OVERTIME_RATE = new BigDecimal("1.25"); + /** 03 CNS-HOLIDAY-RATE PIC 9(001)V9(02) VALUE 1.35.(宣言のみ・本計算では未使用) */ + @SuppressWarnings("unused") + private static final BigDecimal CST_HOLIDAY_RATE = new BigDecimal("1.35"); + + /** PIC 9(009) の最大値(ON SIZE ERROR 判定用)。 */ + private static final BigDecimal MAX_PIC9_9 = BigDecimal.valueOf(999_999_999L); + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R02INNFIL ASSIGN TO KYU04R02.(ABSENCE-AGGREGATE 入力) */ + private static final String R02INN_FILE = "data/KYU04R02.txt"; + /** SELECT R03INNFIL ASSIGN TO KYU04R03.(PAY-PARAM 入力 CSV) */ + private static final String R03INN_FILE = "data/KYU04R03.txt"; + /** SELECT W03OUTFIL ASSIGN TO KYU04W03.(SALARY-DETAIL 出力) */ + private static final String W03OUT_FILE = "data/KYU04W03.txt"; + /** SELECT W04OUTFIL ASSIGN TO KYU04W04.(ERROR-LOG 出力) */ + private static final String W04OUT_FILE = "data/KYU04W04.txt"; + + // ==================== DB接続(EXEC SQL CONNECT 相当) ==================== + + /** CONNECT 先(DbSample に準拠:DB2 URL プレースホルダ。テスト時は -Dkyu.db.url で上書き)。 */ + private static final String DB_URL = System.getProperty("kyu.db.url", "jdbc:db2://localhost:50000/TESTDB"); + private static final String DB_USER = "db2inst1"; + private static final String DB_PASS = "password"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R02INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR02Inn; + /** 03 CUN-W03OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW03Out; + /** 03 CUN-W04OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW04Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-EOF PIC X(001). 読込フラグ(88 WRK-EOF-Y VALUE '1') */ + private String wrkEof = "0"; + /** 03 WRK-PARAM-BASE PIC X(080).(PAY-PARAM 読込バッファ) */ + private String wrkParamBase = ""; + /** 03 WRK-YEAR-MONTH PIC X(006).(対象年月=設計に従い PAY-PARAM から設定) */ + private String wrkYearMonth = ""; + /** 03 WRK-EMP-ID PIC X(008). */ + private String wrkEmpId = ""; + /** 03 WRK-EMP-NAME PIC X(040). */ + private String wrkEmpName = ""; + /** 03 WRK-DEPT-CODE PIC X(002). */ + private String wrkDeptCode = ""; + /** 03 WRK-REGION-CODE PIC X(002).(EMP-MASTER より取得・FYI用未使用) */ + private String wrkRegionCode = ""; + /** 03 WRK-CATEGORY-CODE PIC X(003).(EMP-MASTER より取得・FYI用未使用) */ + private String wrkCategoryCode = ""; + /** 03 WRK-BASE-SALARY PIC 9(009). */ + private long wrkBaseSalary; + /** 03 WRK-HOURLY-RATE PIC 9(007). */ + private long wrkHourlyRate; + /** 03 WRK-DEPENDENT-COUNT PIC 9(002). */ + private int wrkDependentCount; + /** 03 WRK-ABSENT-HOURS PIC 9(004)V9(001). */ + private BigDecimal wrkAbsentHours = BigDecimal.ZERO; + /** 03 WRK-OVT-HOURS PIC 9(004)V9(001).(宣言のみ・計算は DBV-OVT-HOURS を使用) */ + private BigDecimal wrkOvtHours = BigDecimal.ZERO; + /** 03 WRK-ABSENT-DEDUCT PIC 9(009). */ + private long wrkAbsentDeduct; + /** 03 WRK-OVT-AMOUNT PIC 9(009). */ + private long wrkOvtAmount; + /** 03 WRK-GROSS-PAYMENT PIC 9(009). */ + private long wrkGrossPayment; + /** 03 WRK-ALLOWANCE-TYPE PIC X(003). */ + private String wrkAllowanceType = ""; + /** 03 WRK-ATTEND-TYPE PIC X(001). */ + private String wrkAttendType = ""; + /** 03 WRK-AMOUNT PIC 9(009).(EVALUATE ALSO 算出・出力非使用) */ + private long wrkAmount; + /** 03 WS-FILE-STATUS PIC X(002).(宣言のみ・FILE STATUS 未管理) */ + @SuppressWarnings("unused") + private String wsFileStatus = ""; + /** 03 WS-PAD-CHAR PIC X(001).(宣言のみ) */ + @SuppressWarnings("unused") + private String wsPadChar = " "; + + /** 88 WRK-EOF-Y VALUE '1'. */ + private boolean isEof() { + return "1".equals(wrkEof); + } + + // ==================== DB2ホスト変数領域(DBVARA → 本クラスフィールド) ==================== + + /** 03 DBV-EMP-ID PIC X(008). */ + private String dbvEmpId = ""; + /** 03 DBV-BASE-SALARY PIC 9(009). */ + private long dbvBaseSalary; + /** 03 DBV-HOURLY-RATE PIC 9(007). */ + private long dbvHourlyRate; + /** 03 DBV-DEPENDENT-COUNT PIC 9(002). */ + private int dbvDependentCount; + /** 03 DBV-EMPLOYEE-NAME PIC X(040). */ + private String dbvEmployeeName = ""; + /** 03 DBV-DEPT-CODE PIC X(002). */ + private String dbvDeptCode = ""; + /** 03 DBV-OVT-HOURS PIC 9(004)V9(001).(小数点以下1桁) */ + private BigDecimal dbvOvtHours = BigDecimal.ZERO; + /** 03 DBV-STATUS PIC X(001). */ + private String dbvStatus = ""; + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + /** COPY ZANDATAC.(SUB01DAT用) */ + private final ZandatacPar d01UbsPar = new ZandatacPar(); + + // ==================== 入出力レコード(FILE SECTION → DTO) ==================== + + /** 01 R02INNREC.(FD R02INNFIL / COPY KYU02REC)— 固定長行を直接解析(DTO非依存) */ + private String r02EmpId = ""; + private String r02YearMonth = ""; + private BigDecimal r02TotalHours = BigDecimal.ZERO; + private int r02AbsentCount; + /** 01 W04OUTREC.(FD W04OUTFIL / COPY KYU99REC 再利用) */ + private final Kyu99Rec w04 = new Kyu99Rec(); + + // ==================== ファイルハンドル / DB接続(OPEN/CLOSE 相当) ==================== + + private BufferedReader r02InnFil; + private PrintWriter w03OutFil; + private PrintWriter w04OutFil; + private Connection conn; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + new Kyu04CalMain().run(); + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!isEof()) { // PERFORM 2000MAJSOR UNTIL WRK-EOF = '1' + majPara(); + } + termPara(); + } finally { + closeFiles(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始→コンパイル日時→ワーク初期化→運用日付取得(SUB01DAT)→ + * PAY-PARAM読込/解析→R02/W03/W04 OPEN→DB接続→初回R02読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※FUNCTION WHEN-COMPILED→起動時刻で代用 + final String compiled = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + initWrk(); + + // *** 運用日付取得 SUB01DAT + d01UbsPar.reset(); + Sub01DatSub.execute(d01UbsPar); + if (d01UbsPar.getD01FkiCod() != 0) { + // 復帰コード≠ZERO → サブエラーメッセージ+ABEND + msgOutPara(CST_MSGSUBEEK, "SUB01DAT", String.valueOf(d01UbsPar.getD01FkiCod())); + abendPara(); + } + + // *** PAY-PARAM読込(OPEN INPUT R03INNFIL / READ / CLOSE) + readParam(); + + // *** パラメータ解析(UNSTRING WRK-PARAM-BASE DELIMITED BY ',' INTO WRK-YEAR-MONTH) + // 設計準拠:対象年月は PAY-PARAM の第1フィールドを使用(COBOL は R02 で上書きしているが設計優先)。 + final String[] tokens = wrkParamBase.split(",", -1); + if (tokens.length > 0) { + wrkYearMonth = ConvUtil.padRight(tokens[0].trim(), 6); + } + + // *** 使用ファイル OPEN(R02 INPUT / W03,W04 OUTPUT) + openFiles(); + + // *** DB接続(EXEC SQL CONNECT TO ...) + connectDb(); + + // *** 初回R02読込(1100R02INNSOR) + readR02Para(); + } + + /** INITIALIZE WRKARA 相当(数値→0、文字→SPACE)。 */ + private void initWrk() { + wrkEof = "0"; + wrkParamBase = ""; + wrkYearMonth = ""; + wrkEmpId = ""; + wrkEmpName = ""; + wrkDeptCode = ""; + wrkRegionCode = ""; + wrkCategoryCode = ""; + wrkBaseSalary = 0; + wrkHourlyRate = 0; + wrkDependentCount = 0; + wrkAbsentHours = BigDecimal.ZERO; + wrkOvtHours = BigDecimal.ZERO; + wrkAbsentDeduct = 0; + wrkOvtAmount = 0; + wrkGrossPayment = 0; + wrkAllowanceType = ""; + wrkAttendType = ""; + wrkAmount = 0; + wsFileStatus = ""; + wsPadChar = " "; + } + + /** + * 段落 1100R02INNSOR:R02読込処理。 + * READ R02INNFIL AT END → SET WRK-EOF-Y / NOT AT END → ADD 1 TO CUN-R02INN. + */ + private void readR02Para() { + try { + final String line = r02InnFil.readLine(); + if (line == null) { + // AT END: SET WRK-EOF-Y TO TRUE. + wrkEof = "1"; + } else { + // NOT AT END: 固定長行を 05 層定義の位置で切出し(COPY KYU02REC 準拠) + // EMP-ID(1:8)/YEAR-MONTH(9:14)/TOTAL-HOURS(15:19)=9(4)V9(1)/ABSENT-COUNT(20:21) + r02EmpId = ConvUtil.padRight(ConvUtil.safeSub(line, 0, 8), 8); + r02YearMonth = ConvUtil.padRight(ConvUtil.safeSub(line, 8, 14), 6); + r02TotalHours = ConvUtil.toDecimal(ConvUtil.safeSub(line, 14, 19), 1); + r02AbsentCount = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 19, 21), 0); + cunR02Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * ホスト変数セット→EMP-MASTER参照(SQLCODE≠0ならエラー出力+次件)→ + * OVT-MONTHLY参照→給与計算→SALARY-DETAIL出力→次件読込。 + */ + private void majPara() { + // *** ホスト変数セット MOVE R02EMP-ID TO DBV-EMP-ID. + dbvEmpId = r02EmpId; + + // *** DB2 EMP-MASTER参照(EXEC SQL SELECT ... INTO :DBV-*) + boolean empOk; + try { + empOk = selectEmpMaster(); + } catch (SQLException e) { + // SQLCODE≠0 に相当 + empOk = false; + } + if (!empOk) { + // SQLCODE NOT = 0 → 2200ERROUTSOR + 次件読込 + EXIT SECTION + errorOutPara(); + readR02Para(); + return; + } + + // *** MOVE EMP-MASTER項目 → WRK-*(出力用) + wrkEmpName = dbvEmployeeName; + wrkBaseSalary = dbvBaseSalary; + wrkHourlyRate = dbvHourlyRate; + wrkDeptCode = dbvDeptCode; + // MOVE R02YEAR-MONTH TO WRK-YEAR-MONTH. ※設計優先で PAY-PARAM の値を維持(上書きしない) + wrkAbsentHours = r02TotalHours; + + // *** DB2 OVT-MONTHLY参照(EXEC SQL SELECT SUM(OVT-HOURS)) + selectOvtMonthly(); + + // *** 給与計算(2100CALCSOR) + calcPara(); + + // *** SALARY-DETAIL出力(2110WRITOUTSOR) + writeOutPara(); + + // *** 次件読込(1100R02INNSOR) + readR02Para(); + } + + /** + * EXEC SQL:EMP-MASTER 参照(SELECT ... INTO :DBV-*)。 + * @return 正常に取得できた場合 true(SQLCODE=0 かつ該当行あり)、 + * 該当行なし/異常時 false(SQLCODE≠0 に相当) + */ + private boolean selectEmpMaster() throws SQLException { + final String sql = "SELECT \"EMP-NAME\", \"DEPT-CODE\", \"REGION-CODE\", \"CATEGORY-CODE\", " + + "\"BASE-SALARY\", \"HOURLY-RATE\", \"DEPENDENT-COUNT\", \"STATUS\" " + + "FROM \"EMP-MASTER\" WHERE \"EMP-ID\" = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, dbvEmpId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return false; // SQLCODE=100(該当なし)→ エラー扱い + } + String name = rs.getString("EMP-NAME"); + dbvEmployeeName = (name == null) ? "" : name; + String dept = rs.getString("DEPT-CODE"); + dbvDeptCode = (dept == null) ? "" : dept; + String region = rs.getString("REGION-CODE"); + wrkRegionCode = (region == null) ? "" : region; // :WRK-REGION-CODE + String cat = rs.getString("CATEGORY-CODE"); + wrkCategoryCode = (cat == null) ? "" : cat; // :WRK-CATEGORY-CODE + dbvBaseSalary = rs.getLong("BASE-SALARY"); // :DBV-BASE-SALARY + dbvHourlyRate = rs.getLong("HOURLY-RATE"); // :DBV-HOURLY-RATE + dbvDependentCount = rs.getInt("DEPENDENT-COUNT"); // :DBV-DEPENDENT-COUNT + String status = rs.getString("STATUS"); + dbvStatus = (status == null) ? "" : status; // :DBV-STATUS + return true; + } + } + } + + /** + * EXEC SQL:OVT-MONTHLY 参照(SELECT SUM(OVT-HOURS) ... INTO :DBV-OVT-HOURS)。 + * SQLCODE≠0 の場合は ZERO を設定(COBOL: MOVE ZERO TO DBV-OVT-HOURS)。 + */ + private void selectOvtMonthly() { + BigDecimal ovt = BigDecimal.ZERO; + try { + final String sql = "SELECT SUM(\"OVT-HOURS\") FROM \"OVT-MONTHLY\" " + + "WHERE \"EMP-ID\" = ? AND \"YEAR-MONTH\" = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, dbvEmpId); + ps.setString(2, wrkYearMonth); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + final BigDecimal s = rs.getBigDecimal(1); + if (s != null) { + ovt = s; // SUM 結果(小数点以下1桁想定) + } + } + } + } + } catch (SQLException e) { + ovt = BigDecimal.ZERO; // SQLCODE≠0 → ZERO + } + dbvOvtHours = ovt; + } + + /** + * 段落 2100CALCSOR:給与計算処理。 + * 欠勤控除額→時間外手当→EVALUATE ALSO(勤怠区分×手当種別)→支給総額。 + */ + private void calcPara() { + // *** 欠勤控除額 COMPUTE WRK-ABSENT-DEDUCT ROUNDED = HOURLY-RATE * ABSENT-HOURS ON SIZE ERROR ZERO + final BigDecimal absentVal = new BigDecimal(wrkHourlyRate).multiply(wrkAbsentHours); + wrkAbsentDeduct = roundedP9(absentVal); + + // *** 時間外手当 COMPUTE WRK-OVT-AMOUNT ROUNDED = HOURLY-RATE * OVT-HOURS * 1.25 ON SIZE ERROR ZERO + final BigDecimal ovtVal = new BigDecimal(wrkHourlyRate) + .multiply(dbvOvtHours) + .multiply(CST_OVERTIME_RATE); + wrkOvtAmount = roundedP9(ovtVal); + + // *** EVALUATE ALSO(勤怠区分×手当種別) + // MOVE 'OVT' TO WRK-ALLOWANCE-TYPE./MOVE 'W' TO WRK-ATTEND-TYPE. + wrkAllowanceType = "OVT"; + wrkAttendType = "W"; + + final BigDecimal hr = new BigDecimal(wrkHourlyRate); + final BigDecimal ovt = dbvOvtHours; + final BigDecimal amt; + if ("OVT".equals(wrkAllowanceType)) { + if ("W".equals(wrkAttendType)) { + // WHEN 'OVT' ALSO 'W' → HOURLY-RATE * OVT-HOURS * 1.25 + amt = hr.multiply(ovt).multiply(new BigDecimal("1.25")); + } else if ("H".equals(wrkAttendType)) { + // WHEN 'OVT' ALSO 'H' → * 1.35 + amt = hr.multiply(ovt).multiply(new BigDecimal("1.35")); + } else { + amt = BigDecimal.ZERO; + } + } else if ("LATE".equals(wrkAllowanceType)) { + // WHEN 'LATE' ALSO ANY → * 0.50 + amt = hr.multiply(ovt).multiply(new BigDecimal("0.50")); + } else { + // WHEN OTHER → ZERO + amt = BigDecimal.ZERO; + } + wrkAmount = roundedP9(amt); + + // *** 支給総額 COMPUTE WRK-GROSS-PAYMENT = BASE-SALARY + OVT-AMOUNT - ABSENT-DEDUCT + wrkGrossPayment = wrkBaseSalary + wrkOvtAmount - wrkAbsentDeduct; + } + + /** + * 段落 2110WRITOUTSOR:SALARY-DETAIL出力処理。 + * MOVE 各項目→W03OUTREC(240B 固定長)/ WRITE / ADD 1 TO CUN-W03OUT. + * ※W03 レコードは COPY KYU03REC のレイアウトに従いインライン構築(DTO非依存)。 + * 配置: EMP-ID(8)+EMP-NAME(40)+DEPT-CODE(2)+BASE-SALARY(9)+OVT-AMOUNT(9) + * +HOLIDAY-AMOUNT(9)+ABSENT-DEDUCT(9)+GROSS-PAYMENT(9)+OVT-HOURS(5)+ABSENT-HOURS(5) + * +ATTEND-TYPE(1)+ALLOWANCE-TYPE(3)+FILLER(131) = 240B + */ + private void writeOutPara() { + final String line = ConvUtil.padRight(r02EmpId, 8) // MOVE R02EMP-ID + + ConvUtil.padRight(dbvEmployeeName, 40) // MOVE WRK-EMP-NAME + + ConvUtil.padRight(dbvDeptCode, 2) // MOVE WRK-DEPT-CODE + + ConvUtil.padLeftZero(dbvBaseSalary, 9) // MOVE DBV-BASE-SALARY + + ConvUtil.padLeftZero(wrkOvtAmount, 9) // MOVE WRK-OVT-AMOUNT + + ConvUtil.padLeftZero(0L, 9) // MOVE ZERO(固定) + + ConvUtil.padLeftZero(wrkAbsentDeduct, 9) // MOVE WRK-ABSENT-DEDUCT + + ConvUtil.padLeftZero(wrkGrossPayment, 9) // MOVE WRK-GROSS-PAYMENT + + ConvUtil.formatZoned(dbvOvtHours, 4, 1) // MOVE DBV-OVT-HOURS + + ConvUtil.formatZoned(r02TotalHours, 4, 1) // MOVE WRK-ABSENT-HOURS(R02TOTAL-HOURS) + + ConvUtil.padRight(wrkAttendType, 1) // MOVE WRK-ATTEND-TYPE + + ConvUtil.padRight(wrkAllowanceType, 3) // MOVE WRK-ALLOWANCE-TYPE + + ConvUtil.padRight("", 131); // FILLER X(131) + w03OutFil.println(line); + cunW03Out++; + } + + /** + * 段落 2200ERROUTSOR:エラー出力処理。 + * MOVE 'DB-ERR' TO W04ERR-CATEGORY./MOVE R02EMP-ID TO W04ERR-DETAIL./WRITE / ADD 1 TO CUN-W04OUT. + */ + private void errorOutPara() { + w04.reset(); + w04.setErrCategory("DB-ERR"); + w04.setErrDetail(r02EmpId); + w04OutFil.println(w04.toRecordLine()); + cunW04Out++; + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→入出力件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + closeFiles(); + msgOutPara(CST_MSGIINKES, "KYU04R02", ConvUtil.padLeftZero(cunR02Inn, 9)); + msgOutPara(CST_MSGOUTKES, "KYU04W03", ConvUtil.padLeftZero(cunW03Out, 9)); + msgOutPara(CST_MSGOUTKES, "KYU04W04", ConvUtil.padLeftZero(cunW04Out, 9)); + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR.(System.exit で異常終了) + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R02INNFIL / OUTPUT W03OUTFIL W04OUTFIL. */ + private void openFiles() { + try { + r02InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(R02INN_FILE), StandardCharsets.UTF_8)); + w03OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W03OUT_FILE), StandardCharsets.UTF_8)); + w04OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W04OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** CLOSE R02INNFIL W03OUTFIL W04OUTFIL(idempotent)。 */ + private void closeFiles() { + closeQuiet(r02InnFil); + closeQuiet(w03OutFil); + closeQuiet(w04OutFil); + r02InnFil = null; + w03OutFil = null; + w04OutFil = null; + closeQuiet(conn); + conn = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } else if (handle instanceof Connection) { + ((Connection) handle).close(); + } + } catch (Exception e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } + + /** PAY-PARAM(R03) 読込:OPEN INPUT / READ INTO WRK-PARAM-BASE / CLOSE. */ + private void readParam() { + try (BufferedReader r03 = new BufferedReader( + new InputStreamReader(new FileInputStream(R03INN_FILE), StandardCharsets.UTF_8))) { + final String line = r03.readLine(); + wrkParamBase = (line == null) ? "" : line; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** EXEC SQL CONNECT TO ...(DbSample 準拠:DriverManager + setAutoCommit(false))。 */ + private void connectDb() { + try { + conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); + conn.setAutoCommit(false); // バッチ標準:明示的トランザクション制御 + } catch (SQLException e) { + // 接続失敗は ABEND 相当(メッセージ出力せずそのまま異常終了) + throw new UncheckedIOException(new IOException("DB CONNECT FAILED: " + e.getMessage())); + } + } + + // ==================== 数値計算補助 ==================== + + /** + * COMPUTE ... ROUNDED(PIC 9(009))相当。 + * 四捨五入(HALF_UP)の上、桁溢れ(ON SIZE ERROR)なら 0 を返す。 + */ + private static long roundedP9(final BigDecimal v) { + final BigDecimal r = v.setScale(0, RoundingMode.HALF_UP); + if (r.compareTo(MAX_PIC9_9) > 0) { + return 0L; // ON SIZE ERROR → MOVE ZERO + } + return r.longValue(); + } +} diff --git a/JavaSrc/src/Kyu05DedMain.java b/JavaSrc/src/Kyu05DedMain.java new file mode 100644 index 0000000..187bf0f --- /dev/null +++ b/JavaSrc/src/Kyu05DedMain.java @@ -0,0 +1,561 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import dto.Kyu03Rec; +import dto.Kyu04Rec; +import dto.Kyu99Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU05DED 控除計算処理 + * ============================================================ + * PROGRAM-ID. KYU05DED → 类 Kyu05DedMain(PGMパターン:SELECT条件+DB参照) + * + * 使用ファイル(SELECT ASSIGN → パス定数): + * R01INNFIL(KYU05R01 入力 FB240B) → R01INN_FILE(COPY KYU03REC → dto.Kyu03Rec) + * W01OUTFIL(KYU05W01 出力 FB200B) → W01OUT_FILE(COPY KYU04REC → dto.Kyu04Rec) + * W02OUTFIL(KYU05W02 出力 VB200B) → W02OUT_FILE(COPY KYU99REC → dto.Kyu99Rec 再利用) + * 使用DB2(JDBC): + * TAX-TABLE :SELECT ... WHERE :WRK-TAXABLE-INCOME BETWEEN TAX-FROM AND TAX-TO + * INSURANCE-TABLE:DECLARE CURSOR → FETCH全件 → WORKING-STORAGE(INSURANCE-WORK) → SEARCH + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100INSLOADSOR→insLoadPara() + * 1200R01INNSOR→readR01Para() 2000MAJSOR→majPara() 2100WRITOUTSOR→writeW01Para() + * 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 処理要点(詳細設計書_KYU05DED 準拠): + * - 課税所得=総支給-基礎控除(50000)。所得税=課税所得×税率-控除額(ROUNDED、負は0)。 + * - 社会保険料=課税所得×(健康+厚生)比率(ROUNDED)。住民税=課税所得×0.10(ROUNDED)。 + * - 差引支給額=総支給-所得税-保険料-住民税。 + * - 金額計算は BigDecimal.setScale(0, HALF_UP) で COBOL ROUNDED を再現(转换基准 3.2)。 + * - 原COBOLの CONNECT TO 'data/SALARY.db' は SQLite 指定だが、設計書は DB2 を規定。 + * 转换基准「COBOLと式様書不一致は式様書優先」により DB2 URL を採用(報告書に注記)。 + */ +public class Kyu05DedMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU05DED'. */ + private static final String CST_PRGIDX = "KYU05DED"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005.(設計上予約、本処理では未使用) */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数メッセージ */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数メッセージ */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時メッセージ */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-TAX-BASIC-DEDUCTION PIC 9(006) VALUE 050000. 基礎控除 */ + private static final long CST_TAX_BASIC_DEDUCTION = 50000L; + /** 03 CNS-RESIDENT-TAX-RATE PIC 9(001)V9(02) VALUE 0.10. 住民税率 */ + private static final BigDecimal CST_RESIDENT_TAX_RATE = new BigDecimal("0.10"); + + /** 各SQL文の定数 */ + private static final String INS_SELECT_SQL = + "SELECT \"INS-INCOME-FROM\", \"INS-INCOME-TO\", \"INS-HEALTH-RATE\", \"INS-PENSION-RATE\" " + + "FROM \"INSURANCE-TABLE\" ORDER BY \"INS-INCOME-FROM\""; + private static final String TAX_SELECT_SQL = + "SELECT \"TAX-RATE\", \"DEDUCTION\" FROM \"TAX-TABLE\" WHERE ? BETWEEN \"TAX-FROM\" AND \"TAX-TO\""; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KYU05R01.(FB 入力 240B) */ + private static final String R01INN_FILE = "data/KYU05R01.txt"; + /** SELECT W01OUTFIL ASSIGN TO KYU05W01.(FB 出力 200B) */ + private static final String W01OUT_FILE = "data/KYU05W01.txt"; + /** SELECT W02OUTFIL ASSIGN TO KYU05W02.(VB 出力 200B) */ + private static final String W02OUT_FILE = "data/KYU05W02.txt"; + + // ==================== DB 接続(EXEC SQL CONNECT 相当) ==================== + + /** DB2 接続先(設計書準拠;原COBOLの 'data/SALARY.db' は SQLite 指定のため非採用。テスト時は -Dkyu.db.url で上書き)。 */ + private static final String DB_URL = System.getProperty("kyu.db.url", "jdbc:db2://localhost:50000/TESTDB"); + private static final String DB_USER = "db2inst1"; + private static final String DB_PASS = "password"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + /** 03 CUN-W02OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW02Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-EOF PIC X(001). 読込フラグ(88 WRK-EOF-Y VALUE '1') */ + private String wrkEof = "0"; + /** 03 WRK-GROSS-PAYMENT PIC 9(009). */ + private long wrkGrossPayment; + /** 03 WRK-TAXABLE-INCOME PIC 9(009). */ + private long wrkTaxableIncome; + /** 03 WRK-INCOME-TAX PIC 9(009). */ + private long wrkIncomeTax; + /** 03 WRK-INSURANCE PIC 9(009). */ + private long wrkInsurance; + /** 03 WRK-RESIDENT-TAX PIC 9(009). */ + private long wrkResidentTax; + /** 03 WRK-NET-PAYMENT PIC 9(009). */ + private long wrkNetPayment; + + /** 88 WRK-EOF-Y VALUE '1'. */ + private boolean isEof() { + return "1".equals(wrkEof); + } + + // ==================== DB2 ホスト変数 DBVARA ==================== + + /** 03 DBV-EMP-ID PIC X(008).(R01EMP-ID 設定、SQLでは未直接使用) */ + private String dbvEmpId = ""; + /** 03 DBV-TAX-RATE PIC 9(001)V9(04).(TAX-TABLE.TAX-RATE DECIMAL(5,4)) */ + private BigDecimal dbvTaxRate = BigDecimal.ZERO; + /** 03 DBV-TAX-DEDUCTION PIC 9(009).(TAX-TABLE.DEDUCTION DECIMAL(9,0)) */ + private long dbvTaxDeduction; + + // ==================== 保険料率 WORKING-STORAGE(SEARCH 相当) ==================== + + /** 03 IW-COUNT PIC 9(004).(OCCURS DEPENDING ON) */ + private int iwCount; + /** 最大 200 件(OCCURS 1 TO 200)。 */ + private static final int IW_MAX = 200; + /** 05 IW-INCOME-FROM PIC 9(009). */ + private final long[] iwIncomeFrom = new long[IW_MAX]; + /** 05 IW-INCOME-TO PIC 9(009). */ + private final long[] iwIncomeTo = new long[IW_MAX]; + /** 05 IW-HEALTH-RATE PIC V9(006).(0.xxxxxx、DECIMAL(7,6) 相当) */ + private final BigDecimal[] iwHealthRate = new BigDecimal[IW_MAX]; + /** 05 IW-PENSION-RATE PIC V9(006). */ + private final BigDecimal[] iwPensionRate = new BigDecimal[IW_MAX]; + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== レコード(FILE SECTION → DTO) ==================== + + /** 01 R01INNREC.(FD R01INNFIL / COPY KYU03REC) */ + private final Kyu03Rec r01 = new Kyu03Rec(); + /** 01 W01OUTREC.(FD W01OUTFIL / COPY KYU04REC) */ + private final Kyu04Rec w01 = new Kyu04Rec(); + /** 01 W02OUTREC.(FD W02OUTFIL / COPY KYU99REC 再利用) */ + private final Kyu99Rec w02 = new Kyu99Rec(); + + // ==================== ファイルハンドル / DB接続(OPEN/CLOSE 相当) ==================== + + private BufferedReader r01InnFil; + private PrintWriter w01OutFil; + private PrintWriter w02OutFil; + private Connection conn; + + /** 入力ファイルパス。デフォルトは SELECT ASSIGN 対応の R01INN_FILE。 */ + private final String r01InnFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + final String inputFile = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + new Kyu05DedMain(inputFile).run(); + } + + public Kyu05DedMain() { + this(R01INN_FILE); + } + + public Kyu05DedMain(final String r01InnFile) { + this.r01InnFile = r01InnFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!isEof()) { // PERFORM 2000MAJSOR UNTIL WRK-EOF = '1' + majPara(); + } + termPara(); + // EXEC SQL COMMIT END-EXEC(正常終了時) + if (conn != null) { + conn.commit(); + } + } catch (SQLException e) { + // EXEC SQL WHENEVER SQLERROR 相当(DbSample パターン) + System.out.println("[db] SQLERROR code=" + e.getErrorCode() + " msg=" + e.getMessage()); + rollbackQuiet(conn); + abendPara(); // MOVE 999 TO E01ABDCOD / CALL SUB03END(System.exit) + } finally { + closeFiles(); + closeQuiet(conn); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→DB接続→INSURANCE-TABLE LOAD→OPEN→初回READ. + */ + private void initPara() throws SQLException { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※FUNCTION WHEN-COMPILED→起動時刻で代用 + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + cunR01Inn = 0; + cunW01Out = 0; + cunW02Out = 0; + wrkEof = "0"; + wrkGrossPayment = 0; + wrkTaxableIncome = 0; + wrkIncomeTax = 0; + wrkInsurance = 0; + wrkResidentTax = 0; + wrkNetPayment = 0; + dbvEmpId = ""; + dbvTaxRate = BigDecimal.ZERO; + dbvTaxDeduction = 0; + iwCount = 0; + + // *** DB接続 EXEC SQL CONNECT TO 'data/SALARY.db'(設計書準拠で DB2 URL を使用) + conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); + conn.setAutoCommit(false); // 批処理標準(DbSample パターン) + + // *** 使用ファイル OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. + openFiles(); + + // *** INSURANCE-TABLE 全件 LOAD(1100INSLOADSOR) + insLoadPara(); + + // *** 初回 READ(1200R01INNSOR) + readR01Para(); + } + + /** + * 段落 1100INSLOADSOR:INSURANCE-TABLE 全件 LOAD 処理。 + * DECLARE/OPEN CURSOR → FETCH ループ → INSURANCE-WORK 配列へ格納 → CLOSE CURSOR. + * OPEN/FETCH 異常(SQLCODE NOT = 0)時は W02 に詳細を書き ABEND. + */ + private void insLoadPara() { + try { + // EXEC SQL DECLARE INS-CURSOR CURSOR FOR ... / OPEN は executeQuery で暗黙実行 + try (PreparedStatement ps = conn.prepareStatement(INS_SELECT_SQL); + ResultSet rs = ps.executeQuery()) { // OPEN INS-CURSOR + // PERFORM UNTIL SQLCODE NOT = 0(rs.next() == false で終了) + while (rs.next()) { + iwCount++; // ADD 1 TO IW-COUNT + final int idx = iwCount - 1; // 1-based → 0-based + // MOVE DBV-INS-* TO IW-*(OW 配列へ格納) + iwIncomeFrom[idx] = rs.getLong("INS-INCOME-FROM"); + iwIncomeTo[idx] = rs.getLong("INS-INCOME-TO"); + // DECIMAL(7,6) → scale 6(PIC V9(006) 相当) + iwHealthRate[idx] = rs.getBigDecimal("INS-HEALTH-RATE"); + iwPensionRate[idx] = rs.getBigDecimal("INS-PENSION-RATE"); + } + } // CLOSE INS-CURSOR(自動) + } catch (SQLException e) { + // IF SQLCODE NOT = 0 → MOVE 'INS-CURSOR OPEN ERR' TO W02ERR-DETAIL / PERFORM 9999ABDSOR + w02.reset(); + w02.setErrDetail("INS-CURSOR OPEN ERR"); + writeW02(); + abendPara(); + } + } + + /** + * 段落 1200R01INNSOR:R01 読込処理。 + * READ AT END → SET WRK-EOF-Y / NOT AT END → ADD 1 TO CUN-R01INN. + */ + private void readR01Para() { + try { + final String line = r01InnFil.readLine(); + if (line == null) { + // AT END: SET WRK-EOF-Y TO TRUE. + wrkEof = "1"; + } else { + // NOT AT END: MOVE R01INNREC … / ADD 1 TO CUN-R01INN. + r01.setRecordLine(line); + cunR01Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * 課税所得→所得税(SELECT BETWEEN)→社会保険料(SEARCH)→住民税→差引支給額→W01出力→次READ. + */ + private void majPara() { + // MOVE R01GROSS-PAYMENT TO WRK-GROSS-PAYMENT. + wrkGrossPayment = r01.getGrossPayment(); + // MOVE R01EMP-ID TO DBV-EMP-ID. + dbvEmpId = r01.getEmpId(); + + // *** 課税所得計算(IF WRK-GROSS-PAYMENT < CNS-TAX-BASIC-DEDUCTION …) + if (wrkGrossPayment < CST_TAX_BASIC_DEDUCTION) { + // MOVE ZERO TO WRK-TAXABLE-INCOME. + wrkTaxableIncome = 0; + } else { + // COMPUTE WRK-TAXABLE-INCOME = WRK-GROSS-PAYMENT - CNS-TAX-BASIC-DEDUCTION. + wrkTaxableIncome = wrkGrossPayment - CST_TAX_BASIC_DEDUCTION; + } + + // *** TAX-TABLE 参照(SELECT ... BETWEEN)→ 所得税計算 + taxSelectPara(); + + // *** 社会保険料計算(SEARCH IW-ENTRY) + insSearchPara(); + + // *** 住民税計算 COMPUTE WRK-RESIDENT-TAX ROUNDED = 課税所得 × 0.10. + BigDecimal resident = BigDecimal.valueOf(wrkTaxableIncome) + .multiply(CST_RESIDENT_TAX_RATE) + .setScale(0, RoundingMode.HALF_UP); + wrkResidentTax = resident.longValue(); + + // *** 差引支給額 COMPUTE WRK-NET-PAYMENT = 総支給 - 所得税 - 保険料 - 住民税. + wrkNetPayment = wrkGrossPayment - wrkIncomeTax - wrkInsurance - wrkResidentTax; + + // *** W01 出力(2100WRITOUTSOR) + writeW01Para(); + + // *** 次レコード読込(1200R01INNSOR) + readR01Para(); + } + + /** + * 段落 2000MAJSOR 内:DB2 TAX-TABLE 税率取得(EXEC SQL SELECT ... BETWEEN)。 + * SQLCODE = 0 の場合のみ所得税を計算(負数は 0 に補正)、それ以外は 0. + */ + private void taxSelectPara() { + wrkIncomeTax = 0; // 既定(SQLCODE NOT = 0 / エラー時は 0) + try (PreparedStatement ps = conn.prepareStatement(TAX_SELECT_SQL)) { + // WHERE :WRK-TAXABLE-INCOME BETWEEN TAX-FROM AND TAX-TO + ps.setLong(1, wrkTaxableIncome); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { // IF SQLCODE = 0 + // INTO :DBV-TAX-RATE, :DBV-TAX-DEDUCTION + dbvTaxRate = rs.getBigDecimal("TAX-RATE"); // DECIMAL(5,4) + dbvTaxDeduction = rs.getLong("DEDUCTION"); // DECIMAL(9,0) + // COMPUTE WRK-INCOME-TAX ROUNDED = 課税所得 × 税率 - 控除額. + BigDecimal incomeTax = BigDecimal.valueOf(wrkTaxableIncome) + .multiply(dbvTaxRate) + .subtract(BigDecimal.valueOf(dbvTaxDeduction)) + .setScale(0, RoundingMode.HALF_UP); + long it = incomeTax.longValue(); + if (it < 0) { + it = 0; // IF WRK-INCOME-TAX < ZERO → MOVE ZERO. + } + wrkIncomeTax = it; + } + // ELSE(SQLCODE NOT = 0)→ WRK-INCOME-TAX は 0 のまま + } + } catch (SQLException e) { + // SQLCODE NOT = 0(実エラー含む)→ WRK-INCOME-TAX = 0(COBOL の ELSE 分岐と同義) + wrkIncomeTax = 0; + } + } + + /** + * 段落 2000MAJSOR 内:社会保険料計算(SEARCH IW-ENTRY)。 + * 条件:IW-INCOME-FROM(idx) ≦ 課税所得 AND 課税所得 ≦ IW-INCOME-TO(idx) + * AT END → WRK-INSURANCE = 0. + */ + private void insSearchPara() { + boolean found = false; + // SET IW-IDX TO 1 からの SEARCH(順次スキャン:COBOL SEARCH IW-ENTRY 相当) + for (int i = 0; i < iwCount; i++) { + if (iwIncomeFrom[i] <= wrkTaxableIncome && wrkTaxableIncome <= iwIncomeTo[i]) { + // COMPUTE WRK-INSURANCE ROUNDED = 課税所得 × (健康比率 + 厚生比率). + final BigDecimal sum = iwHealthRate[i].add(iwPensionRate[i]); // 双方 scale 6 + BigDecimal insurance = BigDecimal.valueOf(wrkTaxableIncome) + .multiply(sum) + .setScale(0, RoundingMode.HALF_UP); + wrkInsurance = insurance.longValue(); + found = true; + break; + } + } + if (!found) { + wrkInsurance = 0; // AT END: MOVE ZERO TO WRK-INSURANCE. + } + } + + /** + * 段落 2100WRITOUTSOR:SALARY-NET 出力処理。 + * MOVE 各項目 → W01OUTREC/WRITE W01OUTREC/ADD 1 TO CUN-W01OUT. + */ + private void writeW01Para() { + w01.reset(); + // MOVE R01EMP-ID TO W01EMP-ID. + w01.setEmpId(r01.getEmpId()); + // MOVE 'S' TO W01REC-TYPE. + w01.setRecType("S"); + // MOVE R01EMP-NAME TO W01EMP-NAME. + w01.setEmpName(r01.getEmpName()); + // MOVE R01DEPT-CODE TO W01DEPT-CODE. + w01.setDeptCode(r01.getDeptCode()); + // MOVE R01BASE-SALARY TO W01GROSS-PAYMENT.(設計書:設定元 R01BASE-SALARY) + w01.setGrossPayment(r01.getBaseSalary()); + // MOVE WRK-INCOME-TAX TO W01INCOME-TAX. + w01.setIncomeTax(wrkIncomeTax); + // MOVE WRK-INSURANCE TO W01INSURANCE. + w01.setInsurance(wrkInsurance); + // MOVE WRK-RESIDENT-TAX TO W01RESIDENT-TAX. + w01.setResidentTax(wrkResidentTax); + // MOVE WRK-NET-PAYMENT TO W01NET-PAYMENT. + w01.setNetPayment(wrkNetPayment); + // MOVE WRK-TAXABLE-INCOME TO W01TAXABLE-INCOME. + w01.setTaxableIncome(wrkTaxableIncome); + // MOVE WRK-NET-PAYMENT TO W01PAY-AMOUNT. + w01.setPayAmount(wrkNetPayment); + writeW01(); + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE → 入出力件数メッセージ×3 → 終了メッセージ. + */ + private void termPara() { + closeFiles(); + msgOutPara(CST_MSGIINKES, "KYU05R01", ConvUtil.padLeftZero(cunR01Inn, 9)); + msgOutPara(CST_MSGOUTKES, "KYU05W01", ConvUtil.padLeftZero(cunW01Out, 9)); + msgOutPara(CST_MSGOUTKES, "KYU05W02", ConvUtil.padLeftZero(cunW02Out, 9)); + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND 処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), StandardCharsets.UTF_8)); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), StandardCharsets.UTF_8)); + w02OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W02OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** WRITE W01OUTREC./ADD 1 TO CUN-W01OUT. */ + private void writeW01() { + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** WRITE W02OUTREC./ADD 1 TO CUN-W02OUT. */ + private void writeW02() { + w02OutFil.println(w02.toRecordLine()); + cunW02Out++; + } + + /** CLOSE R01INNFIL W01OUTFIL W02OUTFIL. */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w01OutFil); + closeQuiet(w02OutFil); + r01InnFil = null; + w01OutFil = null; + w02OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序も FILE STATUS 未管理) + } + } + + /** EXEC SQL ROLLBACK END-EXEC(異常時) */ + private static void rollbackQuiet(final Connection c) { + if (c != null) { + try { + c.rollback(); + System.out.println("[db] ROLLBACK DONE"); + } catch (SQLException e) { + System.out.println("[db] ROLLBACK FAILED: " + e.getMessage()); + } + } + } + + private static void closeQuiet(final Connection c) { + if (c != null) { + try { + c.close(); + } catch (SQLException e) { + System.out.println("[db] CLOSE FAILED: " + e.getMessage()); + } + } + } +} diff --git a/JavaSrc/src/Kyu06UpdMain.java b/JavaSrc/src/Kyu06UpdMain.java new file mode 100644 index 0000000..af89c30 --- /dev/null +++ b/JavaSrc/src/Kyu06UpdMain.java @@ -0,0 +1,481 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import dto.Kyu04Rec; +import dto.Kyu99Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU06UPD 給与計算結果DB2更新処理 + * ============================================================ + * PROGRAM-ID. KYU06UPD → 类 Kyu06UpdMain(PGMパターン:DB更新(UPSERT)) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R02INNFIL(KYU06R02 SALARY-NET FB200B) → R02INN_FILE(COPY KYU04REC → dto.Kyu04Rec 共通DTO) + * W03OUTFIL(KYU06W03 ERROR-LOG VB200B) → W03OUT_FILE(COPY KYU99REC → dto.Kyu99Rec 再利用) + * 使用DB2(EXEC SQL → JDBC): + * SALARY-RESULTS へ INSERT 試行 → SQLCODE=-803(重複) で UPDATE に切替(UPSERT) + * 接続は DbSample に準拠:DB2 URL + プレースホルダ資格 + setAutoCommit(false) + * 正常終了時に COMMIT、致命的SQL例外時に ROLLBACK(プロジェクトDB規約準拠) + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R02INNSOR→readR02Para() + * 2000MAJSOR→majPara() 2100ERROUTSOR→errorOutPara() 3000STPSOR→termPara() + * 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 処理要点(詳細設計書_KYU06UPD 準拠): + * - PARM(YEAR-MONTH) をコマンドラインから取得、未指定時は '202605'。 + * - R02 を全件読み、ホスト変数へ MOVE 後 INSERT。 + * SQLCODE=0 → CUN-DB-INS+1/-803 → UPDATE(SQLCODE=0 で CUN-DB-UPD+1)/ + * その他 → ERROR-LOG('DB-ERR' + EMP-ID) 出力。いずれも次件読込へ継続。 + * - COBOL 原本に COMMIT 文は無いが、プロジェクトDB規約(commit on success / + * rollback on SQLException)に従い、正常終了時に 3000STPSOR 内で COMMIT を実施。 + * + * ※COBOL の CONNECT TO 'data/SALARY.db'(SQLite) は、プロジェクトDB規約により + * DB2 URL(jdbc:db2://localhost:50000/TESTDB) のプレースホルダ接続に読替える。 + */ +public class Kyu06UpdMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU06UPD'. */ + private static final String CST_PRGIDX = "KYU06UPD"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. (本PGM未使用だが定数保持) */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数 */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数 */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-YEAR-MONTH-PARM PIC X(006). 取得PARM(初期値'202605'の格納先とは別に保持) */ + private static final String CST_YEAR_MONTH_DEF = "202605"; + + // ==================== DB 接続定数(DbSample 準拠) ==================== + + /** EXEC SQL CONNECT TO 'data/SALARY.db' の読替(DB2 URL プレースホルダ。テスト時は -Dkyu.db.url で上書き)。 */ + private static final String DB_URL = System.getProperty("kyu.db.url", "jdbc:db2://localhost:50000/TESTDB"); + private static final String DB_USER = "db2inst1"; + private static final String DB_PASS = "password"; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R02INNFIL ASSIGN TO EXTERNAL KYU06R02.(SALARY-NET入力 FB200B) */ + private static final String R02INN_FILE = "data/KYU06R02.txt"; + /** SELECT W03OUTFIL ASSIGN TO EXTERNAL KYU06W03.(ERROR-LOG出力 VB200B) */ + private static final String W03OUT_FILE = "data/KYU06W03.txt"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R02INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR02Inn; + /** 03 CUN-W03OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW03Out; + /** 03 CUN-DB-INS PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunDbIns; + /** 03 CUN-DB-UPD PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunDbUpd; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-EOF PIC X(001). 読込フラグ(88 WRK-EOF-Y VALUE '1') */ + private String wrkEof = "0"; + /** 03 WRK-YEAR-MONTH PIC X(006). コマンドラインPARM受領領域 */ + private String wrkYearMonth = ""; + /** 03 CNS-YEAR-MONTH-PARM PIC X(006). MOVE WRK-YEAR-MONTH → 本値を INSERT/UPDATE に使用 */ + private String cnsYearMonthParm = ""; + + /** 88 WRK-EOF-Y VALUE '1'. */ + private boolean isEof() { + return "1".equals(wrkEof); + } + + // ==================== DB2 ホスト変数領域 DBVARA ==================== + + /** 03 DBV-EMP-ID PIC X(008). */ + private String dbvEmpId = ""; + /** 03 DBV-YEAR-MONTH PIC X(006). */ + private String dbvYearMonth = ""; + /** 03 DBV-GROSS-PAYMENT PIC 9(009). */ + private long dbvGrossPayment; + /** 03 DBV-INCOME-TAX PIC 9(009). */ + private long dbvIncomeTax; + /** 03 DBV-INSURANCE PIC 9(009). */ + private long dbvInsurance; + /** 03 DBV-RESIDENT-TAX PIC 9(009). */ + private long dbvResidentTax; + /** 03 DBV-NET-PAYMENT PIC 9(009). */ + private long dbvNetPayment; + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 入出力レコード(FILE SECTION → DTO) ==================== + + /** 01 R02INNREC.(FD R02INNFIL / COPY KYU04REC REPLACING ==(A)== BY ==R02==) */ + private final Kyu04Rec r02 = new Kyu04Rec(); + /** 01 W03OUTREC.(FD W03OUTFIL / COPY KYU99REC 再利用) */ + private final Kyu99Rec w03 = new Kyu99Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private BufferedReader r02InnFil; + private PrintWriter w03OutFil; + /** EXEC SQL CONNECT で取得したコネクション */ + private Connection conn; + + /** コマンドライン引数(先頭要素=YEAR-MONTH)。 */ + private final String[] args; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + new Kyu06UpdMain(args).run(); + } + + public Kyu06UpdMain(final String[] args) { + this.args = (args != null) ? args : new String[0]; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!isEof()) { // PERFORM 2000MAJSOR UNTIL WRK-EOF-Y + majPara(); + } + termPara(); + } catch (SQLException e) { + // EXEC SQL WHENEVER SQLERROR 相当(接続等の致命例外) + System.out.println("[db] SQLERROR code=" + e.getErrorCode() + " msg=" + e.getMessage()); + rollbackQuiet(conn); + abendPara(); + } finally { + closeQuiet(conn); + closeFiles(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始MSG→コンパイル日時MSG→ワーク初期化→PARM取得→OPEN→DB接続→初回読込。 + */ + private void initPara() throws SQLException { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力(FUNCTION WHEN-COMPILED → 起動時刻で代用) + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** INITIALIZE WRKARA. + wrkEof = "0"; + wrkYearMonth = ""; + cnsYearMonthParm = ""; + + // *** PARM取得(ACCEPT WRK-YEAR-MONTH FROM COMMAND-LINE) + if (args.length > 0 && args[0] != null && !args[0].trim().isEmpty()) { + wrkYearMonth = ConvUtil.padRight(args[0].trim(), 6); + } + // *** IF WRK-YEAR-MONTH = SPACES MOVE '202605' TO WRK-YEAR-MONTH + if (wrkYearMonth.trim().isEmpty()) { + wrkYearMonth = CST_YEAR_MONTH_DEF; + } + // *** MOVE WRK-YEAR-MONTH TO CNS-YEAR-MONTH-PARM. + cnsYearMonthParm = wrkYearMonth; + + // *** OPEN INPUT R02INNFIL / OUTPUT W03OUTFIL. + openFiles(); + + // *** DB接続(EXEC SQL CONNECT TO 'data/SALARY.db' → DB2 URL 読替) + conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); + conn.setAutoCommit(false); // バッチ標準:明示的トランザクション制御 + + // *** 初回 READ(PERFORM 1100R02INNSOR) + readR02Para(); + } + + /** + * 段落 1100R02INNSOR:R02読込処理。 + * READ AT END / NOT AT END のマッピング。 + */ + private void readR02Para() { + try { + final String line = r02InnFil.readLine(); + if (line == null) { + // AT END: SET WRK-EOF-Y TO TRUE. + wrkEof = "1"; + } else { + // NOT AT END: R02INNREC 受信 → MOVE 各項目 / ADD 1 TO CUN-R02INN. + r02.fromRecordLine(line); + cunR02Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理(UPSERT)。 + * ホスト変数セット→INSERT→SQLCODE判定(-803→UPDATE/その他→ERR)→次件読込。 + */ + private void majPara() { + // *** MOVE R02... TO DBV...(ホスト変数セット) + dbvEmpId = r02.getEmpId(); + dbvYearMonth = cnsYearMonthParm; + dbvGrossPayment = r02.getGrossPayment(); + dbvIncomeTax = r02.getIncomeTax(); + dbvInsurance = r02.getInsurance(); + dbvResidentTax = r02.getResidentTax(); + dbvNetPayment = r02.getNetPayment(); + + // *** EXEC SQL INSERT INTO SALARY-RESULTS ... + final int insCode = insertRow(); + if (insCode == 0) { + // *** SQLCODE = 0 → ADD 1 TO CUN-DB-INS/次件読込 + cunDbIns++; + readR02Para(); + return; + } + + if (insCode == -803) { + // *** SQLCODE = -803(重複)→ EXEC SQL UPDATE SALARY-RESULTS ... + final int updCode = updateRow(); + if (updCode == 0) { + // *** UPDATE SQLCODE = 0 → ADD 1 TO CUN-DB-UPD + cunDbUpd++; + } else { + // *** UPDATE SQLCODE ≠ 0 → エラー出力 + errorOutPara(); + } + readR02Para(); + return; + } + + // *** SQLCODE = その他(DBエラー)→ エラー出力 + errorOutPara(); + readR02Para(); + } + + /** + * EXEC SQL INSERT ... VALUES (:DBV-..., CURRENT TIMESTAMP). + * 戻り値: 0=成功 / -803=重複 / その他=SQLエラーコード。 + */ + private int insertRow() { + final String sql = "INSERT INTO \"SALARY-RESULTS\" " + + "(\"EMP-ID\", \"YEAR-MONTH\", \"GROSS-PAYMENT\", \"INCOME-TAX\", " + + " \"INSURANCE\", \"RESIDENT-TAX\", \"NET-PAYMENT\", \"UPDATED-AT\") " + + "VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, dbvEmpId); // :DBV-EMP-ID + ps.setString(2, dbvYearMonth); // :DBV-YEAR-MONTH + ps.setLong(3, dbvGrossPayment); // :DBV-GROSS-PAYMENT + ps.setLong(4, dbvIncomeTax); // :DBV-INCOME-TAX + ps.setLong(5, dbvInsurance); // :DBV-INSURANCE + ps.setLong(6, dbvResidentTax); // :DBV-RESIDENT-TAX + ps.setLong(7, dbvNetPayment); // :DBV-NET-PAYMENT + ps.executeUpdate(); + return 0; // SQLCODE = 0 + } catch (SQLException e) { + // -803 は重複キー(UPSERT の UPDATE へ切替)。SQLite は UNIQUE constraint / SQLState 23000。 + if (isDuplicateKey(e)) { + return -803; + } + return e.getErrorCode(); // その他 SQL エラー + } + } + + /** + * EXEC SQL UPDATE SALARY-RESULTS SET ... WHERE EMP-ID AND YEAR-MONTH. + * 戻り値: 0=成功 / その他=SQLエラーコード。 + */ + private int updateRow() { + final String sql = "UPDATE \"SALARY-RESULTS\" SET " + + " \"GROSS-PAYMENT\" = ?, \"INCOME-TAX\" = ?, \"INSURANCE\" = ?, " + + " \"RESIDENT-TAX\" = ?, \"NET-PAYMENT\" = ?, \"UPDATED-AT\" = CURRENT_TIMESTAMP " + + "WHERE \"EMP-ID\" = ? AND \"YEAR-MONTH\" = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setLong(1, dbvGrossPayment); // :DBV-GROSS-PAYMENT + ps.setLong(2, dbvIncomeTax); // :DBV-INCOME-TAX + ps.setLong(3, dbvInsurance); // :DBV-INSURANCE + ps.setLong(4, dbvResidentTax); // :DBV-RESIDENT-TAX + ps.setLong(5, dbvNetPayment); // :DBV-NET-PAYMENT + ps.setString(6, dbvEmpId); // :DBV-EMP-ID + ps.setString(7, dbvYearMonth); // :DBV-YEAR-MONTH + ps.executeUpdate(); + return 0; // SQLCODE = 0 + } catch (SQLException e) { + return e.getErrorCode(); + } + } + + /** + * 段落 2100ERROUTSOR:エラー出力処理。 + * MOVE 'DB-ERR' TO W03ERR-CATEGORY./MOVE DBV-EMP-ID TO W03ERR-DETAIL./WRITE W03OUTREC. + */ + private void errorOutPara() { + w03.reset(); + w03.setErrCategory("DB-ERR"); + w03.setErrDetail(dbvEmpId); + writeW03(); + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→件数メッセージ×4→COMMIT→終了メッセージ。 + */ + private void termPara() { + closeFiles(); + + msgOutPara(CST_MSGIINKES, "KYU06R02", ConvUtil.padLeftZero(cunR02Inn, 9)); + msgOutPara(CST_MSGOUTKES, "DB-INSERT", ConvUtil.padLeftZero(cunDbIns, 9)); + msgOutPara(CST_MSGOUTKES, "DB-UPDATE", ConvUtil.padLeftZero(cunDbUpd, 9)); + msgOutPara(CST_MSGOUTKES, "KYU06W03", ConvUtil.padLeftZero(cunW03Out, 9)); + + // *** EXEC SQL COMMIT END-EXEC(プロジェクトDB規約:成功時コミット) + if (conn != null) { + try { + conn.commit(); + } catch (SQLException e) { + System.out.println("[db] COMMIT FAILED code=" + e.getErrorCode() + " " + e.getMessage()); + rollbackQuiet(conn); + abendPara(); + } + } + + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN INPUT R02INNFIL / OUTPUT W03OUTFIL. */ + private void openFiles() { + try { + r02InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(R02INN_FILE), StandardCharsets.UTF_8)); + w03OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W03OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** WRITE W03OUTREC./ADD 1 TO CUN-W03OUT. */ + private void writeW03() { + w03OutFil.println(w03.toRecordLine()); + cunW03Out++; + } + + /** CLOSE R02INNFIL W03OUTFIL. */ + private void closeFiles() { + closeQuiet(r02InnFil); + closeQuiet(w03OutFil); + r02InnFil = null; + w03OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } + + /** EXEC SQL ROLLBACK END-EXEC(異常時) */ + private static void rollbackQuiet(final Connection c) { + if (c != null) { + try { + c.rollback(); + } catch (SQLException e) { + // ignore + } + } + } + + private static void closeQuiet(final Connection c) { + if (c != null) { + try { + c.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** DB2(-803) と SQLite(UNIQUE constraint / SQLState 23000) の両方で重複キーを判定。 */ + private static boolean isDuplicateKey(final SQLException e) { + if (e.getErrorCode() == -803) { + return true; + } + if ("23000".equals(e.getSQLState())) { + return true; + } + final String m = e.getMessage(); + return m != null && m.contains("UNIQUE constraint failed"); + } +} diff --git a/JavaSrc/src/Kyu07DivMain.java b/JavaSrc/src/Kyu07DivMain.java new file mode 100644 index 0000000..d770945 --- /dev/null +++ b/JavaSrc/src/Kyu07DivMain.java @@ -0,0 +1,578 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +import dto.Kyu07R01Rec; +import dto.Kyu99Rec; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU07DIV 部門別分割出力処理 + * ============================================================ + * PROGRAM-ID. KYU07DIV → 类 Kyu07DivMain(PGMパターン:50分割) + * + * 使用文件(SELECT ASSIGN → 路径常量): + * R01INNFIL(KYU07R01 SALARY-NET FB200B入力) → R01INN_FILE(COPY KYU04REC → dto.Kyu07R01Rec) + * SYSIN-FILE(KYU07SYS 制御カード F80B入力) → SYSIN_FILE(自前 PIC X(80)) + * W99OUTFIL(KYU07W99 ERROR-LOG VB200B出力) → W99OUT_FILE(COPY KYU99REC → dto.Kyu99Rec) + * DEPT01〜50(KYU07D01〜D50 各200B FB出力) → getDeptPath()(自前 PIC X(200)) + * 使用SUB(CALL → 类#execute): + * SUB02MSG→Sub02MsgSub(4000MSGOUTSOR) / SUB03END→Sub03EndSub(9999ABDSOR) + * 段落对应: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100SYSINSOR→sysInPara() + * 1200R01INNSOR→readR01Para() 2000MAJSOR→majPara() 2100OPENSOR→openDeptPara() + * 2200CLOSESOR→closeDeptPara() 2300WRITESOR→writeDeptPara() 3000STPSOR→termPara() + * 4000MSGOUTSOR→msgOutPara() 9999ABDSOR→abendPara() + * + * 处理要点(詳細設計書_KYU07DIV 準拠): + * - JCL SORT済 SALARY-NET を部署コード(50-51桁)で50ファイルに分割出力。 + * - 常に1ファイルのみOPEN:部署切替時に前部署CLOSE→新部署OPEN。 + * - SYSIN制御カード "DEPT=01,02,05,10" で出力対象部署を絞り込み(省略時は全50部署)。 + * - 無効部署(01〜50 範囲外)は ERROR-LOG(ERR-CATEGORY='INVDEPT', ERR-DETAIL='INVALID DEPT')を出力。 + * ※設計書は W99ERR-CATEGORY='INVDEPT' を固定値指定(原COBOLは DETAIL のみ設定)→ 設計書優先。 + */ +public class Kyu07DivMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU07DIV'. */ + private static final String CST_PRGIDX = "KYU07DIV"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. (設計上使用なし) */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数メッセージ */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数メッセージ */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時 */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KYU07R01.(SALARY-NET入力) */ + private static final String R01INN_FILE = "data/KYU07R01.txt"; + /** SELECT SYSIN-FILE ASSIGN TO KYU07SYS.(制御カード入力) */ + private static final String SYSIN_FILE = "data/KYU07SYS.txt"; + /** SELECT W99OUTFIL ASSIGN TO KYU07W99.(ERROR-LOG出力) */ + private static final String W99OUT_FILE = "data/KYU07W99.txt"; + + /** 部署コード 01〜50 の定数配列(DEPT01〜50 / 0は未使用)。 */ + private static final String[] DEPT_CODE = new String[51]; + static { + for (int k = 1; k <= 50; k++) { + DEPT_CODE[k] = String.format("%02d", k); + } + } + + /** SELECT DEPTnn-FILE ASSIGN TO KYU07Dnn.(部署別出力、nn=01〜50) */ + private static String getDeptPath(final String deptCode) { + return "data/KYU07D" + deptCode + ".txt"; + } + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-W99OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW99Out; + /** 03 CUN-DEPT-OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunDeptOut; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-EOF PIC X(001). 読込フラグ(88 WRK-EOF-Y VALUE '1') */ + private String wrkEof = "0"; + /** 03 WRK-DEPT-CODE PIC X(002). */ + private String wrkDeptCode = ""; + /** 03 WRK-PREV-DEPT PIC X(002). */ + private String wrkPrevDept = ""; + /** 03 WRK-FIRST PIC X(001) VALUE '1'.(88 WRK-FIRST-Y '1' / WRK-FIRST-N '0') */ + private String wrkFirst = "1"; + + /** 03 WRK-TARGET-DEPT / 05 WRK-TARGET OCCURS 50 TIMES PIC X(002). */ + private final String[] wrkTarget = new String[51]; + /** 03 WRK-TARGET-COUNT PIC 9(002). */ + private int wrkTargetCount; + /** 03 WRK-HAS-TARGET PIC X(001).(88 WRK-ALL-DEPTS 'A' / WRK-FILTER-DEPTS 'F') */ + private String wrkHasTarget = ""; + /** 03 WRK-SYSIN-LINE PIC X(080). */ + private String wrkSysinLine = ""; + /** 03 WRK-I PIC 9(002).(パース用索引) */ + private int wrkI; + /** 03 WRK-J PIC 9(002).(パース用索引) */ + private int wrkJ; + /** 03 WRK-TOKEN PIC X(080). */ + private String wrkToken = ""; + + /** 03 WS-PAD-CHAR PIC X(001) VALUE SPACE. */ + private String wsPadChar = " "; + /** 03 WS-FILE-STATUS PIC X(002). */ + private String wsFileStatus = ""; + + /** 88 WRK-EOF-Y VALUE '1'. */ + private boolean isEof() { + return "1".equals(wrkEof); + } + /** 88 WRK-FIRST-Y VALUE '1'. */ + private boolean isFirstY() { + return "1".equals(wrkFirst); + } + /** 88 WRK-ALL-DEPTS VALUE 'A'. */ + private boolean isAllDepts() { + return "A".equals(wrkHasTarget); + } + /** 88 WRK-FILTER-DEPTS VALUE 'F'. */ + private boolean isFilterDepts() { + return "F".equals(wrkHasTarget); + } + /** SET WRK-ALL-DEPTS TO TRUE. */ + private void setAllDepts() { + wrkHasTarget = "A"; + } + /** SET WRK-FILTER-DEPTS TO TRUE. */ + private void setFilterDepts() { + wrkHasTarget = "F"; + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 入出力レコード(FILE SECTION → DTO) ==================== + + /** 01 R01INNREC.(FD R01INNFIL / COPY KYU04REC → dto.Kyu07R01Rec) */ + private final Kyu07R01Rec r01 = new Kyu07R01Rec(); + /** 01 W99OUTREC.(FD W99OUTFIL / COPY KYU99REC) */ + private final Kyu99Rec w99 = new Kyu99Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private BufferedReader r01InnFil; + private PrintWriter w99OutFil; + private PrintWriter deptOutFil; + /** 現在OPEN中の部署コード(1ファイルのみOPENを管理)。 */ + private String openDeptCode = ""; + + /** 入力ファイルパス。デフォルトは SELECT ASSIGN 対応の R01INN_FILE。 */ + private final String r01InnFile; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + final String inputFile = (args != null && args.length > 0) ? args[0] : R01INN_FILE; + new Kyu07DivMain(inputFile).run(); + } + + public Kyu07DivMain() { + this(R01INN_FILE); + } + + public Kyu07DivMain(final String r01InnFile) { + this.r01InnFile = r01InnFile; + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL EOF → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!isEof()) { // PERFORM 2000MAJSOR UNTIL WRK-EOF = '1' + majPara(); + } + termPara(); + } finally { + closeFiles(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→SYSIN読込→OPEN→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※FUNCTION WHEN-COMPILED→起動時刻で代用 + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + resetWrk(); + // *** MOVE '1' TO WRK-FIRST. + wrkFirst = "1"; + + // *** SYSIN読込(対象部署指定) + sysInPara(); + + // *** OPEN INPUT R01INNFIL / OPEN OUTPUT W99OUTFIL. + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(r01InnFile), StandardCharsets.UTF_8)); + w99OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W99OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + + // *** 初回READ + readR01Para(); + } + + /** + * 段落 1100SYSINSOR:SYSIN読込処理。 + * OPEN→READ AT END(全部署対象) / カードありは "DEPT=01,02,05,10" 形式をパース。 + */ + private void sysInPara() { + try (BufferedReader sysIn = new BufferedReader( + new InputStreamReader(new FileInputStream(SYSIN_FILE), StandardCharsets.UTF_8))) { + final String line = sysIn.readLine(); + if (line == null) { + // *** AT END: SET WRK-ALL-DEPTS TO TRUE(制御カードなし→全部署対象) + setAllDepts(); + return; + } + + // *** 制御カードあり:READ INTO WRK-SYSIN-LINE(PIC X(80)) + wrkSysinLine = ConvUtil.padRight(line, 80); + // *** MOVE WRK-SYSIN-LINE TO WRK-TOKEN. + wrkToken = wrkSysinLine; + // *** MOVE ZERO TO WRK-TARGET-COUNT. + wrkTargetCount = 0; + // *** MOVE 1 TO WRK-J. + wrkJ = 1; + + // *** 先頭の "DEPT=" をスキップ('=' まで読飛ばし) + // PERFORM VARYING WRK-I FROM 1 BY 1 + // UNTIL WRK-I > 80 OR SYSIN(WRK-I:1) = SPACE OR '=' + int i = 1; + while (i <= 80) { + final char c = charAt1(wrkSysinLine, i); + if (c == ' ' || c == '=') { + break; + } + i++; + } + // *** ADD 1 TO WRK-I.('=' の次桁へ) + i++; + + // *** トークン解析ループ + // PERFORM VARYING WRK-J FROM 1 BY 1 + // UNTIL WRK-I > 80 OR SYSIN(WRK-I:1) = SPACE + while (i <= 80) { + final char c = charAt1(wrkSysinLine, i); + if (c == ' ') { + break; + } + if (c == ',') { + // *** IF SYSIN(WRK-I:1) = ',' → ADD 1 TO WRK-I(区切り読飛ばし) + i++; + // CONTINUE(直後の MOVE で次トークンを取り込む) + } + if (wrkTargetCount < 50) { + // *** ADD 1 TO WRK-TARGET-COUNT / MOVE SYSIN(WRK-I:2) TO WRK-TARGET-CODE(n) + wrkTargetCount++; + wrkTarget[wrkTargetCount] = read2(wrkSysinLine, i); + } + // *** ADD 2 TO WRK-I. + i += 2; + } + + if (wrkTargetCount > 0) { + // *** SET WRK-FILTER-DEPTS TO TRUE + setFilterDepts(); + } else { + // *** SET WRK-ALL-DEPTS TO TRUE + setAllDepts(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * 段落 1200R01INNSOR:R01読込処理。 + * READ AT END / NOT AT END のマッピング。 + */ + private void readR01Para() { + try { + final String line = r01InnFil.readLine(); + if (line == null) { + // AT END: MOVE '1' TO WRK-EOF. + wrkEof = "1"; + } else { + // NOT AT END: レコード取込み + ADD 1 TO CUN-R01INN. + r01.setRecordLine(line); + cunR01Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * 部署コード取得→SYSINフィルタ→部署切替判定→WRITE。 + */ + private void majPara() { + // *** MOVE R01DEPT-CODE TO WRK-DEPT-CODE. + wrkDeptCode = r01.getDeptCode(); + + // *** SYSIN対象部署フィルタ(WRK-FILTER-DEPTS 時のみ SEARCH) + if (isFilterDepts()) { + boolean found = false; + for (int t = 1; t <= wrkTargetCount; t++) { + if (wrkTarget[t].equals(wrkDeptCode)) { + found = true; + break; + } + } + if (!found) { + // *** AT END: PERFORM 1200R01INNSOR / EXIT SECTION(対象外は読飛ばし) + readR01Para(); + return; + } + } + + // *** 部署切替判定 + if (isFirstY()) { + // *** 初回:WRK-FIRST='0' → 2100OPENSOR + wrkFirst = "0"; + openDeptPara(wrkDeptCode); + } else { + if (!wrkPrevDept.equals(wrkDeptCode)) { + // *** 部署切替:2200CLOSESOR → 2100OPENSOR + closeDeptPara(wrkPrevDept); + openDeptPara(wrkDeptCode); + } + } + + // *** MOVE WRK-DEPT-CODE TO WRK-PREV-DEPT. + wrkPrevDept = wrkDeptCode; + + // *** 該当ファイルにWRITE(2300WRITESOR) + writeDeptPara(wrkDeptCode); + + // *** PERFORM 1200R01INNSOR. + readR01Para(); + } + + /** + * 段落 2100OPENSOR:部署ファイルOPEN処理。 + * EVALUATE WRK-DEPT-CODE → '01'〜'50' は OPEN OUTPUT、それ以外は ERROR-LOG出力。 + * ※常に1ファイルのみOPEN(呼出元で前部署は CLOSE 済)。 + */ + private void openDeptPara(final String deptCode) { + final int k = deptIndex(deptCode); + if (k < 0) { + // *** WHEN OTHER: MOVE 'INVALID DEPT' TO W99ERR-DETAIL / WRITE / ADD 1 CUN-W99OUT + // ※設計書準拠:ERR-CATEGORY は固定値 'INVDEPT' を設定 + w99.reset(); + w99.setErrCategory("INVDEPT"); + w99.setErrDetail("INVALID DEPT"); + writeW99(); + return; + } + // *** WHEN '01' 〜 '50': OPEN OUTPUT DEPTnn-FILE + try { + deptOutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(getDeptPath(deptCode)), StandardCharsets.UTF_8)); + openDeptCode = deptCode; + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** + * 段落 2200CLOSESOR:部署ファイルCLOSE処理。 + * EVALUATE WRK-PREV-DEPT → '01'〜'50' は CLOSE WITH LOCK、それ以外は CONTINUE。 + */ + private void closeDeptPara(final String prevDept) { + final int k = deptIndex(prevDept); + if (k < 0) { + // *** WHEN OTHER: CONTINUE(対象外部署は OPEN されていない) + return; + } + // *** WHEN '01' 〜 '50': CLOSE DEPTnn-FILE WITH LOCK + try { + if (deptOutFil != null) { + deptOutFil.close(); + } + } catch (Exception e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + deptOutFil = null; + openDeptCode = ""; + } + + /** + * 段落 2300WRITESOR:部署WRITE処理。 + * EVALUATE WRK-DEPT-CODE → '01'〜'50' は WRITE、それ以外は CONTINUE。 + * その後 ADD 1 TO CUN-DEPT-OUT(EVALUATE 外・常時カウント)。 + */ + private void writeDeptPara(final String deptCode) { + final int k = deptIndex(deptCode); + if (k >= 0) { + // *** WHEN '01' 〜 '50': WRITE DEPTnn-REC FROM R01INNREC + if (deptOutFil != null) { + deptOutFil.println(r01.toRecordLine()); + } + } + // *** ADD 1 TO CUN-DEPT-OUT(WHEN OTHER 含む全レコードをカウント) + cunDeptOut++; + } + + /** + * 段落 3000STPSOR:終了処理。 + * 最終部署CLOSE→CLOSE R01/W99→件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + // *** IF WRK-PREV-DEPT > SPACES → PERFORM 2200CLOSESOR + if (wrkPrevDept != null && !wrkPrevDept.isEmpty()) { + closeDeptPara(wrkPrevDept); + } + + // *** CLOSE R01INNFIL W99OUTFIL.(finally の closeFiles で実施) + + // *** 入力件数メッセージ + msgOutPara(CST_MSGIINKES, "KYU07R01", ConvUtil.padLeftZero(cunR01Inn, 9)); + // *** 出力件数メッセージ(DEPT-OUT) + msgOutPara(CST_MSGOUTKES, "DEPT-OUT", ConvUtil.padLeftZero(cunDeptOut, 9)); + // *** 出力件数メッセージ(KYU07W99) + msgOutPara(CST_MSGOUTKES, "KYU07W99", ConvUtil.padLeftZero(cunW99Out, 9)); + // *** 終了メッセージ + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + // *** MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1) / -04(1:1).(先頭1桁='2') + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + // *** MOVE CNS-PRGIDX TO M00UMKDATS22-05. + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** INITIALIZE WRKARA 相当:数値→0、文字→SPACE。 */ + private void resetWrk() { + wrkEof = "0"; + wrkDeptCode = ""; + wrkPrevDept = ""; + wrkFirst = "0"; + for (int t = 1; t <= 50; t++) { + wrkTarget[t] = ""; + } + wrkTargetCount = 0; + wrkHasTarget = ""; + wrkSysinLine = ""; + wrkI = 0; + wrkJ = 0; + wrkToken = ""; + wsPadChar = " "; + wsFileStatus = ""; + } + + /** WRITE W99OUTREC./ADD 1 TO CUN-W99OUT. */ + private void writeW99() { + w99OutFil.println(w99.toRecordLine()); + cunW99Out++; + } + + /** CLOSE R01INNFIL W99OUTFIL(+最終部署ファイルが残れば close)。 */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w99OutFil); + closeQuiet(deptOutFil); + r01InnFil = null; + w99OutFil = null; + deptOutFil = null; + openDeptCode = ""; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } + + /** 部署コード→DEPT_CODE 索引(1〜50)を返す。一致しなければ -1。 */ + private static int deptIndex(final String deptCode) { + if (deptCode == null) { + return -1; + } + for (int k = 1; k <= 50; k++) { + if (DEPT_CODE[k].equals(deptCode)) { + return k; + } + } + return -1; + } + + /** 1-indexed 位置の文字を取得(範囲外は SPACE)。 */ + private static char charAt1(final String s, final int idx1) { + final int p = idx1 - 1; + if (p >= 0 && p < s.length()) { + return s.charAt(p); + } + return ' '; + } + + /** 1-indexed 位置から2文字を取得(範囲外は SPACE で補完)。 */ + private static String read2(final String s, final int idx1) { + final char a = charAt1(s, idx1); + final char b = charAt1(s, idx1 + 1); + return "" + a + b; + } +} diff --git a/JavaSrc/src/Kyu08PriMain.java b/JavaSrc/src/Kyu08PriMain.java new file mode 100644 index 0000000..5bc8dc1 --- /dev/null +++ b/JavaSrc/src/Kyu08PriMain.java @@ -0,0 +1,413 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +import dto.Kyu04Rec; +import dto.Kyu05Rec; +import dto.Kyu99Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU08PRI 給与明細印刷処理 + * ============================================================ + * PROGRAM-ID. KYU08PRI → 类 Kyu08PriMain(PGMパターン:レイアウト編集のみ GETPUT) + * + * 使用ファイル(SELECT ASSIGN → パス定数): + * R01INNFIL(KYU08R01 SALARY-NET F200B 入力) → R01INN_FILE(COPY KYU04REC → dto.Kyu04Rec) + * W01OUTFIL(KYU08W01 PAYSLIP F200B 出力) → W01OUT_FILE(COPY KYU05REC → dto.Kyu05Rec) + * W02OUTFIL(KYU08W02 ERROR-LOG VB200B 出力)→ W02OUT_FILE(COPY KYU99REC → dto.Kyu99Rec、将来拡張・現状出力なし) + * + * 使用SUB(CALL → 类#execute): + * SUB01DAT→Sub01DatSub / SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * + * 段落対応: + * 0000MAJCOLSOR→run() 1000ITTSOR→initPara() 1100R01INNSOR→readR01Para() + * 2000MAJSOR→majPara() 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() + * 9999ABDSOR→abendPara() + * + * 処理要点(詳細設計書_KYU08PRI 準拠): + * - 1社員1ページ。ヘッダ行→明細行→フッタ行を 200B 固定長で順次出力(LINAGE 66行/ページ相当)。 + * - ヘッダ行:HL-TITLE('========== 給与明細書 ==========') + ページ番号(Z(03)9) + 'PG'。 + * - 明細行:DETAIL-LINE は KYU05REC 布局と一致(PAGE-TITLE=SPACE)するため Kyu05Rec で編集。 + * EMP-NAME は X(020) JUSTIFIED RIGHT、BASIC/NET は Z(008)9 BLANK WHEN ZERO で編集。 + * - フッタ行:FL-MSG('--- 明細終了 ---')。 + * - COBOL は LINAGE/ADVANCING PAGE を用いるが、Java 平文テキスト出力では 200B 行の連続書出とし + * ページ制御(フォームフィード等)は行わない(設計上 1社員1ページのため実害なし)。 + * - W02(ERROR-LOG) は OPEN OUTPUT のみ(現状 WRITE なし/将来拡張)。 + */ +public class Kyu08PriMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU08PRI'. */ + private static final String CST_PRGIDX = "KYU08PRI"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. サブエラーメッセージ */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数メッセージ */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数メッセージ */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時メッセージ */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-HEADER-LINES PIC 9(002) VALUE 6. (LINAGE TOP 相当・参照用) */ + private static final int CST_HEADER_LINES = 6; + /** 03 CNS-DETAIL-LINES PIC 9(002) VALUE 54. (LINAGE 明細相当・参照用) */ + private static final int CST_DETAIL_LINES = 54; + /** 03 CNS-FOOTER-LINES PIC 9(002) VALUE 6. (LINAGE BOTTOM 相当・参照用) */ + private static final int CST_FOOTER_LINES = 6; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT R01INNFIL ASSIGN TO KYU08R01.(SALARY-NET 入力 F200B) */ + private static final String R01INN_FILE = "data/KYU08R01.txt"; + /** SELECT W01OUTFIL ASSIGN TO KYU08W01.(PAYSLIP 出力 F200B) */ + private static final String W01OUT_FILE = "data/KYU08W01.txt"; + /** SELECT W02OUTFIL ASSIGN TO KYU08W02.(ERROR-LOG 出力 VB200B・現状出力なし) */ + private static final String W02OUT_FILE = "data/KYU08W02.txt"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-R01INN PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunR01Inn; + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + /** 03 CUN-W02OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW02Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-EOF PIC X(001). 読込フラグ(88 WRK-EOF-Y VALUE '1') */ + private String wrkEof = "0"; + /** 03 WRK-LINE-NO PIC 9(002). */ + private int wrkLineNo; + /** 03 WRK-PAGE-COUNTER PIC 9(004) VALUE 0. */ + private int wrkPageCounter = 0; + /** 03 WS-PAD-CHAR PIC X(001) VALUE SPACE. */ + private String wsPadChar = " "; + /** 03 WS-FILE-STATUS PIC X(002). */ + private String wsFileStatus = ""; + /** 03 WS-DISP-PG PIC Z(03)9. (HL-PAGE-NO 編集用・参照用) */ + private int wsDispPg; + + /** 88 WRK-EOF-Y VALUE '1'. */ + private boolean isEof() { + return "1".equals(wrkEof); + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + /** COPY ZANDATAC.(SUB01DAT用) */ + private final ZandatacPar d01UbsPar = new ZandatacPar(); + + // ==================== 入出力レコード(FILE SECTION → DTO) ==================== + + /** 01 R01INNREC.(FD R01INNFIL / COPY KYU04REC) */ + private final Kyu04Rec r01 = new Kyu04Rec(); + /** 01 W01OUTREC.(FD W01OUTFIL / COPY KYU05REC) */ + private final Kyu05Rec w01 = new Kyu05Rec(); + /** 01 W02OUTREC.(FD W02OUTFIL / COPY KYU99REC) */ + private final Kyu99Rec w02 = new Kyu99Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private BufferedReader r01InnFil; + private PrintWriter w01OutFil; + private PrintWriter w02OutFil; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + new Kyu08PriMain().run(); + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → PERFORM 2000MAJSOR UNTIL WRK-EOF-Y → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); + while (!isEof()) { // PERFORM 2000MAJSOR UNTIL WRK-EOF = '1' + majPara(); + } + termPara(); + } finally { + closeFiles(); + } + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→SUB01DAT→OPEN→初回読込。 + */ + private void initPara() { + // *** 開始メッセージ出力 + // MOVE CNS-MSGSTR TO M00MSGCOD./PERFORM 4000MSGOUTSOR. + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※FUNCTION WHEN-COMPILED→起動時刻で代用 + // MOVE CNS-MSGKEYINF TO M00MSGCOD. + // MOVE FUNCTION WHEN-COMPILED TO M00UMKDATS22-01. + // MOVE 'COMPILED' TO M00UMKDATS22-02. + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + wrkEof = "0"; + wrkLineNo = 0; + wrkPageCounter = 0; + wsPadChar = " "; + wsFileStatus = ""; + wsDispPg = 0; + + // *** 運用日付取得 SUB01DAT + // INITIALIZE D01UBSPAR./CALL 'SUB01DAT' USING D01UBSPAR. + d01UbsPar.reset(); + Sub01DatSub.execute(d01UbsPar); + if (d01UbsPar.getD01FkiCod() != 0) { + // IF D01FKICOD NOT = ZERO + // MOVE CNS-MSGSUBEEK TO M00MSGCOD + // MOVE 'SUB01DAT' TO M00UMKDATS22-01 + // MOVE D01FKICOD TO M00UMKDATS22-02 + // PERFORM 4000MSGOUTSOR + // PERFORM 9999ABDSOR + msgOutPara(CST_MSGSUBEEK, "SUB01DAT", + ConvUtil.padLeftZero(d01UbsPar.getD01FkiCod(), 4)); + abendPara(); + return; + } + + // *** 使用ファイルのオープン + // OPEN INPUT R01INNFIL./OPEN OUTPUT W01OUTFIL W02OUTFIL. + openFiles(); + + // *** R01 初回読込(1100R01INNSOR) + readR01Para(); + } + + /** + * 段落 1100R01INNSOR:R01読込処理。 + * READ R01INNFIL AT END SET WRK-EOF-Y / NOT AT END ADD 1 TO CUN-R01INN. + */ + private void readR01Para() { + try { + final String line = r01InnFil.readLine(); + if (line == null) { + // AT END: SET WRK-EOF-Y TO TRUE. + wrkEof = "1"; + } else { + // NOT AT END: 固定長解析 → R01INNREC、ADD 1 TO CUN-R01INN. + r01.parseRecord(line); + cunR01Inn++; + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ異常 ≈ ABEND + } + } + + /** + * 段落 2000MAJSOR:主処理。 + * ヘッダ出力(改ページ+ページ番号)→明細行編集・出力→フッタ出力(改ページ)→次レコード読込。 + */ + private void majPara() { + // *** ページカウンタ加算 + // ADD 1 TO WRK-PAGE-COUNTER. + wrkPageCounter++; + // MOVE WRK-PAGE-COUNTER TO HL-PAGE-NO./MOVE WRK-PAGE-COUNTER TO WS-DISP-PG. + wsDispPg = wrkPageCounter; + + // *** ヘッダ出力(BEFORE ADVANCING PAGE) + // WRITE W01OUTREC FROM HEADER-LINE BEFORE ADVANCING PAGE. + w01OutFil.println(buildHeaderLine(wrkPageCounter)); + + // *** 明細行編集(DETAIL-LINE ≡ KYU05REC 布局:PAGE-TITLE=SPACE) + // MOVE R01EMP-ID TO DL-EMP-ID. + // MOVE R01EMP-NAME TO DL-EMP-NAME. + // MOVE R01GROSS-PAYMENT TO DL-BASIC-SALARY. + // MOVE R01NET-PAYMENT TO DL-NET-PAYMENT. + w01.reset(); + w01.setW01EmpId(r01.getR01EmpId()); + w01.setW01EmpName(r01.getR01EmpName()); + w01.setW01BasicSalary((int) r01.getR01GrossPayment()); + w01.setW01NetPayment((int) r01.getR01NetPayment()); + w01.setW01PageTitle(""); // 明細行の PAGE-TITLE は SPACE + + // WRITE W01OUTREC FROM DETAIL-LINE BEFORE ADVANCING 1 LINE + // AT END-OF-PAGE WRITE W01OUTREC FROM FOOTER-LINE BEFORE ADVANCING PAGE. + // (1社員1ページのため AT END-OF-PAGE は発動しない) + w01OutFil.println(w01.toRecordLine()); + // ADD 1 TO CUN-W01OUT. + cunW01Out++; + + // *** フッタ出力(BEFORE ADVANCING PAGE) + // WRITE W01OUTREC FROM FOOTER-LINE BEFORE ADVANCING PAGE. + w01OutFil.println(buildFooterLine()); + + // *** 次レコード読込(1100R01INNSOR) + readR01Para(); + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE→入出力件数メッセージ×3→終了メッセージ。 + */ + private void termPara() { + // *** 入出力ファイルのクローズ + // CLOSE R01INNFIL W01OUTFIL W02OUTFIL. + closeFiles(); + + // *** 入出力件数メッセージ出力 + // MOVE CNS-MSGIINKES TO M00MSGCOD. MOVE 'KYU08R01' TO PARM1. MOVE CUN-R01INN TO PARM2. + msgOutPara(CST_MSGIINKES, "KYU08R01", ConvUtil.padLeftZero(cunR01Inn, 9)); + // MOVE CNS-MSGOUTKES TO M00MSGCOD. MOVE 'KYU08W01' TO PARM1. MOVE CUN-W01OUT TO PARM2. + msgOutPara(CST_MSGOUTKES, "KYU08W01", ConvUtil.padLeftZero(cunW01Out, 9)); + // MOVE CNS-MSGOUTKES TO M00MSGCOD. MOVE 'KYU08W02' TO PARM1. MOVE CUN-W02OUT TO PARM2. + msgOutPara(CST_MSGOUTKES, "KYU08W02", ConvUtil.padLeftZero(cunW02Out, 9)); + + // *** 終了メッセージ出力 + // MOVE CNS-MSGFIN TO M00MSGCOD./PERFORM 4000MSGOUTSOR. + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + // MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1). → 先頭1桁 '2' + 後続SPACE(30) + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + // MOVE CNS-KN0002 TO M00UMKDATS22-04(1:1). + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + // MOVE CNS-PRGIDX TO M00UMKDATS22-05. + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + // ==================== 印刷行編集補助(HEADER-LINE / FOOTER-LINE) ==================== + + /** + * 01 HEADER-LINE 編集(200B)。 + * 05 HL-TITLE PIC X(040) VALUE '========== 給与明細書 ==========' + * 05 FILLER PIC X(010) VALUE SPACES + * 05 HL-PAGE-NO PIC Z(03)9 + * 05 FILLER PIC X(002) VALUE 'PG' + * 05 FILLER PIC X(144) VALUE SPACES + */ + private static String buildHeaderLine(final int pageNo) { + // PIC Z(03)9 編集(4桁、先頭3桁ゼロ抑制) + final String pageNoEd = editZ(pageNo, 3); + return ConvUtil.padRight("========== 給与明細書 ==========", 40) + + ConvUtil.padRight("", 10) + + pageNoEd + + "PG" + + ConvUtil.padRight("", 144); + } + + /** + * 01 FOOTER-LINE 編集(200B)。 + * 05 FL-MSG PIC X(040) VALUE '--- 明細終了 ---' + * 05 FILLER PIC X(160) VALUE SPACES + */ + private static String buildFooterLine() { + return ConvUtil.padRight("--- 明細終了 ---", 40) + + ConvUtil.padRight("", 160); + } + + /** PIC Z(n)9 BLANK WHEN ZERO 編集(編集桁数 = zCount + 1)。 */ + private static String editZ(final int val, final int zCount) { + final int len = zCount + 1; + if (val == 0) { + // BLANK WHEN ZERO + return ConvUtil.padRight("", len); + } + final String s = ConvUtil.padLeftZero(val, len); + final char[] ca = s.toCharArray(); + for (int i = 0; i < zCount; i++) { + if (ca[i] == '0') { + ca[i] = ' '; + } else { + break; + } + } + return new String(ca); + } + + // ==================== ファイル操作補助(OPEN/CLOSE 相当) ==================== + + /** OPEN INPUT R01INNFIL / OUTPUT W01OUTFIL W02OUTFIL. */ + private void openFiles() { + try { + r01InnFil = new BufferedReader( + new InputStreamReader(new FileInputStream(R01INN_FILE), StandardCharsets.UTF_8)); + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), StandardCharsets.UTF_8)); + w02OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W02OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN失敗 ≈ ABEND + } + } + + /** CLOSE R01INNFIL W01OUTFIL W02OUTFIL. */ + private void closeFiles() { + closeQuiet(r01InnFil); + closeQuiet(w01OutFil); + closeQuiet(w02OutFil); + r01InnFil = null; + w01OutFil = null; + w02OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof BufferedReader) { + ((BufferedReader) handle).close(); + } else if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (IOException e) { + // CLOSE異常は無視(原程序もFILE STATUS未管理) + } + } +} diff --git a/JavaSrc/src/Kyu09MrgMain.java b/JavaSrc/src/Kyu09MrgMain.java new file mode 100644 index 0000000..44061b7 --- /dev/null +++ b/JavaSrc/src/Kyu09MrgMain.java @@ -0,0 +1,450 @@ +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import dto.Kyu04Rec; +import dto.Kyu06Rec; +import dto.Kyu99Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】KYU09MRG 支給データ結合処理 + * ============================================================ + * PROGRAM-ID. KYU09MRG → 类 Kyu09MrgMain(PGMパターン:MERGE 複数ファイル結合) + * + * 使用ファイル(SELECT ASSIGN → 路径常量): + * SALARY-FILE(KYU09M01 SALARY-NET F200B) → SAL_FILE(COPY KYU04REC → dto.Kyu04Rec) + * BONUS-FILE(KYU09M02 BONUS-DATA F200B) → BON_FILE(COPY KYU04REC → dto.Kyu04Rec) + * W01OUTFIL(KYU09W01 PAYMENT-DATA F200B) → W01OUT_FILE(COPY KYU06REC → dto.Kyu06Rec) + * W02OUTFIL(KYU09W02 ERROR-LOG VB200B) → W02OUT_FILE(COPY KYU99REC → dto.Kyu99Rec) + * MERGE-FILE(KYU09M99 作業SD) → Java 側は2ファイルのマージ結合で再現 + * 使用SUB(CALL → 类#execute): + * SUB01DAT→Sub01DatSub / SUB02MSG→Sub02MsgSub / SUB03END→Sub03EndSub + * 段落对应: + * 0000MAJCOLSOR→run()/mrgPara() 1000ITTSOR→initPara() 2000MRGOUTSOR→mrgOutSor() + * 2100PAYOUTSOR→payOutPara() 3000STPSOR→termPara() 4000MSGOUTSOR→msgOutPara() + * 9999ABDSOR→abendPara() 0010DIRECTSOR→dirSor()(ENTRY 'KYU09ENT' 代替入口) + * + * 处理要点(詳細設計書_KYU09MRG 準拠): + * - COBOL の MERGE 文(SALARY/BONUS を EMP-ID 昇順に結合)は、Java に同等文が + * 無いため「両入力ファイルは EMP-ID 昇順済」という前提の下、2ポインタ・マージ + * 結合リストとして再現。結合後ストリームを OUTPUT PROCEDURE(2000MRGOUTSOR) に渡す。 + * - 同一社員内で TYPE='S'(給与) は WRK-SALARY-AMOUNT/WRK-BANK-CODE/WRK-ACCOUNT-NO に、 + * TYPE='B'(賞与) は WRK-BONUS-AMOUNT に集約、合算して PAYMENT-DATA を出力。 + */ +public class Kyu09MrgMain { + + // ==================== コンスタント領域 CNSARA ==================== + + /** 03 CNS-PRGIDX PIC X(008) VALUE 'KYU09MRG'. */ + private static final String CST_PRGIDX = "KYU09MRG"; + /** 03 CNS-MSGSTR PIC 9(003) VALUE 001. 開始メッセージ */ + private static final int CST_MSGSTR = 1; + /** 03 CNS-MSGFIN PIC 9(003) VALUE 002. 終了メッセージ */ + private static final int CST_MSGFIN = 2; + /** 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. サブエラーメッセージ */ + private static final int CST_MSGSUBEEK = 5; + /** 03 CNS-MSGIINKES PIC 9(003) VALUE 006. 入力件数メッセージ */ + private static final int CST_MSGIINKES = 6; + /** 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. 出力件数メッセージ */ + private static final int CST_MSGOUTKES = 7; + /** 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. コンパイル日時メッセージ */ + private static final int CST_MSGKEYINF = 33; + /** 03 CNS-KN0002 PIC 9(001) VALUE 2. */ + private static final int CST_KN0002 = 2; + /** 03 CNS-ABD999 PIC 9(003) VALUE 999. */ + private static final int CST_ABD999 = 999; + /** 03 CNS-PAY-TYPE-SALARY PIC X(001) VALUE 'S'. */ + private static final String CST_PAY_TYPE_SALARY = "S"; + /** 03 CNS-PAY-TYPE-BONUS PIC X(001) VALUE 'B'. */ + private static final String CST_PAY_TYPE_BONUS = "B"; + /** 03 CNS-PAY-TYPE-TOTAL PIC X(001) VALUE 'T'. */ + private static final String CST_PAY_TYPE_TOTAL = "T"; + + /** 88 WRK-MERGE-EOF-Y の HIGH-VALUES(PIC X(008) の最大値)相当。 */ + private static final String HIGH_VALUES_EMP_ID = "\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF"; + + // ==================== ファイル定義(SELECT ASSIGN 相当) ==================== + + /** SELECT SALARY-FILE ASSIGN TO KYU09M01.(SALARY-NET 入力 F200B) */ + private static final String SAL_FILE = "data/KYU09M01.txt"; + /** SELECT BONUS-FILE ASSIGN TO KYU09M02.(BONUS-DATA 入力 F200B) */ + private static final String BON_FILE = "data/KYU09M02.txt"; + /** SELECT W01OUTFIL ASSIGN TO KYU09W01.(PAYMENT-DATA 出力 F200B) */ + private static final String W01OUT_FILE = "data/KYU09W01.txt"; + /** SELECT W02OUTFIL ASSIGN TO KYU09W02.(ERROR-LOG 出力 VB200B) */ + private static final String W02OUT_FILE = "data/KYU09W02.txt"; + + // ==================== カウンタ領域 CUNARA(COMP-3 → long) ==================== + + /** 03 CUN-W01OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW01Out; + /** 03 CUN-W02OUT PIC S9(009) COMP-3 VALUE ZERO. */ + private long cunW02Out; + + // ==================== 作業領域 WRKARA ==================== + + /** 03 WRK-MERGE-EOF PIC X(001). 読込フラグ(88 WRK-MERGE-EOF-Y VALUE '1') */ + private String wrkMergeEof = "0"; + /** 03 WRK-CURRENT-EMP PIC X(008). */ + private String wrkCurrentEmp = ""; + /** 03 WRK-SALARY-AMOUNT PIC 9(009). */ + private int wrkSalaryAmount; + /** 03 WRK-BONUS-AMOUNT PIC 9(009). */ + private int wrkBonusAmount; + /** 03 WRK-TOTAL-PAYMENT PIC 9(009). */ + private int wrkTotalPayment; + /** 03 WRK-BANK-CODE PIC X(004). */ + private String wrkBankCode = ""; + /** 03 WRK-ACCOUNT-NO PIC 9(008). */ + private int wrkAccountNo; + /** 03 WS-PAD-CHAR PIC X(001) VALUE SPACE. */ + private String wsPadChar = ""; + /** 03 WS-FILE-STATUS PIC X(002). */ + private String wsFileStatus = ""; + + /** 88 WRK-MERGE-EOF-Y VALUE '1'. */ + private boolean isMergeEof() { + return "1".equals(wrkMergeEof); + } + + // ==================== サブプログラム連絡領域(LINKAGE COPY → DTO) ==================== + + /** COPY ZANDATAC.(SUB01DAT用) */ + private final ZandatacPar d01UbsPar = new ZandatacPar(); + /** COPY ZANMSGAC.(SUB02MSG用) */ + private final ZanmsgacPar m00MhoPar = new ZanmsgacPar(); + /** COPY ZANENDAC.(SUB03END用) */ + private final ZanendacPar e01AbdPar = new ZanendacPar(); + + // ==================== 出力レコード(FILE SECTION → DTO) ==================== + + /** 01 W01OUTREC.(FD W01OUTFIL) */ + private final Kyu06Rec w01 = new Kyu06Rec(); + /** 01 W02OUTREC.(FD W02OUTFIL) */ + private final Kyu99Rec w02 = new Kyu99Rec(); + + // ==================== ファイルハンドル(OPEN/CLOSE 相当) ==================== + + private PrintWriter w01OutFil; + private PrintWriter w02OutFil; + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(final String[] args) { + new Kyu09MrgMain().run(); + } + + /** + * 段落 0000MAJCOLSOR:制御処理。 + * PERFORM 1000ITTSOR → MERGE実行 → PERFORM 3000STPSOR → GOBACK. + */ + private void run() { + try { + initPara(); // 1000ITTSOR + mrgPara(); // MERGE 実行(0000MAJCOLSOR 内)→ OUTPUT PROCEDURE 2000MRGOUTSOR + termPara(); // 3000STPSOR + } finally { + closeFiles(); + } + } + + /** + * MERGE 実行(0000MAJCOLSOR 内)。 + * COBOL: MERGE MERGE-FILE ON ASCENDING KEY MERGE-EMP-ID + * USING SALARY-FILE BONUS-FILE + * OUTPUT PROCEDURE 2000MRGOUTSOR. + * Java: 2入力ファイルは EMP-ID 昇順済と仮定し、2ポインタ・マージ結合リストを構築、 + * そのストリームを 2000MRGOUTSOR に渡す。 + */ + private void mrgPara() { + // *** MERGE 作業領域構築(SALARY + BONUS を EMP-ID 昇順に結合) + final List merged = mergeInputs(); + // *** OUTPUT PROCEDURE 2000MRGOUTSOR + mrgOutSor(merged); + } + + /** + * 両入力ファイル読込+EMP-ID 昇順マージ結合(COBOL MERGE 文の再現)。 + * 入力ファイルは MERGE 文が自動 OPEN するため、ここで開いて読込後にクローズ。 + */ + private List mergeInputs() { + final List sal = readFile(SAL_FILE); // SALARY-FILE(REC-TYPE='S') + final List bon = readFile(BON_FILE); // BONUS-FILE(REC-TYPE='B') + final List merged = new ArrayList<>(); + int i = 0; + int j = 0; + // USING 順(SALARY 優先)で同キー時の安定性を維持 + while (i < sal.size() && j < bon.size()) { + final int cmp = sal.get(i).getR01EmpId().compareTo(bon.get(j).getR01EmpId()); + if (cmp <= 0) { + merged.add(sal.get(i)); + i++; + } else { + merged.add(bon.get(j)); + j++; + } + } + while (i < sal.size()) { + merged.add(sal.get(i)); + i++; + } + while (j < bon.size()) { + merged.add(bon.get(j)); + j++; + } + return merged; + } + + /** 入力ファイル読込(OPEN INPUT / READ AT END 相当)。 */ + private List readFile(final String path) { + final List list = new ArrayList<>(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.isEmpty()) { + continue; // 空行はスキップ + } + // READ → MOVE 行 TO レコード(200B 固定長) + final Kyu04Rec r = new Kyu04Rec(); + r.parseRecord(line); // READ → MOVE 行 TO レコード(200B 固定長) + list.add(r); + } + } catch (IOException e) { + throw new UncheckedIOException(e); // READ/OPEN 異常 ≈ ABEND + } + return list; + } + + /** + * 段落 1000ITTSOR:初期処理。 + * 開始メッセージ→コンパイル日時→ワーク初期化→運用日付取得→出力ファイル OPEN。 + */ + private void initPara() { + // *** 開始メッセージ出力 + msgOutPara(CST_MSGSTR, null, null); + + // *** コンパイル日時出力 ※FUNCTION WHEN-COMPILED→起動時刻で代用 + final String compiled = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + msgOutPara(CST_MSGKEYINF, compiled, "COMPILED"); + + // *** ワークエリア初期化 INITIALIZE WRKARA. + wrkMergeEof = "0"; + wrkCurrentEmp = ""; + wrkSalaryAmount = 0; + wrkBonusAmount = 0; + wrkTotalPayment = 0; + wrkBankCode = ""; + wrkAccountNo = 0; + cunW01Out = 0; + cunW02Out = 0; + wsPadChar = ""; + wsFileStatus = ""; + + // *** 運用日付取得 SUB01DAT + d01UbsPar.reset(); + Sub01DatSub.execute(d01UbsPar); + if (d01UbsPar.getD01FkiCod() != 0) { + // 復帰コード≠ZERO → サブエラーメッセージ出力 → ABEND + msgOutPara(CST_MSGSUBEEK, "SUB01DAT", + ConvUtil.padLeftZero(d01UbsPar.getD01FkiCod(), 4)); + abendPara(); + } + + // *** OPEN OUTPUT W01OUTFIL W02OUTFIL.(入力は MERGE が自動 OPEN) + openFiles(); + } + + /** + * 段落 2000MRGOUTSOR:MERGE 出力(同一社員統合)処理。 + * RETURN MERGE-FILE ループ → 同一社員内で TYPE=S/B を集約 → 合算 → 2100PAYOUTSOR。 + */ + private void mrgOutSor(final List merged) { + // *** MERGE 結果受取ループ(PERFORM WITH TEST AFTER UNTIL WRK-MERGE-EOF-Y) + wrkMergeEof = "0"; + int idx = 0; + // 外側 RETURN MERGE-FILE INTO MERGE-REC AT END SET WRK-MERGE-EOF-Y TO TRUE + Kyu04Rec rec = returnMerge(merged, idx); + idx = nextIdx; + while (!isMergeEof()) { + // 2-2-1. 現社員番号を WRK-CURRENT-EMP に設定 + wrkCurrentEmp = rec.getR01EmpId(); + // 2-2-2. WRK-SALARY-AMOUNT/WRK-BONUS-AMOUNT を ZERO 初期化 + wrkSalaryAmount = 0; + wrkBonusAmount = 0; + wrkTotalPayment = 0; + + // 2-2-3. 同一社員内ループ(WRK-CURRENT-EMP ≠ MERGE-EMP-ID まで) + // RETURN は returnMerge 経由で行い、EOF は wrkMergeEof で判定する。 + while (!isMergeEof() && wrkCurrentEmp.equals(rec.getR01EmpId())) { + // 2-2-3-1. MERGE-REC-TYPE = 'S'(給与) の場合 + if (CST_PAY_TYPE_SALARY.equals(rec.getR01RecType())) { + // MOVE MERGE-PAY-AMOUNT TO WRK-SALARY-AMOUNT. + wrkSalaryAmount = (int) rec.getR01PayAmount(); + // MOVE MERGE-GROSS-PAYMENT TO WRK-BANK-CODE.(9(9)→X(4) 先頭4桁) + wrkBankCode = ConvUtil.padLeftZero(rec.getR01GrossPayment(), 9).substring(0, 4); + // MOVE MERGE-EMP-ID(1:4) TO WRK-ACCOUNT-NO.(X(4)→9(8) 右詰) + wrkAccountNo = ConvUtil.parseIntSafe(rec.getR01EmpId().substring(0, 4), 0); + } else { + // 2-2-3-2. 上記以外(賞与) → MOVE MERGE-PAY-AMOUNT TO WRK-BONUS-AMOUNT. + wrkBonusAmount = (int) rec.getR01PayAmount(); + } + // 2-2-3-3. RETURN MERGE-FILE で次レコード取得(AT END は returnMerge が wrkMergeEof='1') + rec = returnMerge(merged, idx); + idx = nextIdx; + } + + // 2-2-4. 給与+賞与合算 ADD WRK-SALARY-AMOUNT TO WRK-BONUS-AMOUNT GIVING WRK-TOTAL-PAYMENT + wrkTotalPayment = wrkSalaryAmount + wrkBonusAmount; + // 2-2-5. 2100PAYOUTSOR で PAYMENT-DATA 出力 + payOutPara(); + // ※外側ループ末尾の RETURN は上記同一社員内ループの最後の RETURN で完了済み。 + // 重複 RETURN を削除(旧実装は次社員の先頭レコードを 1 件読み飛ばしていた)。 + } + } + + /** RETURN MERGE-FILE 擬似実装:idx が範囲外なら EOF をセットして null を返す。 */ + private Kyu04Rec returnMerge(final List merged, final int idx) { + if (idx < merged.size()) { + nextIdx = idx + 1; + return merged.get(idx); + } + wrkMergeEof = "1"; // AT END: SET WRK-MERGE-EOF-Y TO TRUE + nextIdx = idx; + return null; + } + + /** returnMerge が次回の読込位置を戻すための作業変数。 */ + private int nextIdx; + + /** + * 段落 2100PAYOUTSOR:PAYMENT-DATA 出力処理。 + * W01OUTREC 編集 → WRITE W01OUTREC → ADD 1 TO CUN-W01OUT. + */ + private void payOutPara() { + // INITIALIZE W01OUTREC. + w01.reset(); + // MOVE WRK-CURRENT-EMP TO W01EMP-ID. + w01.setEmpId(wrkCurrentEmp); + // MOVE CNS-PAY-TYPE-TOTAL('T') TO W01PAY-TYPE. + w01.setPayType(CST_PAY_TYPE_TOTAL); + // MOVE WRK-TOTAL-PAYMENT TO W01AMOUNT. + w01.setAmount(wrkTotalPayment); + // MOVE WRK-BANK-CODE TO W01BANK-CODE. + w01.setBankCode(wrkBankCode); + // MOVE WRK-ACCOUNT-NO TO W01ACCOUNT-NO. + w01.setAccountNo(wrkAccountNo); + // WRITE W01OUTREC. / ADD 1 TO CUN-W01OUT. + w01OutFil.println(w01.toRecordLine()); + cunW01Out++; + } + + /** + * 段落 3000STPSOR:終了処理。 + * CLOSE → 入出力件数メッセージ×3 → 終了メッセージ。 + */ + private void termPara() { + closeFiles(); + + // 入力件数メッセージ(PARM1='MERGE-FILE') + // ※COBOL は CUN-W01OUT を流用しており、設計書に準拠し MERGE 入力件数≈出力件数で出力 + msgOutPara(CST_MSGIINKES, "MERGE-FILE", ConvUtil.padLeftZero(cunW01Out, 9)); + // 出力件数メッセージ(PARM1='KYU09W01') + msgOutPara(CST_MSGOUTKES, "KYU09W01", ConvUtil.padLeftZero(cunW01Out, 9)); + // 出力件数メッセージ(PARM1='KYU09W02') + msgOutPara(CST_MSGOUTKES, "KYU09W02", ConvUtil.padLeftZero(cunW02Out, 9)); + // 終了メッセージ + msgOutPara(CST_MSGFIN, null, null); + } + + /** + * 段落 4000MSGOUTSOR:メッセージ編集出力処理。 + * M00UMKDATS22-03(1:1)/-04(1:1) に '2'、-05 にプログラムIDを設定し SUB02MSG を呼ぶ。 + */ + private void msgOutPara(final int msgCod, final String p1, final String p2) { + m00MhoPar.reset(); + m00MhoPar.setM00MsgCod(msgCod); + if (p1 != null) { + m00MhoPar.setM00Umkdats2201(p1); + } + if (p2 != null) { + m00MhoPar.setM00Umkdats2202(p2); + } + // MOVE CNS-KN0002 TO M00UMKDATS22-03(1:1) / -04(1:1). + m00MhoPar.setM00Umkdats2203(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + m00MhoPar.setM00Umkdats2204(ConvUtil.padRight(Integer.toString(CST_KN0002), 30)); + // MOVE CNS-PRGIDX TO M00UMKDATS22-05. + m00MhoPar.setM00Umkdats2205(CST_PRGIDX); + Sub02MsgSub.execute(m00MhoPar); + } + + /** + * 段落 9999ABDSOR:ABEND 処理。 + * MOVE 999 TO E01ABDCOD./CALL 'SUB03END' USING E01ABDPAR. + */ + private void abendPara() { + e01AbdPar.reset(); + e01AbdPar.setE01AbdCod(CST_ABD999); + Sub03EndSub.execute(e01AbdPar); + } + + /** + * 段落 0010DIRECTSOR:ENTRY 'KYU09ENT' 代替エントリポイント(直接結合モード)。 + * COBOL は ENTRY 文による別入口だが Java に同等が無いため、main からは呼ばれない + * 独立メソッドとして再現。簡易エラーログ('DIRECT ENTRY')を W02 へ出力。 + */ + private void dirSor() { + // MOVE 'DIRECT ENTRY' TO W02ERR-DETAIL. + w02.reset(); + w02.setErrDetail("DIRECT ENTRY"); + // WRITE W02OUTREC. / ADD 1 TO CUN-W02OUT. + w02OutFil.println(w02.toRecordLine()); + cunW02Out++; + } + + // ==================== ファイル操作補助(OPEN/CLOSE/WRITE 相当) ==================== + + /** OPEN OUTPUT W01OUTFIL W02OUTFIL. */ + private void openFiles() { + try { + w01OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W01OUT_FILE), StandardCharsets.UTF_8)); + w02OutFil = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(W02OUT_FILE), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); // OPEN 失敗 ≈ ABEND + } + } + + /** CLOSE W01OUTFIL W02OUTFIL. */ + private void closeFiles() { + closeQuiet(w01OutFil); + closeQuiet(w02OutFil); + w01OutFil = null; + w02OutFil = null; + } + + private static void closeQuiet(final Object handle) { + try { + if (handle instanceof PrintWriter) { + ((PrintWriter) handle).close(); + } + } catch (Exception e) { + // CLOSE 異常は無視(原程序も FILE STATUS 未管理) + } + } +} diff --git a/JavaSrc/src/Sample01Main.java b/JavaSrc/src/Sample01Main.java new file mode 100644 index 0000000..d07752a --- /dev/null +++ b/JavaSrc/src/Sample01Main.java @@ -0,0 +1,138 @@ +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * ============================================================ + * 【COBOL→Java 标准例】主模板(常用语法映射) + * ============================================================ + * 程序整体结构对应: + * IDENTIFICATION DIVISION. PROGRAM-ID. SAMPLE01. → 本类名 Sample01Main + * ENVIRONMENT DIVISION. → (无直接对应,文件定义见 FileSample) + * DATA DIVISION. WORKING-STORAGE SECTION. → 类的字段区 + * PROCEDURE DIVISION. 各段落 → 各 private 方法(xxxPara) + * STOP RUN → main 方法正常返回 + * + * 命名规范: + * COBOL 变量 WS-XXX → Java 字段 wsXxx + * COBOL 段落 XXX-PARA → Java 方法 xxxPara() + * COBOL 常量 / 88级 → static final 常量 或 isXxx() 判定方法 + */ +public class Sample01Main { + + // ==================== WORKING-STORAGE SECTION ==================== + + // 01 WS-CST.(VALUE 子句的常量定义 → static final 常量) + private static final BigDecimal CST_TAX_RATE = new BigDecimal("0.08"); // 77 CST-TAX-RATE PIC V99 VALUE 0.08. + private static final String CST_DEPT_SALES = "10"; // 78 CST-DEPT-SALES VALUE '10'. + private static final String CST_DEPT_DEV = "20"; // 78 CST-DEPT-DEV VALUE '20'. + + // 01 WS-WORK.(各 05 层项目 → 私有字段,类型按 ConvUtil 头部规则映射) + private String wsDept = ""; // 05 WS-DEPT PIC X(2). + private BigDecimal wsAmount = BigDecimal.ZERO; // 05 WS-AMOUNT PIC 9(5)V99. + private BigDecimal wsTax = BigDecimal.ZERO; // 05 WS-TAX PIC 9(3)V99. + private int wsIdx = 0; // 05 WS-IDX PIC 9(3). + private String wsEndFlg = "0"; // 05 WS-END-FLG PIC X(1) VALUE '0'. + + // 88 END-ON VALUE '1'.(88 级条件名 → 常量 + 判定方法) + private static final String FLG_ON = "1"; + + private boolean isEndOn() { + return FLG_ON.equals(wsEndFlg); + } + + // ==================== PROCEDURE DIVISION ==================== + + public static void main(String[] args) { + new Sample01Main().run(); // PERFORM RUN-PARA 相当 + } + + /** + * 主控制段落。 + * COBOL: + * MAIN-PROC. + * PERFORM INIT-PROC THRU TERM-PROC. + * STOP RUN. + */ + private void run() { + initPara(); + calcPara(); + judgePara(); + loopPara(); + fileProcPara(); + termPara(); + // STOP RUN → main 正常返回即结束 + } + + /** 初期处理段落(MOVE / INITIALIZE 的映射) */ + private void initPara() { + // MOVE 1000 TO WS-AMOUNT. + wsAmount = new BigDecimal("1000.00"); + // MOVE "10" TO WS-DEPT. + wsDept = CST_DEPT_SALES; + // INITIALIZE WS-WORK.(字段恢复初值:数值→ZERO、文字→SPACE) + wsTax = BigDecimal.ZERO; + wsIdx = 0; + wsEndFlg = "0"; + } + + /** 计算处理段落(COMPUTE / ADD / SUBTRACT / ROUNDED 的映射) */ + private void calcPara() { + // COMPUTE WS-TAX ROUNDED = WS-AMOUNT * CST-TAX-RATE. + wsTax = wsAmount.multiply(CST_TAX_RATE).setScale(0, RoundingMode.HALF_UP); + // ADD 100 TO WS-AMOUNT. / SUBTRACT 50 FROM WS-AMOUNT. + wsAmount = wsAmount.add(BigDecimal.valueOf(100)).subtract(BigDecimal.valueOf(50)); + // DISPLAY "WS-AMOUNT=" WS-AMOUNT " WS-TAX=" WS-TAX. + System.out.println("[calc] amount=" + wsAmount.toPlainString() + " tax=" + wsTax.toPlainString()); + } + + /** 判定处理段落(IF / EVALUATE 的映射) */ + private void judgePara() { + // IF WS-DEPT = "10" ... ELSE ... END-IF. + if (CST_DEPT_SALES.equals(wsDept)) { + System.out.println("[judge] dept=SALES"); + } else { + System.out.println("[judge] dept=OTHER"); + } + // EVALUATE WS-DEPT + // WHEN "10" ... WHEN "20" ... WHEN OTHER ... + // END-EVALUATE. + switch (wsDept) { + case CST_DEPT_SALES: + System.out.println("[eval] branch=SALES DEPT"); + break; + case CST_DEPT_DEV: + System.out.println("[eval] branch=DEV DEPT"); + break; + default: + System.out.println("[eval] branch=OTHER"); + break; + } + } + + /** 反复处理段落(PERFORM UNTIL / PERFORM VARYING 的映射) */ + private void loopPara() { + // MOVE 0 TO WS-END-FLG. PERFORM UNTIL WS-END-FLG = '1' ... END-PERFORM. + wsEndFlg = "0"; + while (!isEndOn()) { + wsIdx++; + if (wsIdx >= 3) { + wsEndFlg = FLG_ON; // MOVE '1' TO WS-END-FLG. + } + } + // PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5. → for 文 + for (int i = 1; i <= 5; i++) { + // 段内处理 + } + System.out.println("[loop] idx=" + wsIdx); + } + + /** 文件处理段落(READ / WRITE 的映射 → FileSample 参照) */ + private void fileProcPara() { + FileSample.execute(); + } + + /** 终了处理段落 */ + private void termPara() { + System.out.println("[term] SAMPLE01 NORMAL END"); + } +} diff --git a/JavaSrc/src/Sub01DatSub.java b/JavaSrc/src/Sub01DatSub.java new file mode 100644 index 0000000..0c8db3d --- /dev/null +++ b/JavaSrc/src/Sub01DatSub.java @@ -0,0 +1,34 @@ +import dto.ZandatacPar; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * ============================================================ + * 【COBOL→Java】SUB01DAT 運用日付取得サブ + * ============================================================ + * PROGRAM-ID. SUB01DAT → 类 Sub01DatSub + * LINKAGE: D01UBSPAR(COPY ZANDATAC → dto.ZandatacPar) + * 処理概要: FUNCTION CURRENT-DATE により現在日付を取得し + * YYYYMMDD 形式で返す。 + * + * ※FUNCTION CURRENT-DATE(26桁 YYYYMMDDhhmmsscc…)の先頭8桁を使用。 + */ +public class Sub01DatSub { + + private static final DateTimeFormatter YMD_FMT = + DateTimeFormatter.ofPattern("yyyyMMdd"); + + /** + * PROCEDURE DIVISION USING D01UBSPAR / 段落 0000MAINSOR。 + */ + public static void execute(final ZandatacPar par) { + // MOVE FUNCTION CURRENT-DATE TO WRK-SYS-DATE.(X(026)) + String wrkSysDate = LocalDateTime.now().format(YMD_FMT); // 先頭8桁相当 + + // MOVE WRK-SYS-DATE(1:8) TO D01UBSUDATE. + par.setD01UbsUdate(Integer.parseInt(wrkSysDate.substring(0, 8))); + + // MOVE ZERO TO D01FKICOD.(正常終了) + par.setD01FkiCod(0); + } +} diff --git a/JavaSrc/src/Sub02MsgSub.java b/JavaSrc/src/Sub02MsgSub.java new file mode 100644 index 0000000..d5f4b8a --- /dev/null +++ b/JavaSrc/src/Sub02MsgSub.java @@ -0,0 +1,34 @@ +import dto.ZanmsgacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】SUB02MSG メッセージ編集出力サブ + * ============================================================ + * PROGRAM-ID. SUB02MSG → 类 Sub02MsgSub + * LINKAGE: M00MHOPAR(COPY ZANMSGAC → dto.ZanmsgacPar) + * 処理概要: メッセージ番号とパラメータを編集し標準出力へ出力する。 + * + * ※STRING文は DELIMITED 句省略時 = DELIMITED BY SIZE: + * 各 X(30) 置換項目は后尾空格含め30桁そのまま連結されるため + * Java 侧也按 padRight(30) 忠实再现。 + * ※DISPLAY は PIC X(303) 全長出力(后尾空格含む)。 + */ +public class Sub02MsgSub { + + /** + * PROCEDURE DIVISION USING M00MHOPAR / 段落 0000MAINSOR。 + */ + public static void execute(final ZanmsgacPar par) { + // STRING 'MSG[' M00MSGCOD '] P1=' ... INTO WRK-MSG-LINE END-STRING. + String line = "MSG[" + + ConvUtil.padLeftZero(par.getM00MsgCod(), 3) + "] P1=" + + ConvUtil.padRight(par.getM00Umkdats2201(), 30) + " P2=" + + ConvUtil.padRight(par.getM00Umkdats2202(), 30) + " P3=" + + ConvUtil.padRight(par.getM00Umkdats2203(), 30) + " P4=" + + ConvUtil.padRight(par.getM00Umkdats2204(), 30) + " P5=" + + ConvUtil.padRight(par.getM00Umkdats2205(), 30); + // DISPLAY WRK-MSG-LINE.(X(303) 全长,含后尾空格) + System.out.println(ConvUtil.padRight(line, 303)); + } +} diff --git a/JavaSrc/src/Sub03EndSub.java b/JavaSrc/src/Sub03EndSub.java new file mode 100644 index 0000000..c426c45 --- /dev/null +++ b/JavaSrc/src/Sub03EndSub.java @@ -0,0 +1,34 @@ +import dto.ZanendacPar; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】SUB03END ABEND処理サブ + * ============================================================ + * PROGRAM-ID. SUB03END → 类 Sub03EndSub + * LINKAGE: E01ABDPAR(COPY ZANENDAC → dto.ZanendacPar) + * 処理概要: ABENDコードを表示しプログラムを異常終了させる。 + * + * ※STOP RUN(ABEND)→ 疑问Q4回答により System.exit(コード) にマッピング。 + */ +public class Sub03EndSub { + + /** + * PROCEDURE DIVISION USING E01ABDPAR / 段落 0000MAINSOR。 + */ + public static void execute(final ZanendacPar par) { + // MOVE 'ABEND CODE=' TO WRK-ABEND-MSG.(X(020) 左詰め) + String wrkAbendMsg = ConvUtil.padRight("ABEND CODE=", 20); + + // MOVE E01ABDCOD TO WRK-ABEND-MSG(12:3).(12~14桁を3桁数字で上書き) + wrkAbendMsg = wrkAbendMsg.substring(0, 11) + + ConvUtil.padLeftZero(par.getE01AbdCod(), 3) + + wrkAbendMsg.substring(14); + + // DISPLAY WRK-ABEND-MSG. + System.out.println(wrkAbendMsg); + + // STOP RUN.(異常終了) + System.exit(par.getE01AbdCod()); + } +} diff --git a/JavaSrc/src/Sub04ChkSub.java b/JavaSrc/src/Sub04ChkSub.java new file mode 100644 index 0000000..96277b9 --- /dev/null +++ b/JavaSrc/src/Sub04ChkSub.java @@ -0,0 +1,135 @@ +import dto.ZanchkacPar; + +/** + * ============================================================ + * 【COBOL→Java】SUB04CHK 項目チェックサブ + * ============================================================ + * PROGRAM-ID. SUB04CHK → 类 Sub04ChkSub + * LINKAGE: C01CHKPAR(COPY ZANCHKAC → dto.ZanchkacPar) + * 処理概要: 指定されたタイプによりデータ項目の妥当性をチェックする。 + * DATE : YYYYMMDD 月日範囲チェック NG=0001 + * TIME : HHMM 時分範囲チェック NG=0002 + * NUM : 数字・空白のみか NG=0003 + * EMPID : 英数字8桁 NG=0004 + * OTHER : 型誤り 9999 + * + * ※原代码各段落内的 GO TO xxx-EXT 均映射为方法提前 return(GO TO 方针:保持原结构)。 + * ※NUMVAL 对非数字数据会运行时异常,对应 Java 的 NumberFormatException + * (≈ COBOL S0C1/S0C7 ABEND),保持一致不另行捕获。 + */ +public class Sub04ChkSub { + + /** + * PROCEDURE DIVISION USING C01CHKPAR / 段落 0000MAINSOR。 + * EVALUATE C01CHKTYP ... END-EVALUATE. + */ + public static void execute(final ZanchkacPar par) { + int rc; + switch (par.getC01ChkTyp().trim()) { + case "DATE": + rc = chkDatePara(par.getC01ChkDat()); + break; + case "TIME": + rc = chkTimePara(par.getC01ChkDat()); + break; + case "NUM": + rc = chkNumPara(par.getC01ChkDat()); + break; + case "EMPID": + rc = chkEmpPara(par.getC01ChkDat()); + break; + default: + rc = 9999; // WHEN OTHER: MOVE 9999 TO WRK-RETURN-CODE + break; + } + // MOVE WRK-RETURN-CODE TO C01CHKRRC.(復帰コード設定して復帰) + par.setC01ChkRrc(rc); + } + + /** + * 1000CHKDATE:YYYYMMDD 形式の妥当性チェック。 + * ※月(5:2)/日(7:2) のみ範囲判定(原逻辑如此,不做历法校验)。 + */ + private static int chkDatePara(final String dat) { + // MOVE FUNCTION NUMVAL(C01CHKDAT(5:2)) TO WRK-NN. + int nn = Integer.parseInt(dat.substring(4, 6)); + if (nn < 1 || nn > 12) { + return 1; // GO TO 1000CHKDATE-EXT 相当 + } + // MOVE FUNCTION NUMVAL(C01CHKDAT(7:2)) TO WRK-NN. + nn = Integer.parseInt(dat.substring(6, 8)); + if (nn < 1 || nn > 31) { + return 1; + } + return 0; // 正常 + } + + /** + * 2000CHKTIME:HHMM 形式の妥当性チェック。 + */ + private static int chkTimePara(final String dat) { + // MOVE FUNCTION NUMVAL(C01CHKDAT(1:2)) TO WRK-NN.(時) + int nn = Integer.parseInt(dat.substring(0, 2)); + if (nn > 23) { + return 2; // GO TO 2000CHKTIME-EXT 相当 + } + // MOVE FUNCTION NUMVAL(C01CHKDAT(3:2)) TO WRK-NN.(分) + nn = Integer.parseInt(dat.substring(2, 4)); + if (nn > 59) { + return 2; + } + return 0; // 正常 + } + + /** + * 3000CHKNUM:全桁が数字であることのチェック。 + * ※全空白は正常扱い(オプショナル項目)。 + */ + private static int chkNumPara(final String dat) { + // INSPECT TALLYING WRK-NN FOR LEADING SPACES.(先行空白数) + int leading = 0; + while (leading < dat.length() && dat.charAt(leading) == ' ') { + leading++; + } + if (leading == dat.length()) { + return 0; // 全スペース → 正常(GO TO EXT 相当) + } + // INSPECT TALLYING FOR ALL "0".."9" SPACE. + // (数字と空白以外が一つでもあれば NG) + for (int i = 0; i < dat.length(); i++) { + char c = dat.charAt(i); + if (c != ' ' && (c < '0' || c > '9')) { + return 3; // GO TO 3000CHKNUM-EXT 相当 + } + } + return 0; // 正常 + } + + /** + * 4000CHKEMP:英数字8桁のチェック。 + */ + private static int chkEmpPara(final String dat) { + // IF C01CHKDAT(9:72) NOT = SPACES THEN NG(8桁超過データあり) + if (!dat.substring(8).trim().isEmpty()) { + return 4; // GO TO 4000CHKEMP-EXT 相当 + } + // IF C01CHKDAT(1:8) = SPACES THEN NG(必須エラー) + if (dat.substring(0, 8).trim().isEmpty()) { + return 4; + } + // 数字/英字/空白をカウントし合計8桁か判定 + // (INSPECT TALLYING FOR ALL ... の集合カウント相当) + int cnt = 0; + for (int i = 0; i < 8; i++) { + char c = dat.charAt(i); + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') || c == ' ') { + cnt++; + } + } + if (cnt != 8) { + return 4; // 英数字以外(特殊文字)あり + } + return 0; // 正常 + } +} diff --git a/JavaSrc/src/Sub05TimSub.java b/JavaSrc/src/Sub05TimSub.java new file mode 100644 index 0000000..f92bdf7 --- /dev/null +++ b/JavaSrc/src/Sub05TimSub.java @@ -0,0 +1,56 @@ +import dto.ZantimacPar; + +/** + * ============================================================ + * 【COBOL→Java】SUB05TIM 時刻丸め計算サブ + * ============================================================ + * PROGRAM-ID. SUB05TIM → 类 Sub05TimSub + * LINKAGE: T01TIMPAR(COPY ZANTIMAC → dto.ZantimacPar) + * 処理概要: 時間値(T01TIMHRS、1桁小数)を指定単位で丸め、T01TIMOUT に格納。 + * + * T01TIMRRC=0: 0.5時間単位・切上(30分単位) + * T01TIMRRC=1: 0.1時間単位・切上(6分単位)← KIN07DAI 使用 + * T01TIMRRC=2: 0.1時間単位・切捨(6分単位) + * T01TIMRRC=3: 単純四捨五入(0.1h単位、+0.05 して V9(1) 切捨) + * OTHER : そのまま出力 + * + * ※T01TIMHRS は 9(4)V9(1)(1桁小数)であるため、内部では「0.1h単位の整数(tenth)」 + * に変換して整数演算で丸め、再び 1桁小数へ戻す(浮動小数点誤差回避)。 + */ +public class Sub05TimSub { + + /** + * PROCEDURE DIVISION USING T01TIMPAR / 0000MAINSOR。 + */ + public static void execute(final ZantimacPar par) { + // 9(4)V9(1) → 0.1h単位整数への変換。設計(mode2=切捨)に合わせ床関数で切捨。 + // ※ZAN05CAL の OVT-HOURS は 0.1h 単位切捨(floor)が仕様。round だと境界で +0.1h ずれる。 + int tenth = (int) Math.floor(par.getT01Timhrs() * 10.0); + int outTenth; + + switch (par.getT01Timrrc()) { + case 0: + // 0.5h単位(=5/10h)で切上 + outTenth = ((tenth + 4) / 5) * 5; + break; + case 1: + // 0.1h単位で切上 → 既に 0.1h 単位のためそのまま(ceil(tenth/1)) + outTenth = tenth; + break; + case 2: + // 0.1h単位で切捨 → そのまま + outTenth = tenth; + break; + case 3: + // 0.1h単位四捨五入((hrs+0.05) して V9(1) 切捨)→ 1桁小数入力では no-op + outTenth = tenth; + break; + default: + // モード指定なし → そのまま + outTenth = tenth; + break; + } + + par.setT01Timout(outTenth / 10.0); + } +} diff --git a/JavaSrc/src/Zan01ChkMain.java b/JavaSrc/src/Zan01ChkMain.java new file mode 100644 index 0000000..4b3ced8 --- /dev/null +++ b/JavaSrc/src/Zan01ChkMain.java @@ -0,0 +1,349 @@ +import dto.ZanchkacPar; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.Zan01Rec; +import dto.Zan05Rec; +import dto.ZanmsgacPar; +import util.ConvUtil; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +/** + * ============================================================ + * 銆怌OBOL鈫扟ava銆慞ROGRAM-ID. ZAN01CHK 鈫?绫?Zan01ChkMain + * ============================================================ + * 娈嬫キ鐢宠珛鎸垎鍑︾悊: CSV(OVT-APPLY) 銈掕銇胯炯銇裤€併偣銉嗐兗銈裤偣銇ф尟銈婂垎銇戙€? * 銈广儐銉笺偪銈?0/1 鈫?闋呯洰銉併偋銉冦偗寰?OVT-VALID(W01) 銇俱仧銇?ERROR-LOG(W03) + * 銈广儐銉笺偪銈?9 鈫?OVT-CANCEL(W02) + * 銇濄伄浠? 鈫?ERROR-LOG(W03) + * 鈥昏ō瑷堟浉(瑭崇窗瑷▓鏇竉ZAN01CHK) 鍎厛銆侰OBOL 銇ㄦ暣鍚堛€? */ +public class Zan01ChkMain { + + // ---- 銈炽兂銈广偪銉炽儓 ---- + private static final String PRGIDX = "ZAN01CHK"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGSUBEEK = 5; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final int CNS_KN0002 = 2; + private static final int CNS_ABD999 = 999; + private static final int CNS_TIME_1830 = 1830; + private static final int CNS_DIFF_30 = 30; + + private static final String R01_PATH = "data/ZAN01R01.txt"; + private static final String W01_PATH = "data/ZAN01W01.txt"; + private static final String W02_PATH = "data/ZAN01W02.txt"; + private static final String W03_PATH = "data/ZAN01W03.txt"; // ERROR-LOG + + // ---- 銈偊銉炽偪 ---- + private long cunR01Inn = 0; + private long cunW01Out = 0; + private long cunW02Out = 0; + private long cunW03Out = 0; + + // ---- 浣滄キ闋樺煙 ---- + private int wrkU06; + private String wrkR01Eof = "0"; + private String r01Line = ""; + private String csvApplId = ""; + private String csvEmpId = ""; + private String csvApplDate = ""; + private String csvStartTime = ""; + private String csvEndTime = ""; + private String csvStatus = ""; + private String csvOvtType = ""; + private String csvFiller = ""; + + // ---- 銈点儢銉椼儹銈般儵銉犻€g怠闋樺煙 ---- + private final ZandatacPar d01 = new ZandatacPar(); + private final ZanmsgacPar m00 = new ZanmsgacPar(); + private final ZanendacPar e01 = new ZanendacPar(); + private final ZanchkacPar c01 = new ZanchkacPar(); + + // ---- 銉曘偂銈ゃ儷 ---- + private BufferedReader r01In; + private BufferedWriter w01Out; + private BufferedWriter w02Out; + private BufferedWriter w03Out; + + public static void main(final String[] args) { + new Zan01ChkMain().run(); + } + + private void run() { + initProc(); + while (!"1".equals(wrkR01Eof)) { + majProc(); + } + stpProc(); + } + + // ---- 1000ITTSOR ---- + private void initProc() { + msgOut(CNS_MSGSTR, "", ""); + msgCompiled(); + // 銉兗銈垵鏈熷寲 + wrkU06 = 0; + wrkR01Eof = "0"; + // 閬嬬敤鏃ヤ粯鍙栧緱 + d01.reset(); + Sub01DatSub.execute(d01); + if (d01.getD01FkiCod() != 0) { + msgOut(CNS_MSGSUBEEK, "SUB01DAT", String.valueOf(d01.getD01FkiCod())); + abend(); + return; + } + wrkU06 = d01.getD01UbsUdate(); + // OPEN + try { + r01In = FileIo.openReader(R01_PATH); + w01Out = FileIo.openWriter(W01_PATH); + w02Out = FileIo.openWriter(W02_PATH); + w03Out = FileIo.openWriter(W03_PATH); + } catch (IOException e) { + throw new RuntimeException("FILE OPEN ERROR", e); + } + readR01(); + } + + // ---- 1100R01INNSOR ---- + private void readR01() { + try { + String line = r01In.readLine(); + if (line == null) { + wrkR01Eof = "1"; + } else { + r01Line = line; + cunR01Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ ERROR", e); + } + } + + // ---- 2000MAJSOR ---- + private void majProc() { + csvSplit(); + if ("0".equals(csvStatus) || "1".equals(csvStatus)) { + validateProc(); + } else if ("9".equals(csvStatus)) { + cancelProc(); + } else { + errorProc(); + } + readR01(); + } + + // ---- 2010CSVSOR ---- + private void csvSplit() { + csvApplId = csvEmpId = csvApplDate = ""; + csvStartTime = csvEndTime = csvStatus = csvOvtType = csvFiller = ""; + String[] parts = r01Line.split(",", -1); + if (parts.length > 0) { + csvApplId = parts[0].trim(); + } + if (parts.length > 1) { + csvEmpId = parts[1].trim(); + } + if (parts.length > 2) { + csvApplDate = parts[2].trim(); + } + if (parts.length > 3) { + csvStartTime = parts[3].trim(); + } + if (parts.length > 4) { + csvEndTime = parts[4].trim(); + } + if (parts.length > 5) { + csvStatus = parts[5].trim(); + } + if (parts.length > 6) { + csvOvtType = parts[6].trim(); + } + if (parts.length > 7) { + csvFiller = parts[7].trim(); + } + } + + // ---- 2020VALIDATESOR ---- + private void validateProc() { + // 绀惧摗鐣彿銉併偋銉冦偗 + c01.reset(); + c01.setC01ChkTyp("EMPID"); + c01.setC01ChkDat(pad8(csvEmpId)); + Sub04ChkSub.execute(c01); + if (c01.getC01ChkRrc() != 0) { + writeW03("01", "EMP-ID ERROR:" + csvEmpId); + return; + } + // 鏃ヤ粯銉併偋銉冦偗 + c01.reset(); + c01.setC01ChkTyp("DATE"); + c01.setC01ChkDat(pad8(csvApplDate)); + Sub04ChkSub.execute(c01); + if (c01.getC01ChkRrc() != 0) { + writeW03("01", "DATE ERROR:" + csvEmpId); + return; + } + int startNum = ConvUtil.parseIntSafe(csvStartTime, 0); + if (startNum < CNS_TIME_1830) { + writeW03("01", "START TIME < 1830"); + return; + } + int endNum = ConvUtil.parseIntSafe(csvEndTime, 0); + if (startNum >= endNum) { + writeW03("01", "START >= END"); + return; + } + int hh1 = startNum / 100; + int mm1 = startNum % 100; + int hh2 = endNum / 100; + int mm2 = endNum % 100; + int diffMin = (hh2 - hh1) * 60 + (mm2 - mm1); + if (diffMin < CNS_DIFF_30) { + writeW03("01", "DIFF < 30MIN"); + return; + } + // W01鍑哄姏 + Zan01Rec rec = new Zan01Rec(); + rec.setApplId(csvApplId); + rec.setEmpId(csvEmpId); + rec.setApplDate(csvApplDate); + rec.setStartTime(csvStartTime); + rec.setEndTime(csvEndTime); + rec.setStatus(csvStatus); + rec.setOvtType(csvOvtType); + rec.setFiller(csvFiller); + try { + w01Out.write(rec.toRecordLine()); + w01Out.newLine(); + cunW01Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W01 ERROR", e); + } + } + + // ---- 2030CANCELSOR ---- + private void cancelProc() { + Zan01Rec rec = new Zan01Rec(); + rec.setApplId(csvApplId); + rec.setEmpId(csvEmpId); + rec.setApplDate(csvApplDate); + rec.setStartTime(csvStartTime); + rec.setEndTime(csvEndTime); + rec.setStatus(csvStatus); + rec.setOvtType(csvOvtType); + rec.setFiller(csvFiller); + try { + w02Out.write(rec.toRecordLine()); + w02Out.newLine(); + cunW02Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W02 ERROR", e); + } + } + + // ---- 2040ERRORSOR ---- + private void errorProc() { + writeW03("01", "INVALID STATUS:" + csvStatus); + } + + private void writeW03(final String category, final String detail) { + Zan05Rec rec = new Zan05Rec(); + rec.setErrCategory(category); + rec.setErrDetail(detail); + try { + w03Out.write(rec.toRecordLine()); + w03Out.newLine(); + cunW03Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W03 ERROR", e); + } + } + + // ---- 3000STPSOR ---- + private void stpProc() { + try { + if (r01In != null) { + r01In.close(); + } + if (w01Out != null) { + w01Out.close(); + } + if (w02Out != null) { + w02Out.close(); + } + if (w03Out != null) { + w03Out.close(); + } + } catch (IOException e) { + throw new RuntimeException("CLOSE ERROR", e); + } + msgInkCnt("ZAN01R01", cunR01Inn); + msgOutCnt("ZAN01W01", cunW01Out); + msgOutCnt("ZAN01W02", cunW02Out); + msgOutCnt("ZAN01W03", cunW03Out); + msgOut(CNS_MSGFIN, "", ""); + } + + // ---- 4000MSGOUTSOR ---- + private void msgOut(final int code, final String p1, final String p2) { + m00.reset(); + m00.setM00MsgCod(code); + m00.setM00Umkdats2201(p1); + m00.setM00Umkdats2202(p2); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgCompiled() { + m00.reset(); + m00.setM00MsgCod(CNS_MSGKEYINF); + m00.setM00Umkdats2201("2026-06-14"); + m00.setM00Umkdats2202("COMPILED"); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgInkCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGIINKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgOutCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGOUTKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void abend() { + e01.reset(); + e01.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(e01); + } + + /** EMPID/鏃ヤ粯绛夈伄銉併偋銉冦偗鐢ㄣ伀8妗佸彸瑭般倎(宸﹀煁銈佹枃瀛楀垪闀峰悎銈忋仜). */ + private String pad8(final String s) { + return ConvUtil.padRight(s, 8); + } +} diff --git a/JavaSrc/src/Zan02ChkMain.java b/JavaSrc/src/Zan02ChkMain.java new file mode 100644 index 0000000..5b317a9 --- /dev/null +++ b/JavaSrc/src/Zan02ChkMain.java @@ -0,0 +1,240 @@ +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.Zan01Rec; +import dto.Zan05Rec; +import dto.ZanmsgacPar; +import util.ConvUtil; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * ============================================================ + * 銆怌OBOL鈫扟ava銆慞ROGRAM-ID. ZAN02CHK 鈫?绫?Zan02ChkMain + * ============================================================ + * 鏅傞枔甯噸瑜囥儊銈с儍銈? OVT-VSORT(R01) 銇悓涓€绀惧摗銉诲悓涓€鏃ヤ粯鍐呫仹 + * 鍓嶃儸銈炽兗銉夌祩浜嗘檪鍒?> 鐝鹃枊濮嬫檪鍒?銇倝 ERROR-LOG(W02) 銇嚭鍔涖€? * 銇濄倢浠ュ銇?OVT-NODUP(W01) 銇稿嚭鍔涖€? * 鈥昏ō瑷堟浉婧栨嫚銆侰OBOL 銇ㄦ暣鍚堛€? */ +public class Zan02ChkMain { + + private static final String PRGIDX = "ZAN02CHK"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGSUBEEK = 5; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final int CNS_KN0002 = 2; + private static final int CNS_ABD999 = 999; + private static final int CNS_ERR_CAT02 = 2; + + private static final String R01_PATH = "data/ZAN02R01.txt"; + private static final String W01_PATH = "data/ZAN02W01.txt"; + private static final String W02_PATH = "data/ZAN02W02.txt"; + + private long cunR01Inn = 0; + private long cunW01Out = 0; + private long cunW02Out = 0; + + private int wrkU06; + private String wrkR01Eof = "0"; + private String r01Line = ""; + + private final ZandatacPar d01 = new ZandatacPar(); + private final ZanmsgacPar m00 = new ZanmsgacPar(); + private final ZanendacPar e01 = new ZanendacPar(); + + private Zan01Rec r01Rec = new Zan01Rec(); + private Zan01Rec prevRec = new Zan01Rec(); + + private BufferedReader r01In; + private BufferedWriter w01Out; + private BufferedWriter w02Out; + + public static void main(final String[] args) { + new Zan02ChkMain().run(); + } + + private void run() { + initProc(); + while (!"1".equals(wrkR01Eof)) { + majProc(); + } + stpProc(); + } + + private void initProc() { + msgOut(CNS_MSGSTR, "", ""); + msgCompiled(); + wrkU06 = 0; + wrkR01Eof = "0"; + d01.reset(); + Sub01DatSub.execute(d01); + if (d01.getD01FkiCod() != 0) { + msgOut(CNS_MSGSUBEEK, "SUB01DAT", String.valueOf(d01.getD01FkiCod())); + abend(); + return; + } + wrkU06 = d01.getD01UbsUdate(); + try { + r01In = FileIo.openReader(R01_PATH); + w01Out = FileIo.openWriter(W01_PATH); + w02Out = FileIo.openWriter(W02_PATH); + } catch (IOException e) { + throw new RuntimeException("FILE OPEN ERROR", e); + } + readR01(); + // 1浠剁洰: W01鍑哄姏 + 鍓嶃儸銈炽兗銉夊垵鏈熷寲 + if (!"1".equals(wrkR01Eof)) { + r01Rec.fromRecordLine(r01Line); + prevRec.fromRecordLine(r01Line); + try { + w01Out.write(r01Rec.toRecordLine()); + w01Out.newLine(); + cunW01Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W01 ERROR", e); + } + readR01(); + } + } + + private void readR01() { + try { + String line = r01In.readLine(); + if (line == null) { + wrkR01Eof = "1"; + } else { + r01Line = line; + cunR01Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ ERROR", e); + } + } + + private void majProc() { + if (!"1".equals(wrkR01Eof)) { + r01Rec.fromRecordLine(r01Line); + checkDupProc(); + readR01(); + } + } + + private void checkDupProc() { + if (prevRec.getEmpId().equals(r01Rec.getEmpId()) + && prevRec.getApplDate().equals(r01Rec.getApplDate())) { + int prevEnd = ConvUtil.parseIntSafe(prevRec.getEndTime(), 0); + int curStart = ConvUtil.parseIntSafe(r01Rec.getStartTime(), 0); + if (prevEnd > curStart) { + overlapErrProc(); + } else { + normalOutProc(); + } + } else { + keyBrProc(); + } + } + + private void overlapErrProc() { + Zan05Rec rec = new Zan05Rec(); + rec.setErrCategory(String.valueOf(CNS_ERR_CAT02)); + rec.setErrDetail("OVERLAP: " + prevRec.getEmpId() + "," + prevRec.getApplDate()); + try { + w02Out.write(rec.toRecordLine()); + w02Out.newLine(); + cunW02Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W02 ERROR", e); + } + } + + private void normalOutProc() { + try { + w01Out.write(r01Rec.toRecordLine()); + w01Out.newLine(); + cunW01Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W01 ERROR", e); + } + prevRec.fromRecordLine(r01Rec.toRecordLine()); + } + + private void keyBrProc() { + prevRec.fromRecordLine(r01Rec.toRecordLine()); + normalOutProc(); + } + + private void stpProc() { + try { + if (r01In != null) { + r01In.close(); + } + if (w01Out != null) { + w01Out.close(); + } + if (w02Out != null) { + w02Out.close(); + } + } catch (IOException e) { + throw new RuntimeException("CLOSE ERROR", e); + } + msgInkCnt("ZAN02R01", cunR01Inn); + msgOutCnt("ZAN02W01", cunW01Out); + msgOutCnt("ZAN02W02", cunW02Out); + msgOut(CNS_MSGFIN, "", ""); + } + + private void msgOut(final int code, final String p1, final String p2) { + m00.reset(); + m00.setM00MsgCod(code); + m00.setM00Umkdats2201(p1); + m00.setM00Umkdats2202(p2); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgCompiled() { + m00.reset(); + m00.setM00MsgCod(CNS_MSGKEYINF); + m00.setM00Umkdats2201("2026-06-14"); + m00.setM00Umkdats2202("COMPILED"); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgInkCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGIINKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgOutCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGOUTKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void abend() { + e01.reset(); + e01.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(e01); + } +} diff --git a/JavaSrc/src/Zan03ChkMain.java b/JavaSrc/src/Zan03ChkMain.java new file mode 100644 index 0000000..f4be729 --- /dev/null +++ b/JavaSrc/src/Zan03ChkMain.java @@ -0,0 +1,337 @@ +import dto.Zan01Rec; +import dto.Zan05Rec; +import dto.ZanmsgacPar; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * ============================================================ + * 銆怌OBOL鈫扟ava銆慞ROGRAM-ID. ZAN03CHK 鈫?绫?Zan03ChkMain + * ============================================================ + * 鎵撳埢鏅傞枔鐓у悎: OVT-NODUP(R01) 銇?PUNCH(R02) 銈掋偔銉?绀惧摗+鐢宠珛鏃?銇х獊鍚堛€? * 涓€鑷淬仐鏅傞枔甯亴鍑哄嫟銆滈€€鍕ょ瘎鍥插唴 鈫?OVT-TYPE浠樺姞銇椼仸 OVT-CHECKED(W01)銆? * 绡勫洸澶栥伨銇熴伅涓嶄竴鑷?鈫?ERROR-LOG(W02) 銈儐銈淬儶03銆? * 浼戞棩鍒ゅ畾銇?R03(浼戞棩銉曘偂銈ゃ儷) 銇ц銇嗐€? * 鈥籞AN03 銇?SUB01DAT 鏈娇鐢?FUNCTION CURRENT-DATE)銆傝ō瑷堟浉婧栨嫚銆? */ +public class Zan03ChkMain { + + private static final String PRGIDX = "ZAN03CHK"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final int CNS_KN0002 = 2; + private static final int CNS_ERR_CAT03 = 3; + private static final String OVT_W = "W"; + private static final String OVT_H = "H"; + private static final String HIGH = "zzzzzzzzzzzzzzzz"; // 16脳'z' = HIGH-VALUE + + private static final String R01_PATH = "data/ZAN03R01.txt"; + private static final String R02_PATH = "data/ZAN03R02.txt"; + private static final String R03_PATH = "data/ZAN03R03.txt"; + private static final String W01_PATH = "data/ZAN03W01.txt"; + private static final String W02_PATH = "data/ZAN03W02.txt"; + + private long cunR01Inn = 0; + private long cunR02Inn = 0; + private long cunR03Inn = 0; + private long cunW01Out = 0; + private long cunW02Out = 0; + + private String wrkR01Eof = "0"; + private String wrkR02Eof = "0"; + private String wrkR03Eof = "0"; + private String r01Line = ""; + private String r02Line = ""; + + private String wrkR01Key = ""; + private String wrkR02Key = HIGH; + private String wrkOvtTypeSet = OVT_W; + + private final ZanmsgacPar m00 = new ZanmsgacPar(); + + private Zan01Rec r01Rec = new Zan01Rec(); + + // R02 銈ゃ兂銉┿偆銉抽爡鐩? // R02 インライン項目 + private String r02EmpId = ""; + private String r02Date = ""; + private String r02TimeIn = ""; + private String r02TimeOut = ""; + + // 浼戞棩銉嗐兗銉栥儷 + private final List holidayTable = new ArrayList<>(); + + private BufferedReader r01In; + private BufferedReader r02In; + private BufferedReader r03In; + private BufferedWriter w01Out; + private BufferedWriter w02Out; + + private static final class Holiday { + final String date; + final String flg; + Holiday(final String d, final String f) { + this.date = d; + this.flg = f; + } + } + + public static void main(final String[] args) { + new Zan03ChkMain().run(); + } + + private void run() { + initProc(); + while (!"1".equals(wrkR01Eof)) { + majProc(); + } + stpProc(); + } + + private void initProc() { + msgOut(CNS_MSGSTR, "", ""); + msgCompiled(); + wrkR01Eof = "0"; + wrkR02Eof = "0"; + wrkR03Eof = "0"; + try { + r01In = FileIo.openReader(R01_PATH); + r02In = FileIo.openReader(R02_PATH); + r03In = FileIo.openReader(R03_PATH); + w01Out = FileIo.openWriter(W01_PATH); + w02Out = FileIo.openWriter(W02_PATH); + } catch (IOException e) { + throw new RuntimeException("FILE OPEN ERROR", e); + } + // 浼戞棩銉曘偂銈ゃ儷瑾炯 + holidayLoad(); + readR01(); + readR02(); + } + + private void holidayLoad() { + try { + String line; + while ((line = r03In.readLine()) != null) { + cunR03Inn++; + String date = line.length() >= 8 ? line.substring(0, 8).trim() : ""; + String flg = line.length() >= 9 ? line.substring(8, 9) : " "; + holidayTable.add(new String[]{date, flg}); + } + wrkR03Eof = "1"; + } catch (IOException e) { + throw new RuntimeException("READ R03 ERROR", e); + } + } + + private void readR01() { + try { + String line = r01In.readLine(); + if (line == null) { + wrkR01Eof = "1"; + } else { + r01Line = line; + r01Rec.fromRecordLine(line); + wrkR01Key = pad8(r01Rec.getEmpId()) + pad8(r01Rec.getApplDate()); + cunR01Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ R01 ERROR", e); + } + } + + private void readR02() { + try { + String line = r02In.readLine(); + if (line == null) { + wrkR02Eof = "1"; + wrkR02Key = HIGH; + } else { + r02Line = line; + r02EmpId = line.length() >= 8 ? line.substring(0, 8).trim() : ""; + r02Date = line.length() >= 16 ? line.substring(8, 16).trim() : ""; + r02TimeIn = line.length() >= 20 ? line.substring(16, 20).trim() : ""; + r02TimeOut = line.length() >= 24 ? line.substring(20, 24).trim() : ""; + wrkR02Key = pad8(r02EmpId) + pad8(r02Date); + cunR02Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ R02 ERROR", e); + } + } + + private void majProc() { + int cmp = wrkR01Key.compareTo(wrkR02Key); + if (cmp < 0) { + noMatchProc(); + readR01(); + } else if (cmp == 0) { + matchProc(); + readR01(); + } else { + readR02(); + } + } + + private void noMatchProc() { + Zan05Rec rec = new Zan05Rec(); + rec.setErrCategory(String.valueOf(CNS_ERR_CAT03)); + rec.setErrDetail(r01Rec.getEmpId() + "/" + r01Rec.getApplDate()); + try { + w02Out.write(rec.toRecordLine()); + w02Out.newLine(); + cunW02Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W02 ERROR", e); + } + } + + private void matchProc() { + int r01Start = parseInt(r01Rec.getStartTime(), 0); + int r01End = parseInt(r01Rec.getEndTime(), 0); + int punchIn = parseInt(r02TimeIn, 0); + int punchOut = parseInt(r02TimeOut, 0); + if (r01Start >= punchIn && r01End <= punchOut) { + setOvtProc(); + try { + w01Out.write(r01Rec.toRecordLine()); + w01Out.newLine(); + cunW01Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W01 ERROR", e); + } + } else { + Zan05Rec rec = new Zan05Rec(); + rec.setErrCategory(String.valueOf(CNS_ERR_CAT03)); + rec.setErrDetail(r01Rec.getEmpId() + "/" + r01Rec.getApplDate() + "/" + + r01Rec.getStartTime() + "-" + r01Rec.getEndTime()); + try { + w02Out.write(rec.toRecordLine()); + w02Out.newLine(); + cunW02Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W02 ERROR", e); + } + } + } + + private void setOvtProc() { + wrkOvtTypeSet = OVT_W; + String date = r01Rec.getApplDate(); + for (String[] h : holidayTable) { + if (h[0].equals(date)) { + // 休日フラグ: COBOL データ慣習は '1'、設計上は 'H' も可。両方受容して運用互換を確保。 + if (OVT_H.equals(h[1]) || "1".equals(h[1])) { + wrkOvtTypeSet = OVT_H; + } + break; + } + } + r01Rec.setOvtType(wrkOvtTypeSet); + } + + private void stpProc() { + try { + if (r01In != null) { + r01In.close(); + } + if (r02In != null) { + r02In.close(); + } + if (r03In != null) { + r03In.close(); + } + if (w01Out != null) { + w01Out.close(); + } + if (w02Out != null) { + w02Out.close(); + } + } catch (IOException e) { + throw new RuntimeException("CLOSE ERROR", e); + } + msgInkCnt("ZAN03R01", cunR01Inn); + msgInkCnt("ZAN03R02", cunR02Inn); + msgInkCnt("ZAN03R03", cunR03Inn); + msgOutCnt("ZAN03W01", cunW01Out); + msgOutCnt("ZAN03W02", cunW02Out); + msgOut(CNS_MSGFIN, "", ""); + } + + private void msgOut(final int code, final String p1, final String p2) { + m00.reset(); + m00.setM00MsgCod(code); + m00.setM00Umkdats2201(p1); + m00.setM00Umkdats2202(p2); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgCompiled() { + m00.reset(); + m00.setM00MsgCod(CNS_MSGKEYINF); + m00.setM00Umkdats2201("2026-06-14"); + m00.setM00Umkdats2202("COMPILED"); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgInkCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGIINKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgOutCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGOUTKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private String pad8(final String s) { + if (s == null) { + return " "; + } + if (s.length() >= 8) { + return s.substring(0, 8); + } + StringBuilder sb = new StringBuilder(s); + while (sb.length() < 8) { + sb.append(' '); + } + return sb.toString(); + } + + private int parseInt(final String s, final int def) { + if (s == null) { + return def; + } + String t = s.trim(); + if (t.isEmpty()) { + return def; + } + try { + return Integer.parseInt(t); + } catch (NumberFormatException e) { + return def; + } + } +} diff --git a/JavaSrc/src/Zan04MatMain.java b/JavaSrc/src/Zan04MatMain.java new file mode 100644 index 0000000..f5a562a --- /dev/null +++ b/JavaSrc/src/Zan04MatMain.java @@ -0,0 +1,277 @@ +import dto.Zan01Rec; +import dto.Zan02Rec; +import dto.Zan04Rec; +import dto.Zan05Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanmsgacPar; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * ============================================================ + * 銆怌OBOL鈫扟ava銆慞ROGRAM-ID. ZAN04MAT 鈫?绫?Zan04MatMain + * ============================================================ + * 鍙栨秷銉炪儍銉併兂銈? OVT-SORTED(R01) 銇?OVT-CSORT(R02) 銈掔敵璜嬬暘鍙?APPL-ID)銇? * 1:1 銉炪儍銉併兂銈般仐銆佺祼鏋溿倰鎸倞鍒嗐亼銈嬨€? * 涓€鑷? 鈫?鍙栨秷娓堛伩鐢宠珛銈?ERROR-LOG(W03) 銇嚭鍔涳紙鐩f熁瑷艰贰锛? * R01銇伩 鈫?鏈夊姽鐢宠珛銈?OVT-MATCHED(W01) 銇嚭鍔? * R02銇伩 鈫?鍙栨秷鐢宠珛銈?OVT-DBCLEAN(W02) 銇嚭鍔? * 鈥昏ō瑷堟浉婧栨嫚銆侰OBOL 銇ㄦ暣鍚堛€傘偔銉笺伅 APPL-ID (8B)銆? */ +public class Zan04MatMain { + + private static final String PRGIDX = "ZAN04MAT"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGSUBEEK = 5; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final int CNS_KN0002 = 2; + private static final int CNS_ABD999 = 999; + private static final int CNS_ERR_CAT04 = 4; + private static final int CNS_PROC_SEQ01 = 1; + private static final String HIGH = "zzzzzzzz"; // 8脳'z' = HIGH-VALUE + + private static final String R01_PATH = "data/ZAN04R01.txt"; + private static final String R02_PATH = "data/ZAN04R02.txt"; + private static final String W01_PATH = "data/ZAN04W01.txt"; + private static final String W02_PATH = "data/ZAN04W02.txt"; + private static final String W03_PATH = "data/ZAN04W03.txt"; + + private long cunR01Inn = 0; + private long cunR02Inn = 0; + private long cunW01Out = 0; + private long cunW02Out = 0; + private long cunW03Out = 0; + + private String r01Key = HIGH; + private String r02Key = HIGH; + + private final ZandatacPar d01 = new ZandatacPar(); + private final ZanmsgacPar m00 = new ZanmsgacPar(); + private final ZanendacPar e01 = new ZanendacPar(); + + private Zan01Rec r01Rec = new Zan01Rec(); + private Zan01Rec r02Rec = new Zan01Rec(); + + private BufferedReader r01In; + private BufferedReader r02In; + private BufferedWriter w01Out; + private BufferedWriter w02Out; + private BufferedWriter w03Out; + + public static void main(final String[] args) { + new Zan04MatMain().run(); + } + + private void run() { + initProc(); + while (!(HIGH.equals(r01Key) && HIGH.equals(r02Key))) { + int cmp = r01Key.compareTo(r02Key); + if (cmp == 0) { + matchProc(); + readR01(); + readR02(); + } else if (cmp < 0) { + r01OutProc(); + readR01(); + } else { + r02OutProc(); + readR02(); + } + } + stpProc(); + } + + private void initProc() { + msgOut(CNS_MSGSTR, "", ""); + msgCompiled(); + d01.reset(); + Sub01DatSub.execute(d01); + if (d01.getD01FkiCod() != 0) { + msgOut(CNS_MSGSUBEEK, "SUB01DAT", String.valueOf(d01.getD01FkiCod())); + abend(); + return; + } + try { + r01In = FileIo.openReader(R01_PATH); + r02In = FileIo.openReader(R02_PATH); + w01Out = FileIo.openWriter(W01_PATH); + w02Out = FileIo.openWriter(W02_PATH); + w03Out = FileIo.openWriter(W03_PATH); + } catch (IOException e) { + throw new RuntimeException("FILE OPEN ERROR", e); + } + readR01(); + readR02(); + } + + private void readR01() { + try { + String line = r01In.readLine(); + if (line == null) { + r01Key = HIGH; + } else { + r01Rec.fromRecordLine(line); + r01Key = pad8(r01Rec.getApplId()); + cunR01Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ R01 ERROR", e); + } + } + + private void readR02() { + try { + String line = r02In.readLine(); + if (line == null) { + r02Key = HIGH; + } else { + r02Rec.fromRecordLine(line); + r02Key = pad8(r02Rec.getApplId()); + cunR02Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ R02 ERROR", e); + } + } + + private void matchProc() { + Zan05Rec rec = new Zan05Rec(); + rec.setErrCategory(String.valueOf(CNS_ERR_CAT04)); + rec.setErrDetail("CANCEL-MATCH: " + r01Rec.getApplId() + " " + + r01Rec.getEmpId() + " " + r01Rec.getApplDate() + " " + + r01Rec.getStartTime() + " " + r01Rec.getEndTime()); + try { + w03Out.write(rec.toRecordLine()); + w03Out.newLine(); + cunW03Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W03 ERROR", e); + } + } + + private void r01OutProc() { + Zan02Rec rec = new Zan02Rec(); + rec.setApplId(r01Rec.getApplId()); + rec.setEmpId(r01Rec.getEmpId()); + rec.setApplDate(r01Rec.getApplDate()); + rec.setStartTime(r01Rec.getStartTime()); + rec.setEndTime(r01Rec.getEndTime()); + rec.setStatus(r01Rec.getStatus()); + rec.setOvtType(r01Rec.getOvtType()); + rec.setProcSeq(String.valueOf(CNS_PROC_SEQ01)); + try { + w01Out.write(rec.toRecordLine()); + w01Out.newLine(); + cunW01Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W01 ERROR", e); + } + } + + private void r02OutProc() { + Zan04Rec rec = new Zan04Rec(); + rec.setApplId(r02Rec.getApplId()); + try { + w02Out.write(rec.toRecordLine()); + w02Out.newLine(); + cunW02Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W02 ERROR", e); + } + } + + private void stpProc() { + try { + if (r01In != null) { + r01In.close(); + } + if (r02In != null) { + r02In.close(); + } + if (w01Out != null) { + w01Out.close(); + } + if (w02Out != null) { + w02Out.close(); + } + if (w03Out != null) { + w03Out.close(); + } + } catch (IOException e) { + throw new RuntimeException("CLOSE ERROR", e); + } + msgInkCnt("ZAN04R01", cunR01Inn); + msgInkCnt("ZAN04R02", cunR02Inn); + msgOutCnt("ZAN04W01", cunW01Out); + msgOutCnt("ZAN04W02", cunW02Out); + msgOutCnt("ZAN04W03", cunW03Out); + msgOut(CNS_MSGFIN, "", ""); + } + + private void msgOut(final int code, final String p1, final String p2) { + m00.reset(); + m00.setM00MsgCod(code); + m00.setM00Umkdats2201(p1); + m00.setM00Umkdats2202(p2); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgCompiled() { + m00.reset(); + m00.setM00MsgCod(CNS_MSGKEYINF); + m00.setM00Umkdats2201("2026-06-15"); + m00.setM00Umkdats2202("COMPILED"); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgInkCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGIINKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgOutCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGOUTKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void abend() { + e01.reset(); + e01.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(e01); + } + + private String pad8(final String s) { + if (s == null) { + return " "; + } + if (s.length() >= 8) { + return s.substring(0, 8); + } + StringBuilder sb = new StringBuilder(s); + while (sb.length() < 8) { + sb.append(' '); + } + return sb.toString(); + } +} diff --git a/JavaSrc/src/Zan05CalMain.java b/JavaSrc/src/Zan05CalMain.java new file mode 100644 index 0000000..b6baf9b --- /dev/null +++ b/JavaSrc/src/Zan05CalMain.java @@ -0,0 +1,275 @@ +import dto.Zan02Rec; +import dto.Zan03Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanmsgacPar; +import dto.ZantimacPar; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * ============================================================ + * 銆怌OBOL鈫扟ava銆慞ROGRAM-ID. ZAN05CAL 鈫?绫?Zan05CalMain + * ============================================================ + * 娈嬫キ鏅傞枔闆嗚▓: OVT-SORTED2(R01, APPL-ID+PROC-SEQ 鏄囬爢) 銈掋偔銉笺儢銉偆銈泦瑷堛仐銆? * 鍚屼竴 APPL-ID 鍐呫伄鍏ㄦ槑绱般伄鍔犵彮鏅傞枔銈掔绠椼仐銇?OVT-SUMMARY(W01) 銇?1 銉偝銉笺儔鍑哄姏銆? * 绌嶇畻鍒嗏啋鏅傚埢涓搞倎銇?SUB05TIM(mode=2: 0.1h 鍗樹綅鍒囨崹) 銈掍娇鐢ㄣ€? * 鈥昏ō瑷堟浉婧栨嫚銆侰OBOL 銇ㄦ暣鍚堛€? */ +public class Zan05CalMain { + + private static final String PRGIDX = "ZAN05CAL"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGSUBEEK = 5; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final int CNS_KN0002 = 2; + private static final int CNS_TIMMODE02 = 2; + private static final int CNS_ABD999 = 999; + private static final String HIGH = "zzzzzzzz"; // 8脳'z' = HIGH-VALUE + + private static final String R01_PATH = "data/ZAN05R01.txt"; + private static final String W01_PATH = "data/ZAN05W01.txt"; + + private long cunR01Inn = 0; + private long cunW01Out = 0; + + private String r01Key = HIGH; + private String prevApplId = ""; + private boolean groupActive = false; + private String groupStart = ""; + private long accumMin = 0; + + private String lastApplId = ""; + private String lastEmpId = ""; + private String lastApplDate = ""; + private String lastEnd = ""; + private String lastOvtType = ""; + + private final ZandatacPar d01 = new ZandatacPar(); + private final ZanmsgacPar m00 = new ZanmsgacPar(); + private final ZanendacPar e01 = new ZanendacPar(); + + private Zan02Rec r01Rec = new Zan02Rec(); + + private BufferedReader r01In; + private BufferedWriter w01Out; + + public static void main(final String[] args) { + new Zan05CalMain().run(); + } + + private void run() { + initProc(); + while (!(HIGH.equals(r01Key) && !groupActive)) { + if (HIGH.equals(r01Key)) { + if (groupActive) { + outGroupProc(); + groupActive = false; + } + } else if (!r01Key.equals(prevApplId)) { + if (groupActive) { + outGroupProc(); + } + prevApplId = r01Rec.getApplId(); + lastApplId = r01Rec.getApplId(); + lastEmpId = r01Rec.getEmpId(); + lastApplDate = r01Rec.getApplDate(); + lastEnd = r01Rec.getEndTime(); + lastOvtType = r01Rec.getOvtType(); + groupStart = r01Rec.getStartTime(); + groupActive = true; + accumProc(); + readR01(); + } else { + accumProc(); + lastApplId = r01Rec.getApplId(); + lastEmpId = r01Rec.getEmpId(); + lastApplDate = r01Rec.getApplDate(); + lastEnd = r01Rec.getEndTime(); + lastOvtType = r01Rec.getOvtType(); + readR01(); + } + } + stpProc(); + } + + private void initProc() { + msgOut(CNS_MSGSTR, "", ""); + msgCompiled(); + d01.reset(); + Sub01DatSub.execute(d01); + if (d01.getD01FkiCod() != 0) { + msgOut(CNS_MSGSUBEEK, "SUB01DAT", String.valueOf(d01.getD01FkiCod())); + abend(); + return; + } + try { + r01In = FileIo.openReader(R01_PATH); + w01Out = FileIo.openWriter(W01_PATH); + } catch (IOException e) { + throw new RuntimeException("FILE OPEN ERROR", e); + } + readR01(); + if (!HIGH.equals(r01Key)) { + prevApplId = r01Rec.getApplId(); + lastApplId = r01Rec.getApplId(); + lastEmpId = r01Rec.getEmpId(); + lastApplDate = r01Rec.getApplDate(); + lastEnd = r01Rec.getEndTime(); + lastOvtType = r01Rec.getOvtType(); + groupStart = r01Rec.getStartTime(); + groupActive = true; + } + } + + private void readR01() { + try { + String line = r01In.readLine(); + if (line == null) { + r01Key = HIGH; + } else { + r01Rec.fromRecordLine(line); + r01Key = pad8(r01Rec.getApplId()); + cunR01Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ R01 ERROR", e); + } + } + + private void accumProc() { + int start = parseInt(r01Rec.getStartTime(), 0); + int end = parseInt(r01Rec.getEndTime(), 0); + int sh = start / 100; + int sm = start % 100; + int eh = end / 100; + int em = end % 100; + int diff = (eh - sh) * 60 + (em - sm); + accumMin += diff; + } + + private void outGroupProc() { + double tempHours = accumMin / 60.0; + ZantimacPar t = new ZantimacPar(); + t.setT01Timhrs(tempHours); + t.setT01Timrrc(CNS_TIMMODE02); + Sub05TimSub.execute(t); + double outHours = t.getT01Timout(); + + Zan03Rec rec = new Zan03Rec(); + rec.setApplId(lastApplId); + rec.setEmpId(lastEmpId); + rec.setApplDate(lastApplDate); + rec.setStartTime(groupStart); + rec.setEndTime(lastEnd); + rec.setOvtHoursFromDouble(outHours); + rec.setOvtType(lastOvtType); + try { + w01Out.write(rec.toRecordLine()); + w01Out.newLine(); + cunW01Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W01 ERROR", e); + } + accumMin = 0; + } + + private void stpProc() { + try { + if (r01In != null) { + r01In.close(); + } + if (w01Out != null) { + w01Out.close(); + } + } catch (IOException e) { + throw new RuntimeException("CLOSE ERROR", e); + } + msgInkCnt("ZAN05R01", cunR01Inn); + msgOutCnt("ZAN05W01", cunW01Out); + msgOut(CNS_MSGFIN, "", ""); + } + + private void msgOut(final int code, final String p1, final String p2) { + m00.reset(); + m00.setM00MsgCod(code); + m00.setM00Umkdats2201(p1); + m00.setM00Umkdats2202(p2); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgCompiled() { + m00.reset(); + m00.setM00MsgCod(CNS_MSGKEYINF); + m00.setM00Umkdats2201("2026-06-15"); + m00.setM00Umkdats2202("COMPILED"); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgInkCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGIINKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgOutCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGOUTKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void abend() { + e01.reset(); + e01.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(e01); + } + + private String pad8(final String s) { + if (s == null) { + return " "; + } + if (s.length() >= 8) { + return s.substring(0, 8); + } + StringBuilder sb = new StringBuilder(s); + while (sb.length() < 8) { + sb.append(' '); + } + return sb.toString(); + } + + private int parseInt(final String s, final int def) { + if (s == null) { + return def; + } + String t = s.trim(); + if (t.isEmpty()) { + return def; + } + try { + return Integer.parseInt(t); + } catch (NumberFormatException e) { + return def; + } + } +} diff --git a/JavaSrc/src/Zan06UpdMain.java b/JavaSrc/src/Zan06UpdMain.java new file mode 100644 index 0000000..d4f213d --- /dev/null +++ b/JavaSrc/src/Zan06UpdMain.java @@ -0,0 +1,502 @@ +import dto.Zan03Rec; +import dto.Zan04Rec; +import dto.Zan05Rec; +import dto.ZandatacPar; +import dto.ZanendacPar; +import dto.ZanmsgacPar; + +import util.ConvUtil; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * ============================================================ + * 銆怌OBOL鈫扟ava銆慞ROGRAM-ID. ZAN06UPD 鈫?绫?Zan06UpdMain + * ============================================================ + * 娈嬫キ绲辫▓DB鏇存柊: OVT-SUMMARY(R01) 銈?OVT_APPLICATIONS 銇?INSERT(閲嶈鏅?UPDATE) + * 銇椼€丱VT-MONTHLY 銇湀娆¢泦瑷堛倰鍙嶆槧銆侽VT-DBCLEAN(R02) 銇┎褰撶敵璜嬨倰鍙栨秷鐘舵厠銇? * 鏇存柊銇椼€丱VT-MONTHLY 銇嬨倝鍔犵彮鏅傞枔銈掓笡绠椼仚銈嬨€? * 鎺ョ稓鍏? data/OVERTIME.DB (SQLite)銆傘儐銉笺儢銉笉鍦ㄦ檪銇?CREATE TABLE IF NOT EXISTS 銇ц嚜鍕曟绡夈€? * 鈥昏ō瑷堟浉婧栨嫚銆侰OBOL 銇ㄦ暣鍚堛€? */ +public class Zan06UpdMain { + + private static final String PRGIDX = "ZAN06UPD"; + private static final int CNS_MSGSTR = 1; + private static final int CNS_MSGFIN = 2; + private static final int CNS_MSGSUBEEK = 5; + private static final int CNS_MSGIINKES = 6; + private static final int CNS_MSGOUTKES = 7; + private static final int CNS_MSGKEYINF = 33; + private static final int CNS_ABD999 = 999; + private static final int CNS_KN0002 = 2; + private static final int CNS_COMMIT_CNT = 50; + private static final String CNS_STATUS_ACTIVE = "0"; + private static final String CNS_STATUS_CANCEL = "9"; + private static final String DB_URL = "jdbc:sqlite:data/OVERTIME.DB"; + + private static final String R01_PATH = "data/ZAN06R01.txt"; + private static final String R02_PATH = "data/ZAN06R02.txt"; + private static final String W01_PATH = "data/ZAN06W01.txt"; + + private long cunR01Inn = 0; + private long cunR02Inn = 0; + private long cunW01Out = 0; + private long cunDbxIns = 0; + private long cunDbxUpd = 0; + private long cunCommit = 0; + + private boolean r01Eof = false; + private boolean r02Eof = false; + + private String wrkApplDateN = ""; + private String wrkYearMonth = ""; + private String wrkMonth = ""; + private int wrkMonthValid = 0; + private String wrkSqlApplId = ""; + private String wrkSqlEmpId = ""; + private String wrkSqlApplDate = ""; + private String wrkSqlYearMonth = ""; + private String wrkSqlStartTime = ""; + private String wrkSqlEndTime = ""; + private BigDecimal wrkSqlOvtHours = BigDecimal.ZERO; + private String wrkSqlOvtType = ""; + private String wrkSqlStatus = ""; + private int sqlcode = 0; + private int wrkErrCategory = 0; + private String wrkErrDetail = ""; + + private final ZandatacPar d01 = new ZandatacPar(); + private final ZanmsgacPar m00 = new ZanmsgacPar(); + private final ZanendacPar e01 = new ZanendacPar(); + + private Zan03Rec r01Rec = new Zan03Rec(); + private Zan04Rec r02Rec = new Zan04Rec(); + + private BufferedReader r01In; + private BufferedReader r02In; + private BufferedWriter w01Out; + private Connection conn; + + public static void main(final String[] args) { + new Zan06UpdMain().run(); + } + + private void run() { + initProc(); + while (!(r01Eof && r02Eof)) { + if (!r01Eof) { + summaryProc(); + readR01(); + } else if (!r02Eof) { + dbCleanProc(); + readR02(); + } + } + stpProc(); + } + + private void initProc() { + msgOut(CNS_MSGSTR, "", ""); + msgCompiled(); + d01.reset(); + Sub01DatSub.execute(d01); + if (d01.getD01FkiCod() != 0) { + msgOut(CNS_MSGSUBEEK, "SUB01DAT", String.valueOf(d01.getD01FkiCod())); + abend(); + return; + } + try { + r01In = FileIo.openReader(R01_PATH); + r02In = FileIo.openReader(R02_PATH); + w01Out = FileIo.openWriter(W01_PATH); + } catch (IOException e) { + throw new RuntimeException("FILE OPEN ERROR", e); + } + try { + Class.forName("org.sqlite.JDBC"); + conn = DriverManager.getConnection(DB_URL); + conn.setAutoCommit(false); + createTables(); + } catch (Exception e) { + throw new RuntimeException("DB CONNECT ERROR: " + e.getMessage(), e); + } + readR01(); + readR02(); + } + + private void createTables() throws SQLException { + try (PreparedStatement ps = conn.prepareStatement( + "CREATE TABLE IF NOT EXISTS OVT_APPLICATIONS (" + + "APPL_ID CHAR(8) NOT NULL PRIMARY KEY, EMP_ID CHAR(8) NOT NULL, " + + "APPL_DATE CHAR(8) NOT NULL, OVT_TYPE CHAR(1) NOT NULL, " + + "START_TIME CHAR(4) NOT NULL, END_TIME CHAR(4) NOT NULL, " + + "OVT_HOURS DECIMAL(4,1) NOT NULL, STATUS CHAR(1) NOT NULL, " + + "UPDATED_AT TIMESTAMP NOT NULL)")) { + ps.executeUpdate(); + } + try (PreparedStatement ps = conn.prepareStatement( + "CREATE TABLE IF NOT EXISTS OVT_MONTHLY (" + + "EMP_ID CHAR(8) NOT NULL, YEAR_MONTH CHAR(6) NOT NULL, " + + "OVT_TYPE CHAR(1) NOT NULL, OVT_HOURS DECIMAL(6,1) NOT NULL, " + + "OVT_COUNT INTEGER NOT NULL, UPDATED_AT TIMESTAMP NOT NULL, " + + "PRIMARY KEY (EMP_ID, YEAR_MONTH, OVT_TYPE))")) { + ps.executeUpdate(); + } + } + + private void readR01() { + try { + String line = r01In.readLine(); + if (line == null) { + r01Eof = true; + } else { + r01Rec.fromRecordLine(line); + cunR01Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ R01 ERROR", e); + } + } + + private void readR02() { + try { + String line = r02In.readLine(); + if (line == null) { + r02Eof = true; + } else { + r02Rec.fromRecordLine(line); + cunR02Inn++; + } + } catch (IOException e) { + throw new RuntimeException("READ R02 ERROR", e); + } + } + + // 2100SUMMARYSOR + private void summaryProc() { + wrkSqlApplId = r01Rec.getApplId(); + wrkSqlEmpId = r01Rec.getEmpId(); + wrkSqlApplDate = r01Rec.getApplDate(); + wrkApplDateN = r01Rec.getApplDate(); + wrkMonth = wrkApplDateN.length() >= 6 ? wrkApplDateN.substring(4, 6) : ""; + wrkMonthValid = 0; + for (int idx = 1; idx <= 12 && wrkMonthValid == 0; idx++) { + if (String.format("%02d", idx).equals(wrkMonth)) { + wrkMonthValid = 1; + } + } + if (wrkMonthValid == 0) { + wrkErrCategory = 10; + wrkErrDetail = "INVALID MONTH APPL-ID=" + wrkSqlApplId; + writeW01(wrkErrCategory, wrkErrDetail); + abend(); + return; + } + wrkYearMonth = wrkApplDateN.length() >= 6 ? wrkApplDateN.substring(0, 6) : ""; + wrkSqlYearMonth = wrkYearMonth; + wrkSqlStartTime = r01Rec.getStartTime(); + wrkSqlEndTime = r01Rec.getEndTime(); + wrkSqlOvtHours = parseOvt(r01Rec.getOvtHours()); + wrkSqlOvtType = r01Rec.getOvtType(); + wrkSqlStatus = CNS_STATUS_ACTIVE; + + String insSql = "INSERT INTO OVT_APPLICATIONS " + + "(APPL_ID, EMP_ID, APPL_DATE, OVT_TYPE, START_TIME, END_TIME, " + + "OVT_HOURS, STATUS, UPDATED_AT) VALUES (?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)"; + try (PreparedStatement ps = conn.prepareStatement(insSql)) { + ps.setString(1, wrkSqlApplId); + ps.setString(2, wrkSqlEmpId); + ps.setString(3, wrkSqlApplDate); + ps.setString(4, wrkSqlOvtType); + ps.setString(5, wrkSqlStartTime); + ps.setString(6, wrkSqlEndTime); + ps.setBigDecimal(7, wrkSqlOvtHours); + ps.setString(8, wrkSqlStatus); + ps.executeUpdate(); + cunDbxIns++; + } catch (SQLException ex) { + sqlcode = ex.getErrorCode(); + String updSql = "UPDATE OVT_APPLICATIONS SET STATUS=?, UPDATED_AT=CURRENT_TIMESTAMP " + + "WHERE APPL_ID=?"; + try (PreparedStatement ps = conn.prepareStatement(updSql)) { + ps.setString(1, wrkSqlStatus); + ps.setString(2, wrkSqlApplId); + ps.executeUpdate(); + } catch (SQLException ex2) { + dbErr(); + return; + } + cunDbxUpd++; + } + cunCommit++; + monthlyUpsert(); + if (cunCommit >= CNS_COMMIT_CNT) { + commitDb(); + } + } + + // 2110MONTHLYUPSOR + private void monthlyUpsert() { + String selSql = "SELECT OVT_HOURS, OVT_COUNT FROM OVT_MONTHLY " + + "WHERE EMP_ID=? AND YEAR_MONTH=? AND OVT_TYPE=?"; + try (PreparedStatement ps = conn.prepareStatement(selSql)) { + ps.setString(1, wrkSqlEmpId); + ps.setString(2, wrkSqlYearMonth); + ps.setString(3, wrkSqlOvtType); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + String updSql = "UPDATE OVT_MONTHLY SET OVT_HOURS=OVT_HOURS + ?, " + + "OVT_COUNT=OVT_COUNT + 1, UPDATED_AT=CURRENT_TIMESTAMP " + + "WHERE EMP_ID=? AND YEAR_MONTH=? AND OVT_TYPE=?"; + try (PreparedStatement ups = conn.prepareStatement(updSql)) { + ups.setBigDecimal(1, wrkSqlOvtHours); + ups.setString(2, wrkSqlEmpId); + ups.setString(3, wrkSqlYearMonth); + ups.setString(4, wrkSqlOvtType); + ups.executeUpdate(); + } + } else { + String insSql = "INSERT INTO OVT_MONTHLY " + + "(EMP_ID, YEAR_MONTH, OVT_TYPE, OVT_HOURS, OVT_COUNT, UPDATED_AT) " + + "VALUES (?,?,?,?,1,CURRENT_TIMESTAMP)"; + try (PreparedStatement ins = conn.prepareStatement(insSql)) { + ins.setString(1, wrkSqlEmpId); + ins.setString(2, wrkSqlYearMonth); + ins.setString(3, wrkSqlOvtType); + ins.setBigDecimal(4, wrkSqlOvtHours); + ins.executeUpdate(); + } + } + } + } catch (SQLException ex) { + sqlcode = ex.getErrorCode(); + dbErr(); + } + } + + // 2200DBCLEANSOR + private void dbCleanProc() { + wrkSqlApplId = r02Rec.getApplId(); + String selSql = "SELECT EMP_ID, APPL_DATE, OVT_TYPE, OVT_HOURS FROM OVT_APPLICATIONS " + + "WHERE APPL_ID=?"; + boolean found = false; + try (PreparedStatement ps = conn.prepareStatement(selSql)) { + ps.setString(1, wrkSqlApplId); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + found = true; + wrkSqlEmpId = rs.getString(1); + wrkSqlApplDate = rs.getString(2); + wrkSqlOvtType = rs.getString(3); + wrkSqlOvtHours = rs.getBigDecimal(4); + if (wrkSqlOvtHours == null) { + wrkSqlOvtHours = BigDecimal.ZERO; + } + } + } + } catch (SQLException ex) { + sqlcode = ex.getErrorCode(); + dbErr(); + return; + } + if (!found) { + wrkErrCategory = 20; + wrkErrDetail = "ORPHAN CANCEL APPL-ID=" + wrkSqlApplId; + writeW01(wrkErrCategory, wrkErrDetail); + abend(); + return; + } + wrkYearMonth = wrkSqlApplDate.length() >= 6 ? wrkSqlApplDate.substring(0, 6) : ""; + wrkSqlYearMonth = wrkYearMonth; + String updSql = "UPDATE OVT_APPLICATIONS SET STATUS=?, UPDATED_AT=CURRENT_TIMESTAMP " + + "WHERE APPL_ID=?"; + try (PreparedStatement ps = conn.prepareStatement(updSql)) { + ps.setString(1, CNS_STATUS_CANCEL); + ps.setString(2, wrkSqlApplId); + ps.executeUpdate(); + } catch (SQLException ex) { + sqlcode = ex.getErrorCode(); + dbErr(); + return; + } + cunDbxUpd++; + cunCommit++; + monthlySub(); + if (cunCommit >= CNS_COMMIT_CNT) { + commitDb(); + } + } + + // 2210MONTHLYSUBSOR + private void monthlySub() { + // COBOL: PERFORM UNTIL WRK-IDX > 3 OR SQLCODE = 0 → 成功(SQLCODE=0)で抜ける。 + // 失敗(SQL例外)時のみリトライ。 + int retryCnt = 3; + sqlcode = 0; + for (int idx = 1; idx <= retryCnt; idx++) { + String updSql = "UPDATE OVT_MONTHLY SET OVT_HOURS=OVT_HOURS - ?, " + + "OVT_COUNT=OVT_COUNT - 1, UPDATED_AT=CURRENT_TIMESTAMP " + + "WHERE EMP_ID=? AND YEAR_MONTH=? AND OVT_TYPE=?"; + try (PreparedStatement ps = conn.prepareStatement(updSql)) { + ps.setBigDecimal(1, wrkSqlOvtHours); + ps.setString(2, wrkSqlEmpId); + ps.setString(3, wrkSqlYearMonth); + ps.setString(4, wrkSqlOvtType); + ps.executeUpdate(); + sqlcode = 0; + break; + } catch (SQLException ex) { + sqlcode = ex.getErrorCode(); + try { + conn.rollback(); + } catch (SQLException ex2) { + // ignore + } + } + } + if (sqlcode != 0) { + wrkErrCategory = 21; + wrkErrDetail = "MONTHLY SUBTRACT FAIL APPL-ID=" + wrkSqlApplId; + writeW01(wrkErrCategory, wrkErrDetail); + abend(); + return; + } + cunDbxUpd++; + cunCommit++; + if (cunCommit >= CNS_COMMIT_CNT) { + commitDb(); + } + } + + private void commitDb() { + try { + conn.commit(); + } catch (SQLException ex) { + sqlcode = ex.getErrorCode(); + dbErr(); + return; + } + cunCommit = 0; + } + + // 9100DBERRSOR + private void dbErr() { + wrkErrCategory = 30; + wrkErrDetail = "DB ERROR SQLCODE=" + sqlcode + " APPL-ID=" + wrkSqlApplId; + writeW01(wrkErrCategory, wrkErrDetail); + try { + conn.rollback(); + } catch (SQLException ex2) { + // ignore + } + cunCommit = 0; + abend(); + } + + private void writeW01(final int category, final String detail) { + Zan05Rec rec = new Zan05Rec(); + rec.setErrCategory(String.valueOf(category)); + rec.setErrDetail(detail); + try { + w01Out.write(rec.toRecordLine()); + w01Out.newLine(); + cunW01Out++; + } catch (IOException e) { + throw new RuntimeException("WRITE W01 ERROR", e); + } + } + + private void stpProc() { + if (cunCommit > 0) { + commitDb(); + } + try { + if (r01In != null) { + r01In.close(); + } + if (r02In != null) { + r02In.close(); + } + if (w01Out != null) { + w01Out.close(); + } + if (conn != null) { + conn.close(); + } + } catch (Exception e) { + throw new RuntimeException("CLOSE ERROR", e); + } + msgInkCnt("ZAN06R01", cunR01Inn); + msgInkCnt("ZAN06R02", cunR02Inn); + msgInkCnt("INS", cunDbxIns); + msgInkCnt("UPD", cunDbxUpd); + msgOutCnt("ZAN06W01", cunW01Out); + msgOut(CNS_MSGFIN, "", ""); + } + + private void msgOut(final int code, final String p1, final String p2) { + m00.reset(); + m00.setM00MsgCod(code); + m00.setM00Umkdats2201(p1); + m00.setM00Umkdats2202(p2); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgCompiled() { + m00.reset(); + m00.setM00MsgCod(CNS_MSGKEYINF); + m00.setM00Umkdats2201("2026-06-16"); + m00.setM00Umkdats2202("COMPILED"); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgInkCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGIINKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void msgOutCnt(final String dd, final long cnt) { + m00.reset(); + m00.setM00MsgCod(CNS_MSGOUTKES); + m00.setM00Umkdats2201(dd); + m00.setM00Umkdats2202(String.valueOf(cnt)); + m00.setM00Umkdats2203(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2204(String.valueOf(CNS_KN0002)); + m00.setM00Umkdats2205(PRGIDX); + Sub02MsgSub.execute(m00); + } + + private void abend() { + e01.reset(); + e01.setE01AbdCod(CNS_ABD999); + Sub03EndSub.execute(e01); + } + + private BigDecimal parseOvt(final String s) { + String t = (s == null) ? "" : s.trim(); + if (!ConvUtil.isNumeric(t) || t.isEmpty()) { + return BigDecimal.ZERO; + } + return new BigDecimal(t).movePointLeft(1); + } +} diff --git a/JavaSrc/src/dto/EmpRec.java b/JavaSrc/src/dto/EmpRec.java new file mode 100644 index 0000000..627fcfc --- /dev/null +++ b/JavaSrc/src/dto/EmpRec.java @@ -0,0 +1,64 @@ +package dto; + +import java.math.BigDecimal; + +/** + * ============================================================ + * 【COBOL→Java 标准例】记录布局类(DTO) + * ============================================================ + * COBOL 的 01 层记录定义 → 一个 DTO 类,各 05 层项目 → 字段。 + * + * 01 EMPREC. + * 05 EMP-ID PIC X(5). 社员番号 + * 05 EMP-NAME PIC X(20). 氏名 + * 05 EMP-DEPT PIC X(2). 部门CD + * 05 EMP-SALARY PIC 9(7)V99. 给料(隐含小数2位) + * + * 固定长记录的拆分在读取侧用 substring 完成,见 FileSample#parseRec。 + */ +public class EmpRec { + + /** 05 EMP-ID PIC X(5). */ + private String empId = ""; + + /** 05 EMP-NAME PIC X(20). */ + private String empName = ""; + + /** 05 EMP-DEPT PIC X(2). */ + private String empDept = ""; + + /** 05 EMP-SALARY PIC 9(7)V99. → 隐含小数点,用 BigDecimal 承载 */ + private BigDecimal empSalary = BigDecimal.ZERO; + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = empId; + } + + public String getEmpName() { + return empName; + } + + public void setEmpName(String empName) { + this.empName = empName; + } + + public String getEmpDept() { + return empDept; + } + + public void setEmpDept(String empDept) { + this.empDept = empDept; + } + + public BigDecimal getEmpSalary() { + return empSalary; + } + + public void setEmpSalary(BigDecimal empSalary) { + this.empSalary = empSalary; + } +} diff --git a/JavaSrc/src/dto/Kin01Rec.java b/JavaSrc/src/dto/Kin01Rec.java new file mode 100644 index 0000000..bae4c7a --- /dev/null +++ b/JavaSrc/src/dto/Kin01Rec.java @@ -0,0 +1,177 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN01REC(勤怠: 休暇申請中間レコード WORK-LEAVE 80B) + * ============================================================ + * 原定义:COPY KIN01REC REPLACING ==(A)== BY ==W01 → FD W01OUTFIL 的 01 W01OUTREC。 + * 文件属性:KIN01W01 / 順編成 / F(固定长) / 80B + * + * 03 (A)APPL-ID PIC 9(009). → applId + * 03 (A)EMP-ID PIC X(008). → empId + * 03 (A)LEAVE-TYPE PIC X(002). → leaveType + * 03 (A)START-DATE PIC 9(008). → startDate + * 03 (A)START-TIME PIC 9(004). → startTime + * 03 (A)END-DATE PIC 9(008). → endDate + * 03 (A)END-TIME PIC 9(004). → endTime + * 03 (A)STATUS PIC X(001). → status + * 03 (A)FILLER PIC X(036). → filler + */ +public class Kin01Rec { + + /** 03 W01APPL-ID PIC 9(009). */ + private int applId; + + /** 03 W01EMP-ID PIC X(008). */ + private String empId = ""; + + /** 03 W01LEAVE-TYPE PIC X(002). */ + private String leaveType = ""; + + /** 03 W01START-DATE PIC 9(008). YYYYMMDD */ + private int startDate; + + /** 03 W01START-TIME PIC 9(004). HHMM */ + private int startTime; + + /** 03 W01END-DATE PIC 9(008). YYYYMMDD */ + private int endDate; + + /** 03 W01END-TIME PIC 9(004). HHMM */ + private int endTime; + + /** 03 W01STATUS PIC X(001). '1'=有効 / '9'=取消 */ + private String status = ""; + + /** 03 W01FILLER PIC X(036). INITIALIZE 时为 SPACE */ + private String filler = ""; + + /** + * INITIALIZE W01OUTREC 相当:数值→0、文字→SPACE。 + */ + public void reset() { + applId = 0; + empId = ""; + leaveType = ""; + startDate = 0; + startTime = 0; + endDate = 0; + endTime = 0; + status = ""; + filler = ""; + } + + /** + * WRITE W01OUTREC 用:固定长 80B 序列化。 + * 各项目按 05 层字节位置拼接(PIC 转换统一走 ConvUtil)。 + */ + public String toRecordLine() { + return ConvUtil.padLeftZero(applId, 9) // APPL-ID 9(009) [1..9] + + ConvUtil.padRight(empId, 8) // EMP-ID X(008) [10..17] + + ConvUtil.padRight(leaveType, 2) // LEAVE-TYPE X(002) [18..19] + + ConvUtil.padLeftZero(startDate, 8)// START-DATE 9(008) [20..27] + + ConvUtil.padLeftZero(startTime, 4)// START-TIME 9(004) [28..31] + + ConvUtil.padLeftZero(endDate, 8) // END-DATE 9(008) [32..39] + + ConvUtil.padLeftZero(endTime, 4) // END-TIME 9(004) [40..43] + + ConvUtil.padRight(status, 1) // STATUS X(001) [44] + + ConvUtil.padRight(filler, 36); // FILLER X(036) [45..80] + } + + public int getApplId() { + return applId; + } + + public void setApplId(int applId) { + this.applId = applId; + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = empId; + } + + public String getLeaveType() { + return leaveType; + } + + public void setLeaveType(String leaveType) { + this.leaveType = leaveType; + } + + public int getStartDate() { + return startDate; + } + + public void setStartDate(int startDate) { + this.startDate = startDate; + } + + public int getStartTime() { + return startTime; + } + + public void setStartTime(int startTime) { + this.startTime = startTime; + } + + public int getEndDate() { + return endDate; + } + + public void setEndDate(int endDate) { + this.endDate = endDate; + } + + public int getEndTime() { + return endTime; + } + + public void setEndTime(int endTime) { + this.endTime = endTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = filler; + } + + /** + * READ R01INNREC 用:80B 固定長行を解析(COPY KIN01REC → R01 项目)。 + * 位置は toRecordLine() と対称: + * APPL-ID[0..9) EMP-ID[9..17) LEAVE-TYPE[17..19) START-DATE[19..27) + * START-TIME[27..31) END-DATE[31..39) END-TIME[39..43) STATUS[43..44) + * FILLER[44..80) + */ + public void fromRecordLine(final String line) { + reset(); + String s = (line == null) ? "" : line; + while (s.length() < 80) { + s = s + " "; + } + applId = Integer.parseInt(s.substring(0, 9).trim()); + empId = s.substring(9, 17); + leaveType = s.substring(17, 19); + startDate = Integer.parseInt(s.substring(19, 27).trim()); + startTime = Integer.parseInt(s.substring(27, 31).trim()); + endDate = Integer.parseInt(s.substring(31, 39).trim()); + endTime = Integer.parseInt(s.substring(39, 43).trim()); + status = s.substring(43, 44); + filler = s.substring(44, 80); + } +} diff --git a/JavaSrc/src/dto/Kin02Rec.java b/JavaSrc/src/dto/Kin02Rec.java new file mode 100644 index 0000000..4a2c492 --- /dev/null +++ b/JavaSrc/src/dto/Kin02Rec.java @@ -0,0 +1,137 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN02REC(勤怠: 日別展開休暇レコード LEAVE-DAILY 80B) + * ============================================================ + * 原定义:COPY KIN02REC REPLACING ==(A)== BY ==W01 → FD W01OUTFIL 的 01 W01OUTREC。 + * 文件属性:KIN02W01.DAT / 順編成 / F(固定长) / 80B + * + * 03 (A)EMP-ID PIC X(008). → empId + * 03 (A)DATE PIC 9(008). → date(YYYYMMDD) + * 03 (A)LEAVE-TYPE PIC X(002). → leaveType + * 03 (A)START-TIME PIC 9(004). → startTime(HHMM) + * 03 (A)END-TIME PIC 9(004). → endTime(HHMM) + * 03 (A)APPL-ID PIC 9(009). → applId + * 03 (A)FILLER PIC X(045). → filler + */ +public class Kin02Rec { + + /** 03 W01EMP-ID PIC X(008). */ + private String empId = ""; + + /** 03 W01DATE PIC 9(008). YYYYMMDD */ + private int date; + + /** 03 W01LEAVE-TYPE PIC X(002). */ + private String leaveType = ""; + + /** 03 W01START-TIME PIC 9(004). HHMM */ + private int startTime; + + /** 03 W01END-TIME PIC 9(004). HHMM */ + private int endTime; + + /** 03 W01APPL-ID PIC 9(009). */ + private int applId; + + /** 03 W01FILLER PIC X(045). INITIALIZE 时为 SPACE */ + private String filler = ""; + + /** INITIALIZE W01OUTREC 相当:数值→0、文字→SPACE。 */ + public void reset() { + empId = ""; + date = 0; + leaveType = ""; + startTime = 0; + endTime = 0; + applId = 0; + filler = ""; + } + + /** WRITE W01OUTREC 用:固定长 80B 序列化。 */ + public String toRecordLine() { + return ConvUtil.padRight(empId, 8) // EMP-ID X(008) [1..8] + + ConvUtil.padLeftZero(date, 8) // DATE 9(008) [9..16] + + ConvUtil.padRight(leaveType, 2) // LEAVE-TYPE X(002) [17..18] + + ConvUtil.padLeftZero(startTime, 4)// START-TIME 9(004) [19..22] + + ConvUtil.padLeftZero(endTime, 4) // END-TIME 9(004) [23..26] + + ConvUtil.padLeftZero(applId, 9) // APPL-ID 9(009) [27..35] + + ConvUtil.padRight(filler, 45); // FILLER X(045) [36..80] + } + + /** + * READ R02INNFIL 用:80B固定長行から編集(数値項目は数値化、長さ不足は右空白埋め)。 + */ + public void fromRecordLine(final String line) { + String s = (line == null) ? "" : line; + if (s.length() < 80) { + s = ConvUtil.padRight(s, 80); + } + empId = s.substring(0, 8); // EMP-ID X(008) [1..8] + date = Integer.parseInt(s.substring(8, 16)); // DATE 9(008) [9..16] + leaveType = s.substring(16, 18); // LEAVE-TYPE X(002) [17..18] + startTime = Integer.parseInt(s.substring(18, 22)); // START-TIME 9(004) [19..22] + endTime = Integer.parseInt(s.substring(22, 26)); // END-TIME 9(004) [23..26] + applId = Integer.parseInt(s.substring(26, 35)); // APPL-ID 9(009) [27..35] + filler = s.substring(35, 80); // FILLER X(045) [36..80] + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = empId; + } + + public int getDate() { + return date; + } + + public void setDate(int date) { + this.date = date; + } + + public String getLeaveType() { + return leaveType; + } + + public void setLeaveType(String leaveType) { + this.leaveType = leaveType; + } + + public int getStartTime() { + return startTime; + } + + public void setStartTime(int startTime) { + this.startTime = startTime; + } + + public int getEndTime() { + return endTime; + } + + public void setEndTime(int endTime) { + this.endTime = endTime; + } + + public int getApplId() { + return applId; + } + + public void setApplId(int applId) { + this.applId = applId; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = filler; + } +} diff --git a/JavaSrc/src/dto/Kin03Rec.java b/JavaSrc/src/dto/Kin03Rec.java new file mode 100644 index 0000000..158b7e3 --- /dev/null +++ b/JavaSrc/src/dto/Kin03Rec.java @@ -0,0 +1,192 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN03REC(勤怠: 打刻休暇照合レコード KIN-LEAVE 160B FB) + * ============================================================ + * 原定义:COPY KIN03REC REPLACING ==(A)== BY ==W01 → FD W01OUTFIL 的 01 W01OUTREC。 + * 文件属性:KIN05W01 / 順編成 / FB / 160B + * + * 03 (A)EMP-ID PIC X(008). → empId + * 03 (A)WORK-DATE PIC 9(008). → workDate + * 03 (A)STR-TIME PIC 9(004). → strTime + * 03 (A)END-TIME PIC 9(004). → endTime + * 03 (A)DEPT-ID PIC X(004). → deptId + * 03 (A)TERMINAL PIC X(006). → terminal + * 03 (A)FILLER-1 PIC X(046). → filler1 + * 03 (A)LEAVE-TYPE PIC X(002). → leaveType + * 03 (A)LEAVE-STR-TIME PIC 9(004). → leaveStrTime + * 03 (A)LEAVE-END-TIME PIC 9(004). → leaveEndTime + * 03 (A)LEAVE-HOURS PIC 9(004)V9(001). → leaveHours(5桁、小数点以下1桁内蔵) + * 03 (A)APPLICATION-ID PIC 9(009). → applicationId + * 03 (A)FILLER-2 PIC X(056). → filler2 + */ +public class Kin03Rec { + + /** 03 W01EMP-ID PIC X(008). */ + private String empId = ""; + /** 03 W01WORK-DATE PIC 9(008). */ + private String workDate = ""; + /** 03 W01STR-TIME PIC 9(004). */ + private String strTime = ""; + /** 03 W01END-TIME PIC 9(004). */ + private String endTime = ""; + /** 03 W01DEPT-ID PIC X(004). */ + private String deptId = ""; + /** 03 W01TERMINAL PIC X(006). */ + private String terminal = ""; + /** 03 W01LEAVE-TYPE PIC X(002). */ + private String leaveType = ""; + /** 03 W01LEAVE-STR-TIME PIC 9(004). */ + private int leaveStrTime; + /** 03 W01LEAVE-END-TIME PIC 9(004). */ + private int leaveEndTime; + /** 03 W01LEAVE-HOURS PIC 9(004)V9(001). */ + private int leaveHours; + /** 03 W01APPLICATION-ID PIC 9(009). */ + private int applicationId; + + /** + * INITIALIZE W01OUTREC 相当。 + */ + public void reset() { + empId = ""; + workDate = ""; + strTime = ""; + endTime = ""; + deptId = ""; + terminal = ""; + leaveType = ""; + leaveStrTime = 0; + leaveEndTime = 0; + leaveHours = 0; + applicationId = 0; + } + + /** + * WRITE W01OUTREC 用:固定長记录序列化(160B)。 + * 8+8+4+4+4+6+46+2+4+4+5+9+56 = 160。 + */ + public String toRecordLine() { + return ConvUtil.padRight(empId, 8) // EMP-ID X(008) [1..8] + + ConvUtil.padLeftZero(workDate, 8) // WORK-DATE 9(008) [9..16] 前ゼロ埋め + + ConvUtil.padLeftZero(strTime, 4) // STR-TIME 9(004) [17..20] 前ゼロ埋め + + ConvUtil.padLeftZero(endTime, 4) // END-TIME 9(004) [21..24] 前ゼロ埋め + + ConvUtil.padRight(deptId, 4) // DEPT-ID X(004) [25..28] + + ConvUtil.padRight(terminal, 6) // TERMINAL X(006) [29..34] + + ConvUtil.padRight("", 46) // FILLER-1 X(046) [35..80] + + ConvUtil.padRight(leaveType, 2) // LEAVE-TYPE X(002) [81..82] + + ConvUtil.padLeftZero(leaveStrTime, 4) // LEAVE-STR-TIME 9(004) [83..86] + + ConvUtil.padLeftZero(leaveEndTime, 4) // LEAVE-END-TIME 9(004) [87..90] + + ConvUtil.padLeftZero(leaveHours, 5) // LEAVE-HOURS 9(004)V9(1) [91..95] + + ConvUtil.padLeftZero(applicationId, 9)// APPLICATION-ID 9(009) [96..104] + + ConvUtil.padRight("", 56); // FILLER-2 X(056) [105..160] + } + + /** READ R02INNREC 用:160B 固定長文字列からの復元(R02=KIN-LEAVE / KIN03REC)。 */ + public void fromRecordLine(final String line) { + empId = ConvUtil.safeSub(line, 0, 8); // EMP-ID X(008) [1..8] + workDate = ConvUtil.safeSub(line, 8, 16); // WORK-DATE 9(008) [9..16] + strTime = ConvUtil.safeSub(line, 16, 20); // STR-TIME 9(004) [17..20] + endTime = ConvUtil.safeSub(line, 20, 24); // END-TIME 9(004) [21..24] + deptId = ConvUtil.safeSub(line, 24, 28); // DEPT-ID X(004) [25..28] + terminal = ConvUtil.safeSub(line, 28, 34); // TERMINAL X(006) [29..34] + // FILLER-1 X(046) [35..80] + leaveType = ConvUtil.safeSub(line, 80, 82); // LEAVE-TYPE X(002) [81..82] + leaveStrTime = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 82, 86), 0); // [83..86] + leaveEndTime = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 86, 90), 0); // [87..90] + leaveHours = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 90, 95), 0); // [91..95] + applicationId = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 95, 104), 0); // [96..104] + // FILLER-2 X(056) [105..160] + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public String getWorkDate() { + return workDate; + } + + public void setWorkDate(String workDate) { + this.workDate = (workDate == null) ? "" : workDate; + } + + public String getStrTime() { + return strTime; + } + + public void setStrTime(String strTime) { + this.strTime = (strTime == null) ? "" : strTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = (endTime == null) ? "" : endTime; + } + + public String getDeptId() { + return deptId; + } + + public void setDeptId(String deptId) { + this.deptId = (deptId == null) ? "" : deptId; + } + + public String getTerminal() { + return terminal; + } + + public void setTerminal(String terminal) { + this.terminal = (terminal == null) ? "" : terminal; + } + + public String getLeaveType() { + return leaveType; + } + + public void setLeaveType(String leaveType) { + this.leaveType = (leaveType == null) ? "" : leaveType; + } + + public int getLeaveStrTime() { + return leaveStrTime; + } + + public void setLeaveStrTime(int leaveStrTime) { + this.leaveStrTime = leaveStrTime; + } + + public int getLeaveEndTime() { + return leaveEndTime; + } + + public void setLeaveEndTime(int leaveEndTime) { + this.leaveEndTime = leaveEndTime; + } + + public int getLeaveHours() { + return leaveHours; + } + + public void setLeaveHours(int leaveHours) { + this.leaveHours = leaveHours; + } + + public int getApplicationId() { + return applicationId; + } + + public void setApplicationId(int applicationId) { + this.applicationId = applicationId; + } +} diff --git a/JavaSrc/src/dto/Kin04Rec.java b/JavaSrc/src/dto/Kin04Rec.java new file mode 100644 index 0000000..15bae37 --- /dev/null +++ b/JavaSrc/src/dto/Kin04Rec.java @@ -0,0 +1,129 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN04REC(勤怠: 打刻編集レコード EDITED-PUNCH 80B FB) + * ============================================================ + * 原定义:COPY KIN04REC REPLACING ==(A)== BY ==W01 → FD W01OUTFIL 的 01 W01OUTREC。 + * 原定义:COPY KIN04REC REPLACING ==(A)== BY ==O1 → WS-W01REC(WRITE FROM 元)。 + * 文件属性:KIN04W01 / 順編成 / FB / 80B + * + * 03 (A)EMP-ID PIC X(008). → empId + * 03 (A)WORK-DATE PIC 9(008). → workDate + * 03 (A)STR-TIME PIC 9(004). → strTime + * 03 (A)END-TIME PIC 9(004). → endTime + * 03 (A)DEPT-ID PIC X(004). → deptId + * 03 (A)TERMINAL PIC X(006). → terminal + * 03 (A)FILLER PIC X(046). → filler + */ +public class Kin04Rec { + + /** 03 W01EMP-ID PIC X(008). */ + private String empId = ""; + + /** 03 W01WORK-DATE PIC 9(008). */ + private String workDate = ""; + + /** 03 W01STR-TIME PIC 9(004). */ + private String strTime = ""; + + /** 03 W01END-TIME PIC 9(004). */ + private String endTime = ""; + + /** 03 W01DEPT-ID PIC X(004). */ + private String deptId = ""; + + /** 03 W01TERMINAL PIC X(006). */ + private String terminal = ""; + + /** + * INITIALIZE W01OUTREC 相当。 + */ + public void reset() { + empId = ""; + workDate = ""; + strTime = ""; + endTime = ""; + deptId = ""; + terminal = ""; + } + + /** + * WRITE W01OUTREC 用:固定長记录序列化(8 + 8 + 4 + 4 + 4 + 6 + 46 = 80B)。 + */ + public String toRecordLine() { + return ConvUtil.padRight(empId, 8) // EMP-ID X(008) [1..8] + + ConvUtil.padRight(workDate, 8) // WORK-DATE 9(008) [9..16] + + ConvUtil.padRight(strTime, 4) // STR-TIME 9(004) [17..20] + + ConvUtil.padRight(endTime, 4) // END-TIME 9(004) [21..24] + + ConvUtil.padRight(deptId, 4) // DEPT-ID X(004) [25..28] + + ConvUtil.padRight(terminal, 6) // TERMINAL X(006) [29..34] + + ConvUtil.padRight("", 46); // FILLER X(046) [35..80] + } + + /** + * READ R01INNFIL 用:80B固定長行から編集(左詰め、長さ不足は右空白埋め)。 + */ + public void fromRecordLine(final String line) { + String s = (line == null) ? "" : line; + if (s.length() < 80) { + s = ConvUtil.padRight(s, 80); + } + empId = s.substring(0, 8); // EMP-ID X(008) [1..8] + workDate = s.substring(8, 16); // WORK-DATE 9(008) [9..16] + strTime = s.substring(16, 20); // STR-TIME 9(004) [17..20] + endTime = s.substring(20, 24); // END-TIME 9(004) [21..24] + deptId = s.substring(24, 28); // DEPT-ID X(004) [25..28] + terminal = s.substring(28, 34); // TERMINAL X(006) [29..34] + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public String getWorkDate() { + return workDate; + } + + public void setWorkDate(String workDate) { + this.workDate = (workDate == null) ? "" : workDate; + } + + public String getStrTime() { + return strTime; + } + + public void setStrTime(String strTime) { + this.strTime = (strTime == null) ? "" : strTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = (endTime == null) ? "" : endTime; + } + + public String getDeptId() { + return deptId; + } + + public void setDeptId(String deptId) { + this.deptId = (deptId == null) ? "" : deptId; + } + + public String getTerminal() { + return terminal; + } + + public void setTerminal(String terminal) { + this.terminal = (terminal == null) ? "" : terminal; + } +} diff --git a/JavaSrc/src/dto/Kin05Rec.java b/JavaSrc/src/dto/Kin05Rec.java new file mode 100644 index 0000000..1dc5ac6 --- /dev/null +++ b/JavaSrc/src/dto/Kin05Rec.java @@ -0,0 +1,55 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN05REC(勤怠: エラーログレコード KERR-LOG VB可変長) + * ============================================================ + * 原定义:COPY KIN05REC REPLACING ==(A)== BY ==W02 → FD W02OUTFIL 的 01 W02OUTREC。 + * 文件属性:KIN01W02 / 順編成 / V(可变长) / 最大200B + * ※VB 记录的 RDW 由文本行长度天然体现,Java 侧不输出长度前缀。 + * + * 03 (A)ERR-CATEGORY PIC 9(002). → errCategory + * 03 (A)ERR-DETAIL PIC X(198). → errDetail + */ +public class Kin05Rec { + + /** 03 W02ERR-CATEGORY PIC 9(002). */ + private int errCategory; + + /** 03 W02ERR-DETAIL PIC X(198). */ + private String errDetail = ""; + + /** + * INITIALIZE W02OUTREC 相当。 + */ + public void reset() { + errCategory = 0; + errDetail = ""; + } + + /** + * WRITE W02OUTREC 用:可变长记录序列化(2 + 198 = 200B)。 + */ + public String toRecordLine() { + return ConvUtil.padLeftZero(errCategory, 2) // ERR-CATEGORY 9(002) [1..2] + + ConvUtil.padRight(errDetail, 198); // ERR-DETAIL X(198) [3..200] + } + + public int getErrCategory() { + return errCategory; + } + + public void setErrCategory(int errCategory) { + this.errCategory = errCategory; + } + + public String getErrDetail() { + return errDetail; + } + + public void setErrDetail(String errDetail) { + this.errDetail = errDetail; + } +} diff --git a/JavaSrc/src/dto/Kin06Rec.java b/JavaSrc/src/dto/Kin06Rec.java new file mode 100644 index 0000000..5f22146 --- /dev/null +++ b/JavaSrc/src/dto/Kin06Rec.java @@ -0,0 +1,83 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN06REC(勤怠: 出勤日カレンダーレコード WORK-DAY-FILE 80B) + * ============================================================ + * 原定义:COPY KIN06REC REPLACING ==(A)== BY ==OUT → FD OUTRECFIL 的 01 OUTREC。 + * 文件属性:KIN06W01 / 順編成 / F / 80B + * + * 03 (A)EMP-ID PIC X(008). → empId + * 03 (A)DATE PIC 9(008). → date(YYYYMMDD) + * 03 (A)DAY-OF-WEEK PIC 9(001). → dayOfWeek(1=月…7=日) + * 03 (A)FILLER PIC X(063). → filler + */ +public class Kin06Rec { + + /** 03 OUTEMP-ID PIC X(008). */ + private String empId = ""; + /** 03 OUTDATE PIC 9(008). YYYYMMDD */ + private int date; + /** 03 OUTDAY-OF-WEEK PIC 9(001). 1=月…7=日 */ + private int dayOfWeek; + /** 03 OUTFILLER PIC X(063). */ + private String filler = ""; + + /** INITIALIZE OUTREC 相当。 */ + public void reset() { + empId = ""; + date = 0; + dayOfWeek = 0; + filler = ""; + } + + /** WRITE OUTREC 用:固定長 80B 序列化。8+8+1+63=80。 */ + public String toRecordLine() { + return ConvUtil.padRight(empId, 8) // EMP-ID X(008) [1..8] + + ConvUtil.padLeftZero(date, 8) // DATE 9(008) [9..16] + + ConvUtil.padLeftZero(dayOfWeek, 1) // DAY-OF-WEEK 9(001) [17] + + ConvUtil.padRight(filler, 63); // FILLER X(063) [18..80] + } + + /** READ OUTREC 用:80B 固定長文字列からの復元(R01=WORK-DAY-FILE)。 */ + public void fromRecordLine(final String line) { + empId = ConvUtil.safeSub(line, 0, 8); + date = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 8, 16), 0); + dayOfWeek = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 16, 17), 0); + filler = ConvUtil.safeSub(line, 17, 80); + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public int getDate() { + return date; + } + + public void setDate(int date) { + this.date = date; + } + + public int getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(int dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = (filler == null) ? "" : filler; + } +} diff --git a/JavaSrc/src/dto/Kin07Rec.java b/JavaSrc/src/dto/Kin07Rec.java new file mode 100644 index 0000000..994bf75 --- /dev/null +++ b/JavaSrc/src/dto/Kin07Rec.java @@ -0,0 +1,159 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN07REC(勤怠: 日別勤怠計算レコード DAILY-RECORD 200B) + * ============================================================ + * 原定义:COPY KIN07REC REPLACING ==(A)== BY ==SD → FD W01OUTFIL 的 01 W01OUTREC。 + * 文件属性:KIN07W01 / 順編成 / F / 200B + * + * 03 (A)EMP-ID PIC 9(008). → empId + * 03 (A)DATE PIC 9(008). → date + * 03 (A)TIME-IN PIC 9(004). → timeIn + * 03 (A)TIME-OUT PIC 9(004). → timeOut + * 03 (A)ANNUAL-H PIC 9(004)V9(001). → annualH(0.1h単位整数) + * 03 (A)PERSONAL-H PIC 9(004)V9(001). → personalH + * 03 (A)OFFICIAL-H PIC 9(004)V9(001). → officialH + * 03 (A)SICK-H PIC 9(004)V9(001). → sickH + * 03 (A)ABSENT-H PIC 9(004)V9(001). → absentH + * 03 (A)FILLER PIC X(151). → filler + * + * 各 *_H は「0.1h 単位の整数(tenth)」で保持(例: 2.0h = 20)。 + */ +public class Kin07Rec { + + private String empId = ""; + private int date; + private int timeIn; + private int timeOut; + private int annualH; + private int personalH; + private int officialH; + private int sickH; + private int absentH; + private String filler = ""; + + /** INITIALIZE W01OUTREC 相当。 */ + public void reset() { + empId = ""; + date = 0; + timeIn = 0; + timeOut = 0; + annualH = 0; + personalH = 0; + officialH = 0; + sickH = 0; + absentH = 0; + filler = ""; + } + + /** WRITE W01OUTREC 用:固定長 200B 序列化。8+8+4+4+5*5+151=200。 */ + public String toRecordLine() { + return ConvUtil.padRight(empId, 8) // EMP-ID X(008) [1..8] + + ConvUtil.padLeftZero(date, 8) // DATE 9(008) [9..16] + + ConvUtil.padLeftZero(timeIn, 4) // TIME-IN 9(004) [17..20] + + ConvUtil.padLeftZero(timeOut, 4) // TIME-OUT 9(004) [21..24] + + ConvUtil.padLeftZero(annualH, 5) // ANNUAL-H 9(4)V9(1) [25..29] + + ConvUtil.padLeftZero(personalH, 5) // PERSONAL-H 9(4)V9(1) [30..34] + + ConvUtil.padLeftZero(officialH, 5) // OFFICIAL-H 9(4)V9(1) [35..39] + + ConvUtil.padLeftZero(sickH, 5) // SICK-H 9(4)V9(1) [40..44] + + ConvUtil.padLeftZero(absentH, 5) // ABSENT-H 9(4)V9(1) [45..49] + + ConvUtil.padRight(filler, 151); // FILLER X(151) [50..200] + } + + /** READ R01INNREC 用:200B 固定長文字列からの復元(R01=DAILY-RECORD / KIN07REC)。 */ + public void fromRecordLine(final String line) { + empId = ConvUtil.safeSub(line, 0, 8); // EMP-ID X(008) [1..8] + date = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 8, 16), 0); // DATE 9(008) [9..16] + timeIn = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 16, 20), 0); // TIME-IN 9(004) [17..20] + timeOut = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 20, 24), 0); // TIME-OUT 9(004) [21..24] + annualH = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 24, 29), 0); // ANNUAL-H 9(4)V9(1) [25..29] + personalH = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 29, 34), 0); // PERSONAL-H [30..34] + officialH = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 34, 39), 0); // OFFICIAL-H [35..39] + sickH = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 39, 44), 0); // SICK-H [40..44] + absentH = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 44, 49), 0); // ABSENT-H [45..49] + filler = ConvUtil.safeSub(line, 49, 200); // FILLER X(151) [50..200] + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public int getDate() { + return date; + } + + public void setDate(int date) { + this.date = date; + } + + public int getTimeIn() { + return timeIn; + } + + public void setTimeIn(int timeIn) { + this.timeIn = timeIn; + } + + public int getTimeOut() { + return timeOut; + } + + public void setTimeOut(int timeOut) { + this.timeOut = timeOut; + } + + public int getAnnualH() { + return annualH; + } + + public void setAnnualH(int annualH) { + this.annualH = annualH; + } + + public int getPersonalH() { + return personalH; + } + + public void setPersonalH(int personalH) { + this.personalH = personalH; + } + + public int getOfficialH() { + return officialH; + } + + public void setOfficialH(int officialH) { + this.officialH = officialH; + } + + public int getSickH() { + return sickH; + } + + public void setSickH(int sickH) { + this.sickH = sickH; + } + + public int getAbsentH() { + return absentH; + } + + public void setAbsentH(int absentH) { + this.absentH = absentH; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = (filler == null) ? "" : filler; + } +} diff --git a/JavaSrc/src/dto/Kin08Rec.java b/JavaSrc/src/dto/Kin08Rec.java new file mode 100644 index 0000000..f79c1d0 --- /dev/null +++ b/JavaSrc/src/dto/Kin08Rec.java @@ -0,0 +1,121 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KIN08REC(勤怠: 欠勤集計中間ファイル ABSENCE-SUMMARY 80B) + * ============================================================ + * 原定义:COPY KIN08REC REPLACING ==(A)== BY ==W01 → FD W01OUTFIL 的 01 W01OUTREC。 + * 文件属性:KIN08W01 / 順編成 / F / 80B + * + * 03 (A)EMP-ID PIC X(008). → empId + * 03 (A)YEAR-MONTH PIC X(006). → yearMonth + * 03 (A)ANNUAL-LEAVE-H PIC 9(004)V9(001). → annualH(0.1h単位整数) + * 03 (A)PERSONAL-LEAVE-H PIC 9(004)V9(001). → personalH + * 03 (A)OFFICIAL-LEAVE-H PIC 9(004)V9(001). → officialH + * 03 (A)SICK-LEAVE-H PIC 9(004)V9(001). → sickH + * 03 (A)ABSENT-H PIC 9(004)V9(001). → absentH + * 03 (A)FILLER PIC X(041). → filler + * + * 各 *_H は「0.1h 単位の整数(tenth)」で保持(例: 5.0h = 50)。 + */ +public class Kin08Rec { + + private String empId = ""; + private String yearMonth = ""; + private int annualH; + private int personalH; + private int officialH; + private int sickH; + private int absentH; + private String filler = ""; + + /** INITIALIZE W01OUTREC 相当。 */ + public void reset() { + empId = ""; + yearMonth = ""; + annualH = 0; + personalH = 0; + officialH = 0; + sickH = 0; + absentH = 0; + filler = ""; + } + + /** WRITE W01OUTREC 用:固定長 80B 序列化。8+6+5*5+41=80。 */ + public String toRecordLine() { + return ConvUtil.padRight(empId, 8) // EMP-ID X(008) [1..8] + + ConvUtil.padRight(yearMonth, 6) // YEAR-MONTH X(006) [9..14] + + ConvUtil.padLeftZero(annualH, 5) // ANNUAL-LEAVE-H 9(4)V9(1) [15..19] + + ConvUtil.padLeftZero(personalH, 5) // PERSONAL-LEAVE-H 9(4)V9(1) [20..24] + + ConvUtil.padLeftZero(officialH, 5) // OFFICIAL-LEAVE-H 9(4)V9(1) [25..29] + + ConvUtil.padLeftZero(sickH, 5) // SICK-LEAVE-H 9(4)V9(1) [30..34] + + ConvUtil.padLeftZero(absentH, 5) // ABSENT-H 9(4)V9(1) [35..39] + + ConvUtil.padRight(filler, 41); // FILLER X(041) [40..80] + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public String getYearMonth() { + return yearMonth; + } + + public void setYearMonth(String yearMonth) { + this.yearMonth = (yearMonth == null) ? "" : yearMonth; + } + + public int getAnnualH() { + return annualH; + } + + public void setAnnualH(int annualH) { + this.annualH = annualH; + } + + public int getPersonalH() { + return personalH; + } + + public void setPersonalH(int personalH) { + this.personalH = personalH; + } + + public int getOfficialH() { + return officialH; + } + + public void setOfficialH(int officialH) { + this.officialH = officialH; + } + + public int getSickH() { + return sickH; + } + + public void setSickH(int sickH) { + this.sickH = sickH; + } + + public int getAbsentH() { + return absentH; + } + + public void setAbsentH(int absentH) { + this.absentH = absentH; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = (filler == null) ? "" : filler; + } +} diff --git a/JavaSrc/src/dto/Kyu01Rec.java b/JavaSrc/src/dto/Kyu01Rec.java new file mode 100644 index 0000000..1103777 --- /dev/null +++ b/JavaSrc/src/dto/Kyu01Rec.java @@ -0,0 +1,140 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU01REC(EMP-RECORD 社員マスタ固定長レコード 80B) + * ============================================================ + * 01 W01OUTREC.(FD W01OUTFIL / COPY KYU01REC REPLACING ==(A)== BY ==W01==) + * 03 W01EMP-ID PIC X(008). → empId + * 03 W01EMP-NAME PIC X(040). → empName + * 03 W01DEPT-CODE PIC X(002). → deptCode + * 03 W01REGION-CODE PIC X(002). → regionCode + * 03 W01CATEGORY-CODE PIC X(003). → categoryCode + * 03 W01BASE-SALARY PIC 9(009). → baseSalary + * 03 W01HOURLY-RATE PIC 9(007). → hourlyRate + * 03 W01DEPENDENT-COUNT PIC 9(002). → dependentCount + * 03 W01STATUS PIC X(001). → status + * 03 FILLER PIC X(006). → 固定スペース + */ +public class Kyu01Rec { + + /** 03 W01EMP-ID PIC X(008). */ + private String empId = ""; + /** 03 W01EMP-NAME PIC X(040). */ + private String empName = ""; + /** 03 W01DEPT-CODE PIC X(002). */ + private String deptCode = ""; + /** 03 W01REGION-CODE PIC X(002). */ + private String regionCode = ""; + /** 03 W01CATEGORY-CODE PIC X(003). */ + private String categoryCode = ""; + /** 03 W01BASE-SALARY PIC 9(009). */ + private int baseSalary; + /** 03 W01HOURLY-RATE PIC 9(007). */ + private int hourlyRate; + /** 03 W01DEPENDENT-COUNT PIC 9(002). */ + private int dependentCount; + /** 03 W01STATUS PIC X(001). */ + private String status = ""; + + /** INITIALIZE W01OUTREC 相当:数値→0、文字→SPACE。 */ + public void reset() { + empId = ""; + empName = ""; + deptCode = ""; + regionCode = ""; + categoryCode = ""; + baseSalary = 0; + hourlyRate = 0; + dependentCount = 0; + status = ""; + } + + public void setEmpId(final String v) { + empId = ConvUtil.padRight(v, 8); + } + + public void setEmpName(final String v) { + empName = ConvUtil.padRight(v, 40); + } + + public void setDeptCode(final String v) { + deptCode = ConvUtil.padRight(v, 2); + } + + public void setRegionCode(final String v) { + regionCode = ConvUtil.padRight(v, 2); + } + + public void setCategoryCode(final String v) { + categoryCode = ConvUtil.padRight(v, 3); + } + + public void setBaseSalary(final int v) { + baseSalary = v; + } + + public void setHourlyRate(final int v) { + hourlyRate = v; + } + + public void setDependentCount(final int v) { + dependentCount = v; + } + + public void setStatus(final String v) { + status = ConvUtil.padRight(v, 1); + } + + public String getEmpId() { + return empId; + } + + public String getEmpName() { + return empName; + } + + public String getDeptCode() { + return deptCode; + } + + public String getRegionCode() { + return regionCode; + } + + public String getCategoryCode() { + return categoryCode; + } + + public int getBaseSalary() { + return baseSalary; + } + + public int getHourlyRate() { + return hourlyRate; + } + + public int getDependentCount() { + return dependentCount; + } + + public String getStatus() { + return status; + } + + /** WRK-CSV-ACCUM(1:80) 相当の 80B 固定長レコード行を生成(FORMAT WRITE)。 */ + public String toRecordLine() { + return empId + + empName + + deptCode + + regionCode + + categoryCode + + ConvUtil.padLeftZero(baseSalary, 9) + + ConvUtil.padLeftZero(hourlyRate, 7) + + ConvUtil.padLeftZero(dependentCount, 2) + + status + + ConvUtil.padRight("", 6); // FILLER X(006) + } +} diff --git a/JavaSrc/src/dto/Kyu02Rec.java b/JavaSrc/src/dto/Kyu02Rec.java new file mode 100644 index 0000000..f0dd74c --- /dev/null +++ b/JavaSrc/src/dto/Kyu02Rec.java @@ -0,0 +1,94 @@ +package dto; + +import java.math.BigDecimal; +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU02REC(ABSENCE-AGGREGATE 欠勤集約レコード 80B) + * ============================================================ + * 01 R02INNREC.(FD R02INNFIL / COPY KYU02REC REPLACING ==(A)== BY ==R02==) + * 03 R02EMP-ID PIC X(008). → empId + * 03 R02YEAR-MONTH PIC X(006). → yearMonth + * 03 R02TOTAL-HOURS PIC 9(004)V9(001). → totalHours(BigDecimal 小数点以下1桁) + * 03 R02ABSENT-COUNT PIC 9(002). → absentCount + * 03 FILLER PIC X(059). → 固定スペース + * + * レコード長: 8+6+5+2+59 = 80B(固定長)。 + * 読込時に固定長行を各項目に分解する parse() と、書込用 toRecordLine() を提供。 + */ +public class Kyu02Rec { + + /** 03 R02EMP-ID PIC X(008). */ + private String empId = ""; + /** 03 R02YEAR-MONTH PIC X(006). */ + private String yearMonth = ""; + /** 03 R02TOTAL-HOURS PIC 9(004)V9(001).(小数点以下1桁) */ + private BigDecimal totalHours = BigDecimal.ZERO; + /** 03 R02ABSENT-COUNT PIC 9(002). */ + private int absentCount; + + /** INITIALIZE R02INNREC 相当:数値→0、文字→SPACE。 */ + public void reset() { + empId = ""; + yearMonth = ""; + totalHours = BigDecimal.ZERO; + absentCount = 0; + } + + /** + * 固定長行(80B)を各項目に分解する。 + * 位置: EMP-ID(1:8) / YEAR-MONTH(9:14) / TOTAL-HOURS(15:19) / ABSENT-COUNT(20:21) / FILLER(22:80)。 + * @param line 読込レコード行(不足分は空文字として扱う) + */ + public static Kyu02Rec parse(final String line) { + final Kyu02Rec r = new Kyu02Rec(); + r.empId = ConvUtil.padRight(ConvUtil.safeSub(line, 0, 8), 8); + r.yearMonth = ConvUtil.padRight(ConvUtil.safeSub(line, 8, 14), 6); + // TOTAL-HOURS は 9(4)V9(1) → 5桁数値文字列を小数点以下1桁の BigDecimal に復元 + r.totalHours = ConvUtil.toDecimal(ConvUtil.safeSub(line, 14, 19), 1); + r.absentCount = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 19, 21), 0); + return r; + } + + public void setEmpId(final String v) { + empId = ConvUtil.padRight(v, 8); + } + + public void setYearMonth(final String v) { + yearMonth = ConvUtil.padRight(v, 6); + } + + public void setTotalHours(final BigDecimal v) { + totalHours = (v == null) ? BigDecimal.ZERO : v; + } + + public void setAbsentCount(final int v) { + absentCount = v; + } + + public String getEmpId() { + return empId; + } + + public String getYearMonth() { + return yearMonth; + } + + public BigDecimal getTotalHours() { + return totalHours; + } + + public int getAbsentCount() { + return absentCount; + } + + /** 80B 固定長レコード行を生成(FORMAT WRITE 相当)。 */ + public String toRecordLine() { + return empId + + yearMonth + + ConvUtil.formatZoned(totalHours, 4, 1) + + ConvUtil.padLeftZero(absentCount, 2) + + ConvUtil.padRight("", 59); // FILLER X(059) + } +} diff --git a/JavaSrc/src/dto/Kyu03Rec.java b/JavaSrc/src/dto/Kyu03Rec.java new file mode 100644 index 0000000..57a5ddf --- /dev/null +++ b/JavaSrc/src/dto/Kyu03Rec.java @@ -0,0 +1,120 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU03REC(SALARY-DETAIL 給与明細レコード 240B) + * ============================================================ + * 01 R01INNREC.(FD R01INNFIL / COPY KYU03REC REPLACING ==(A)== BY ==R01==) + * 03 R01EMP-ID PIC X(008). → empId + * 03 R01EMP-NAME PIC X(040). → empName + * 03 R01DEPT-CODE PIC X(002). → deptCode + * 03 R01BASE-SALARY PIC 9(009). → baseSalary + * 03 R01OVT-AMOUNT PIC 9(009). → ovtAmount + * 03 R01HOLIDAY-AMOUNT PIC 9(009). → holidayAmount + * 03 R01ABSENT-DEDUCT PIC 9(009). → absentDeduct + * 03 R01GROSS-PAYMENT PIC 9(009). → grossPayment + * 03 R01OVT-HOURS PIC 9(004)V9(001). → ovtHours(5桁ゾーン整数) + * 03 R01ABSENT-HOURS PIC 9(004)V9(001). → absentHours(5桁ゾーン整数) + * 03 R01ATTEND-TYPE PIC X(001). → attendType + * 03 R01ALLOWANCE-TYPE PIC X(003). → allowanceType + * 03 FILLER PIC X(131). → 固定長レイアウト維持用(非保持) + * + * 入力ファイルは FB 240B 固定長。setRecordLine() で位置ごとに切出し + * (COBOL の参照修正/固定長 MOVE 相当:05 層定義に合わせた substring)。 + */ +public class Kyu03Rec { + + /** 03 R01EMP-ID PIC X(008). */ + private String empId = ""; + /** 03 R01EMP-NAME PIC X(040). */ + private String empName = ""; + /** 03 R01DEPT-CODE PIC X(002). */ + private String deptCode = ""; + /** 03 R01BASE-SALARY PIC 9(009). */ + private int baseSalary; + /** 03 R01OVT-AMOUNT PIC 9(009). */ + private int ovtAmount; + /** 03 R01HOLIDAY-AMOUNT PIC 9(009). */ + private int holidayAmount; + /** 03 R01ABSENT-DEDUCT PIC 9(009). */ + private int absentDeduct; + /** 03 R01GROSS-PAYMENT PIC 9(009). */ + private int grossPayment; + /** 03 R01OVT-HOURS PIC 9(004)V9(001)(5桁ゾーン整数). */ + private long ovtHours; + /** 03 R01ABSENT-HOURS PIC 9(004)V9(001)(5桁ゾーン整数). */ + private long absentHours; + /** 03 R01ATTEND-TYPE PIC X(001). */ + private String attendType = ""; + /** 03 R01ALLOWANCE-TYPE PIC X(003). */ + private String allowanceType = ""; + + /** + * 240B 固定長行を 05 層定義の位置で切出してセット(READ R01INNFIL 相当)。 + * @param line 物理レコード行(FB 240B;短い場合は safeSub で欠損部を空文字扱い) + */ + public void setRecordLine(final String line) { + empId = ConvUtil.safeSub(line, 0, 8); // (A)EMP-ID X(008) + empName = ConvUtil.safeSub(line, 8, 48); // (A)EMP-NAME X(040) + deptCode = ConvUtil.safeSub(line, 48, 50); // (A)DEPT-CODE X(002) + baseSalary = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 50, 59), 0); // (A)BASE-SALARY 9(009) + ovtAmount = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 59, 68), 0); // (A)OVT-AMOUNT 9(009) + holidayAmount = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 68, 77), 0); + absentDeduct = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 77, 86), 0); + grossPayment = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 86, 95), 0); // (A)GROSS-PAYMENT 9(009) + ovtHours = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 95, 100), 0); // 9(004)V9(001) + absentHours = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 100, 105), 0); + attendType = ConvUtil.safeSub(line, 105, 106); // (A)ATTEND-TYPE X(001) + allowanceType = ConvUtil.safeSub(line, 106, 109); // (A)ALLOWANCE-TYPE X(003) + } + + public String getEmpId() { + return empId; + } + + public String getEmpName() { + return empName; + } + + public String getDeptCode() { + return deptCode; + } + + public int getBaseSalary() { + return baseSalary; + } + + public int getOvtAmount() { + return ovtAmount; + } + + public int getHolidayAmount() { + return holidayAmount; + } + + public int getAbsentDeduct() { + return absentDeduct; + } + + public int getGrossPayment() { + return grossPayment; + } + + public long getOvtHours() { + return ovtHours; + } + + public long getAbsentHours() { + return absentHours; + } + + public String getAttendType() { + return attendType; + } + + public String getAllowanceType() { + return allowanceType; + } +} diff --git a/JavaSrc/src/dto/Kyu04Rec.java b/JavaSrc/src/dto/Kyu04Rec.java new file mode 100644 index 0000000..77e9d6d --- /dev/null +++ b/JavaSrc/src/dto/Kyu04Rec.java @@ -0,0 +1,169 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU04REC(SALARY-NET 差引支給額レコード 200B)※プロジェクト共通DTO + * ============================================================ + * 使用元:KYU05DED(出力) / KYU06UPD(入力R02) / KYU07DIV(入力) / KYU08PRI(入力R01) / KYU09MRG(入力M01/M02) + * したがって本クラスが KYU04REC の唯一の正DOT であり、全プログラムで共有する。 + * + * 01 R0xOUTREC.(FD / COPY KYU04REC REPLACING ==(A)== BY ==R0x==) + * 03 R0xEMP-ID PIC X(008). -> empId / getR01EmpId + * 03 R0xREC-TYPE PIC X(001). -> recType / getR01RecType + * 03 R0xEMP-NAME PIC X(040). -> empName / getR01EmpName + * 03 R0xDEPT-CODE PIC X(002). -> deptCode / getR01DeptCode + * 03 R0xREGION-CODE PIC X(002). -> regionCode / getR01RegionCode + * 03 R0xCATEGORY-CODE PIC X(003). -> categoryCode / getR01CategoryCode + * 03 R0xGROSS-PAYMENT PIC 9(009). -> grossPayment / getR01GrossPayment + * 03 R0xINCOME-TAX PIC 9(009). -> incomeTax / getR01IncomeTax + * 03 R0xINSURANCE PIC 9(009). -> insurance / getR01Insurance + * 03 R0xRESIDENT-TAX PIC 9(009). -> residentTax / getR01ResidentTax + * 03 R0xNET-PAYMENT PIC 9(009). -> netPayment / getR01NetPayment + * 03 R0xTAXABLE-INCOME PIC 9(009). -> taxableIncome / getR01TaxableIncome + * 03 R0xPAY-AMOUNT PIC 9(009). -> payAmount / getR01PayAmount + * 03 FILLER PIC X(081). -> 固定スペース + * + * 各プログラムの参照名(R0xEMP-ID 等)の違いを吸収するため、標準名(getEmpId等)と + * R01接頭辞名(getR01EmpId等)の両方を提供する(同一フィールドへの別名アクセサ)。 + * fromRecordLine / parseRecord はいずれも 200B 固定長を解析(等価)。 + */ +public class Kyu04Rec { + + private String empId = ""; + private String recType = ""; + private String empName = ""; + private String deptCode = ""; + private String regionCode = ""; + private String categoryCode = ""; + private long grossPayment; + private long incomeTax; + private long insurance; + private long residentTax; + private long netPayment; + private long taxableIncome; + private long payAmount; + + /** INITIALIZE 相当。 */ + public void reset() { + empId = ""; + recType = ""; + empName = ""; + deptCode = ""; + regionCode = ""; + categoryCode = ""; + grossPayment = 0; + incomeTax = 0; + insurance = 0; + residentTax = 0; + netPayment = 0; + taxableIncome = 0; + payAmount = 0; + } + + // ---------- 標準アクセサ(getXxx / setXxx) ---------- + public void setEmpId(final String v) { empId = ConvUtil.padRight(v, 8); } + public String getEmpId() { return empId; } + public void setRecType(final String v) { recType = ConvUtil.padRight(v, 1); } + public String getRecType() { return recType; } + public void setEmpName(final String v) { empName = ConvUtil.padRight(v, 40); } + public String getEmpName() { return empName; } + public void setDeptCode(final String v) { deptCode = ConvUtil.padRight(v, 2); } + public String getDeptCode() { return deptCode; } + public void setRegionCode(final String v) { regionCode = ConvUtil.padRight(v, 2); } + public String getRegionCode() { return regionCode; } + public void setCategoryCode(final String v) { categoryCode = ConvUtil.padRight(v, 3); } + public String getCategoryCode() { return categoryCode; } + public void setGrossPayment(final long v) { grossPayment = v; } + public long getGrossPayment() { return grossPayment; } + public void setIncomeTax(final long v) { incomeTax = v; } + public long getIncomeTax() { return incomeTax; } + public void setInsurance(final long v) { insurance = v; } + public long getInsurance() { return insurance; } + public void setResidentTax(final long v) { residentTax = v; } + public long getResidentTax() { return residentTax; } + public void setNetPayment(final long v) { netPayment = v; } + public long getNetPayment() { return netPayment; } + public void setTaxableIncome(final long v) { taxableIncome = v; } + public long getTaxableIncome() { return taxableIncome; } + public void setPayAmount(final long v) { payAmount = v; } + public long getPayAmount() { return payAmount; } + + // ---------- R01接頭辞別名(他PGMの参照名互換) ---------- + public void setR01EmpId(final String v) { setEmpId(v); } + public String getR01EmpId() { return empId; } + public void setR01RecType(final String v) { setRecType(v); } + public String getR01RecType() { return recType; } + public void setR01EmpName(final String v) { setEmpName(v); } + public String getR01EmpName() { return empName; } + public void setR01DeptCode(final String v) { setDeptCode(v); } + public String getR01DeptCode() { return deptCode; } + public void setR01RegionCode(final String v) { setRegionCode(v); } + public String getR01RegionCode() { return regionCode; } + public void setR01CategoryCode(final String v) { setCategoryCode(v); } + public String getR01CategoryCode() { return categoryCode; } + public void setR01GrossPayment(final long v) { grossPayment = v; } + public long getR01GrossPayment() { return grossPayment; } + public void setR01IncomeTax(final long v) { incomeTax = v; } + public long getR01IncomeTax() { return incomeTax; } + public void setR01Insurance(final long v) { insurance = v; } + public long getR01Insurance() { return insurance; } + public void setR01ResidentTax(final long v) { residentTax = v; } + public long getR01ResidentTax() { return residentTax; } + public void setR01NetPayment(final long v) { netPayment = v; } + public long getR01NetPayment() { return netPayment; } + public void setR01TaxableIncome(final long v) { taxableIncome = v; } + public long getR01TaxableIncome() { return taxableIncome; } + public void setR01PayAmount(final long v) { payAmount = v; } + public long getR01PayAmount() { return payAmount; } + + // ---------- 固定長レコード解析 ---------- + private static String sub(final String s, final int a, final int b) { + String x = (s == null) ? "" : s; + if (x.length() < b) { + x = ConvUtil.padRight(x, b); + } + return x.substring(a, b); + } + + /** 200B 固定長 SALARY-NET レコードを解析(COPY KYU04REC 05層順)。 */ + public void fromRecordLine(final String line) { + empId = sub(line, 0, 8); + recType = sub(line, 8, 9); + empName = sub(line, 9, 49); + deptCode = sub(line, 49, 51); + regionCode = sub(line, 51, 53); + categoryCode = sub(line, 53, 56); + grossPayment = ConvUtil.parseIntSafe(sub(line, 56, 65), 0); + incomeTax = ConvUtil.parseIntSafe(sub(line, 65, 74), 0); + insurance = ConvUtil.parseIntSafe(sub(line, 74, 83), 0); + residentTax = ConvUtil.parseIntSafe(sub(line, 83, 92), 0); + netPayment = ConvUtil.parseIntSafe(sub(line, 92, 101), 0); + taxableIncome = ConvUtil.parseIntSafe(sub(line, 101, 110), 0); + payAmount = ConvUtil.parseIntSafe(sub(line, 110, 119), 0); + } + + /** parseRecord は fromRecordLine と等価(PGM間の命名差を吸収)。 */ + public void parseRecord(final String line) { + fromRecordLine(line); + } + + /** 200B 固定長レコード行を生成(FORMAT WRITE)。 */ + public String toRecordLine() { + return ConvUtil.padRight(empId, 8) + + ConvUtil.padRight(recType, 1) + + ConvUtil.padRight(empName, 40) + + ConvUtil.padRight(deptCode, 2) + + ConvUtil.padRight(regionCode, 2) + + ConvUtil.padRight(categoryCode, 3) + + ConvUtil.padLeftZero(grossPayment, 9) + + ConvUtil.padLeftZero(incomeTax, 9) + + ConvUtil.padLeftZero(insurance, 9) + + ConvUtil.padLeftZero(residentTax, 9) + + ConvUtil.padLeftZero(netPayment, 9) + + ConvUtil.padLeftZero(taxableIncome, 9) + + ConvUtil.padLeftZero(payAmount, 9) + + ConvUtil.padRight("", 81); + } +} diff --git a/JavaSrc/src/dto/Kyu05Rec.java b/JavaSrc/src/dto/Kyu05Rec.java new file mode 100644 index 0000000..0568c0a --- /dev/null +++ b/JavaSrc/src/dto/Kyu05Rec.java @@ -0,0 +1,144 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU05REC(PAYSLIP 給与明細印刷レコード 200B) + * ============================================================ + * 01 W01OUTREC.(FD W01OUTFIL / COPY KYU05REC REPLACING ==(A)== BY ==W01==) + * 03 W01EMP-ID PIC X(008). → w01EmpId + * 03 FILLER PIC X(002). → 固定スペース + * 03 W01EMP-NAME PIC X(020) → w01EmpName(JUSTIFIED RIGHT で右詰め編集) + * 03 FILLER PIC X(002). → 固定スペース + * 03 W01BASIC-SALARY PIC Z(008)9 → w01BasicSalary(編集出力) + * 03 FILLER PIC X(002). → 固定スペース + * 03 W01NET-PAYMENT PIC Z(008)9 → w01NetPayment(編集出力) + * 03 FILLER PIC X(002). → 固定スペース + * 03 W01PAGE-TITLE PIC X(040). → w01PageTitle + * 03 FILLER PIC X(106). → 固定スペース + * + * 編集仕様: + * - W01EMP-NAME は X(020) JUSTIFIED RIGHT → 右詰め(元が 40B の場合は右端20Bを保持)。 + * - W01BASIC-SALARY / W01NET-PAYMENT は PIC Z(008)9 BLANK WHEN ZERO → + * 9桁の内先頭8桁が Z(ゼロ抑制→SPACE)、最終1桁は 9(常時表示)。 + * 値が 0 の場合は全体を SPACE 化(BLANK WHEN ZERO)。 + * + * 本 DTO は明細行(DETAIL-LINE)の出力に使用する。DETAIL-LINE は KYU05REC レイアウトと + * 先頭54B が一致し、PAGE-TITLE を SPACE とすることで尾部146B(FILLER) も一致する。 + * ヘッダ行(HEADER-LINE)/フッタ行(FOOTER-LINE)は KYU05REC 布局と異なるため + * 呼出側(Kyu08PriMain)で個別に編集する。 + */ +public class Kyu05Rec { + + /** 03 W01EMP-ID PIC X(008). */ + private String w01EmpId = ""; + /** 03 W01EMP-NAME PIC X(020) JUSTIFIED RIGHT.(生値を保持、編集時に右詰め) */ + private String w01EmpName = ""; + /** 03 W01BASIC-SALARY PIC Z(008)9 BLANK WHEN ZERO. */ + private int w01BasicSalary; + /** 03 W01NET-PAYMENT PIC Z(008)9 BLANK WHEN ZERO. */ + private int w01NetPayment; + /** 03 W01PAGE-TITLE PIC X(040). */ + private String w01PageTitle = ""; + + /** INITIALIZE W01OUTREC 相当:数値→0、文字→SPACE。 */ + public void reset() { + w01EmpId = ""; + w01EmpName = ""; + w01BasicSalary = 0; + w01NetPayment = 0; + w01PageTitle = ""; + } + + public void setW01EmpId(final String v) { + w01EmpId = ConvUtil.padRight(v, 8); + } + + public void setW01EmpName(final String v) { + w01EmpName = (v == null) ? "" : v; + } + + public void setW01BasicSalary(final int v) { + w01BasicSalary = v; + } + + public void setW01NetPayment(final int v) { + w01NetPayment = v; + } + + public void setW01PageTitle(final String v) { + w01PageTitle = ConvUtil.padRight(v, 40); + } + + public String getW01EmpId() { + return w01EmpId; + } + + public String getW01EmpName() { + return w01EmpName; + } + + public int getW01BasicSalary() { + return w01BasicSalary; + } + + public int getW01NetPayment() { + return w01NetPayment; + } + + public String getW01PageTitle() { + return w01PageTitle; + } + + /** + * PIC Z(n)9 BLANK WHEN ZERO 編集。 + * @param val 数値 + * @param zCount Z の桁数(表示桁数 = zCount + 1) + * @return zCount+1 桁の編集済文字列(先頭 zCount 桁はゼロ抑制→SPACE、最終桁は常時表示) + */ + private static String editZ(final int val, final int zCount) { + final int len = zCount + 1; + if (val == 0) { + // BLANK WHEN ZERO:全体を SPACE 化 + return ConvUtil.padRight("", len); + } + final String s = ConvUtil.padLeftZero(val, len); + final char[] ca = s.toCharArray(); + for (int i = 0; i < zCount; i++) { + if (ca[i] == '0') { + ca[i] = ' '; + } else { + break; + } + } + return new String(ca); + } + + /** X(len) JUSTIFIED RIGHT 編集(元が長い場合は右端 len 桁を保持)。 */ + private static String rightJustify(final String v, final int len) { + String s = (v == null) ? "" : v; + if (s.length() > len) { + return s.substring(s.length() - len); + } + final StringBuilder sb = new StringBuilder(); + while (sb.length() < len - s.length()) { + sb.append(' '); + } + return sb.append(s).toString(); + } + + /** 200B 固定長レコード行(明細行)を生成。 */ + public String toRecordLine() { + return w01EmpId + + ConvUtil.padRight("", 2) // FILLER X(002) + + rightJustify(w01EmpName, 20) // W01EMP-NAME X(020) JUSTIFIED RIGHT + + ConvUtil.padRight("", 2) // FILLER X(002) + + editZ(w01BasicSalary, 8) // W01BASIC-SALARY Z(008)9 + + ConvUtil.padRight("", 2) // FILLER X(002) + + editZ(w01NetPayment, 8) // W01NET-PAYMENT Z(008)9 + + ConvUtil.padRight("", 2) // FILLER X(002) + + w01PageTitle // W01PAGE-TITLE X(040) + + ConvUtil.padRight("", 106); // FILLER X(106) + } +} diff --git a/JavaSrc/src/dto/Kyu06Rec.java b/JavaSrc/src/dto/Kyu06Rec.java new file mode 100644 index 0000000..d205981 --- /dev/null +++ b/JavaSrc/src/dto/Kyu06Rec.java @@ -0,0 +1,68 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU06REC(PAYMENT-DATA 支給データレコード 200B) + * ============================================================ + * 01 W01OUTREC.(FD W01OUTFIL / COPY KYU06REC REPLACING ==(A)== BY ==W01==) + * 03 W01EMP-ID PIC X(008). → empId + * 03 W01PAY-TYPE PIC X(001). → payType('T'=支給合算) + * 03 W01AMOUNT PIC 9(009). → amount(給与+賞与) + * 03 W01BANK-CODE PIC X(004). → bankCode + * 03 W01ACCOUNT-NO PIC 9(008). → accountNo + * 03 FILLER PIC X(170). → 固定スペース + */ +public class Kyu06Rec { + + /** 03 W01EMP-ID PIC X(008). */ + private String empId = ""; + /** 03 W01PAY-TYPE PIC X(001). */ + private String payType = ""; + /** 03 W01AMOUNT PIC 9(009). */ + private int amount; + /** 03 W01BANK-CODE PIC X(004). */ + private String bankCode = ""; + /** 03 W01ACCOUNT-NO PIC 9(008). */ + private int accountNo; + + /** INITIALIZE W01OUTREC 相当:数値→0、文字→SPACE。 */ + public void reset() { + empId = ""; + payType = ""; + amount = 0; + bankCode = ""; + accountNo = 0; + } + + public void setEmpId(final String v) { + empId = ConvUtil.padRight(v, 8); + } + + public void setPayType(final String v) { + payType = ConvUtil.padRight(v, 1); + } + + public void setAmount(final int v) { + amount = v; + } + + public void setBankCode(final String v) { + bankCode = ConvUtil.padRight(v, 4); + } + + public void setAccountNo(final int v) { + accountNo = v; + } + + /** 200B 固定長レコード行を生成(FORMAT WRITE)。 */ + public String toRecordLine() { + return empId + + payType + + ConvUtil.padLeftZero(amount, 9) + + bankCode + + ConvUtil.padLeftZero(accountNo, 8) + + ConvUtil.padRight("", 170); // FILLER X(170) + } +} diff --git a/JavaSrc/src/dto/Kyu07R01Rec.java b/JavaSrc/src/dto/Kyu07R01Rec.java new file mode 100644 index 0000000..cb565af --- /dev/null +++ b/JavaSrc/src/dto/Kyu07R01Rec.java @@ -0,0 +1,216 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU04REC(SALARY-NET / DEPT-OUT 差引支給額レコード 200B) + * ============================================================ + * KYU07DIV 専用DTO。COPY KYU04REC はプロジェクト内に別レイアウトの + * dto.Kyu04Rec(DB-VIEW)が既存のため、本プログラム固有名 Kyu07R01Rec とする。 + * 01 R01INNREC.(FD R01INNFIL / COPY KYU04REC REPLACING ==(A)== BY ==R01==) + * 03 R01EMP-ID PIC X(008). → empId + * 03 R01REC-TYPE PIC X(001). → recType + * 03 R01EMP-NAME PIC X(040). → empName + * 03 R01DEPT-CODE PIC X(002). → deptCode + * 03 R01REGION-CODE PIC X(002). → regionCode + * 03 R01CATEGORY-CODE PIC X(003). → categoryCode + * 03 R01GROSS-PAYMENT PIC 9(009). → grossPayment + * 03 R01INCOME-TAX PIC 9(009). → incomeTax + * 03 R01INSURANCE PIC 9(009). → insurance + * 03 R01RESIDENT-TAX PIC 9(009). → residentTax + * 03 R01NET-PAYMENT PIC 9(009). → netPayment + * 03 R01TAXABLE-INCOME PIC 9(009). → taxableIncome + * 03 R01PAY-AMOUNT PIC 9(009). → payAmount + * 03 FILLER PIC X(081). → 固定スペース + * ※本レコードは R01(SALARY-NET)入力と D01〜D50(DEPT-OUT)出力で共通利用される。 + * DEPT-OUT 出力は「本レコードをそのまま 200B 出力」する(設計書 出力レコード定義準拠)。 + */ +public class Kyu07R01Rec { + + /** 03 R01EMP-ID PIC X(008). */ + private String empId = ""; + /** 03 R01REC-TYPE PIC X(001). */ + private String recType = ""; + /** 03 R01EMP-NAME PIC X(040). */ + private String empName = ""; + /** 03 R01DEPT-CODE PIC X(002).(分割キー) */ + private String deptCode = ""; + /** 03 R01REGION-CODE PIC X(002). */ + private String regionCode = ""; + /** 03 R01CATEGORY-CODE PIC X(003). */ + private String categoryCode = ""; + /** 03 R01GROSS-PAYMENT PIC 9(009). */ + private int grossPayment; + /** 03 R01INCOME-TAX PIC 9(009). */ + private int incomeTax; + /** 03 R01INSURANCE PIC 9(009). */ + private int insurance; + /** 03 R01RESIDENT-TAX PIC 9(009). */ + private int residentTax; + /** 03 R01NET-PAYMENT PIC 9(009). */ + private int netPayment; + /** 03 R01TAXABLE-INCOME PIC 9(009). */ + private int taxableIncome; + /** 03 R01PAY-AMOUNT PIC 9(009). */ + private int payAmount; + + /** INITIALIZE R01INNREC 相当:数値→0、文字→SPACE。 */ + public void reset() { + empId = ""; + recType = ""; + empName = ""; + deptCode = ""; + regionCode = ""; + categoryCode = ""; + grossPayment = 0; + incomeTax = 0; + insurance = 0; + residentTax = 0; + netPayment = 0; + taxableIncome = 0; + payAmount = 0; + } + + /** + * 固定長行(200B)から各項目を解析する(READ R01INNFIL INTO R01INNREC 相当)。 + * 不足分は SPACE / 0 で補完。 + */ + public void setRecordLine(final String line) { + empId = ConvUtil.padRight(ConvUtil.safeSub(line, 0, 8), 8); + recType = ConvUtil.padRight(ConvUtil.safeSub(line, 8, 9), 1); + empName = ConvUtil.padRight(ConvUtil.safeSub(line, 9, 49), 40); + deptCode = ConvUtil.padRight(ConvUtil.safeSub(line, 49, 51), 2); + regionCode = ConvUtil.padRight(ConvUtil.safeSub(line, 51, 53), 2); + categoryCode = ConvUtil.padRight(ConvUtil.safeSub(line, 53, 56), 3); + grossPayment = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 56, 65), 0); + incomeTax = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 65, 74), 0); + insurance = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 74, 83), 0); + residentTax = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 83, 92), 0); + netPayment = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 92, 101), 0); + taxableIncome = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 101, 110), 0); + payAmount = ConvUtil.parseIntSafe(ConvUtil.safeSub(line, 110, 119), 0); + } + + public void setEmpId(final String v) { + empId = ConvUtil.padRight(v, 8); + } + + public void setRecType(final String v) { + recType = ConvUtil.padRight(v, 1); + } + + public void setEmpName(final String v) { + empName = ConvUtil.padRight(v, 40); + } + + public void setDeptCode(final String v) { + deptCode = ConvUtil.padRight(v, 2); + } + + public void setRegionCode(final String v) { + regionCode = ConvUtil.padRight(v, 2); + } + + public void setCategoryCode(final String v) { + categoryCode = ConvUtil.padRight(v, 3); + } + + public void setGrossPayment(final int v) { + grossPayment = v; + } + + public void setIncomeTax(final int v) { + incomeTax = v; + } + + public void setInsurance(final int v) { + insurance = v; + } + + public void setResidentTax(final int v) { + residentTax = v; + } + + public void setNetPayment(final int v) { + netPayment = v; + } + + public void setTaxableIncome(final int v) { + taxableIncome = v; + } + + public void setPayAmount(final int v) { + payAmount = v; + } + + public String getEmpId() { + return empId; + } + + public String getRecType() { + return recType; + } + + public String getEmpName() { + return empName; + } + + public String getDeptCode() { + return deptCode; + } + + public String getRegionCode() { + return regionCode; + } + + public String getCategoryCode() { + return categoryCode; + } + + public int getGrossPayment() { + return grossPayment; + } + + public int getIncomeTax() { + return incomeTax; + } + + public int getInsurance() { + return insurance; + } + + public int getResidentTax() { + return residentTax; + } + + public int getNetPayment() { + return netPayment; + } + + public int getTaxableIncome() { + return taxableIncome; + } + + public int getPayAmount() { + return payAmount; + } + + /** 200B 固定長レコード行を生成(DEPT-OUT 出力 / WRITE ... FROM R01INNREC 相当)。 */ + public String toRecordLine() { + return empId + + recType + + empName + + deptCode + + regionCode + + categoryCode + + ConvUtil.padLeftZero(grossPayment, 9) + + ConvUtil.padLeftZero(incomeTax, 9) + + ConvUtil.padLeftZero(insurance, 9) + + ConvUtil.padLeftZero(residentTax, 9) + + ConvUtil.padLeftZero(netPayment, 9) + + ConvUtil.padLeftZero(taxableIncome, 9) + + ConvUtil.padLeftZero(payAmount, 9) + + ConvUtil.padRight("", 81); // FILLER X(081) + } +} diff --git a/JavaSrc/src/dto/Kyu99Rec.java b/JavaSrc/src/dto/Kyu99Rec.java new file mode 100644 index 0000000..a30ed42 --- /dev/null +++ b/JavaSrc/src/dto/Kyu99Rec.java @@ -0,0 +1,47 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】COPY KYU99REC(ERROR-LOG エラーログレコード VB 200B) + * ============================================================ + * 01 W02OUTREC.(FD W02OUTFIL / COPY KYU99REC REPLACING ==(A)== BY ==W02==) + * 03 W02ERR-CATEGORY PIC X(008). → errCategory('CSV-ERR' / 'EMP-ERR ' 等) + * 03 W02ERR-DETAIL PIC X(192). → errDetail(エラー内容) + * ※全 KYU プログラム共用。 + */ +public class Kyu99Rec { + + /** 03 W02ERR-CATEGORY PIC X(008). */ + private String errCategory = ""; + /** 03 W02ERR-DETAIL PIC X(192). */ + private String errDetail = ""; + + /** INITIALIZE W02OUTREC 相当。 */ + public void reset() { + errCategory = ""; + errDetail = ""; + } + + public void setErrCategory(final String v) { + errCategory = ConvUtil.padRight(v, 8); + } + + public void setErrDetail(final String v) { + errDetail = ConvUtil.padRight(v, 192); + } + + public String getErrCategory() { + return errCategory; + } + + public String getErrDetail() { + return errDetail; + } + + /** 200B 固定長レコード行を生成。 */ + public String toRecordLine() { + return errCategory + errDetail; + } +} diff --git a/JavaSrc/src/dto/Zan01Rec.java b/JavaSrc/src/dto/Zan01Rec.java new file mode 100644 index 0000000..868a9fe --- /dev/null +++ b/JavaSrc/src/dto/Zan01Rec.java @@ -0,0 +1,115 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】ZAN01REC 加班申請レコード(80B, FB) + * ============================================================ + * 使用プログラム: ZAN01CHK(W01/W02) ZAN02CHK(R01/W01) ZAN03CHK(R01/W01) ZAN04MAT(R01/R02) + * レイアウト: APPL-ID X(8) / EMP-ID 9(8) / APPL-DATE 9(8) / + * START-TIME 9(4) / END-TIME 9(4) / STATUS X(1) / + * OVT-TYPE X(1) / FILLER X(46) + */ +public class Zan01Rec { + + private String applId = ""; + private String empId = ""; + private String applDate = ""; + private String startTime = ""; + private String endTime = ""; + private String status = ""; + private String ovtType = ""; + private String filler = ""; + + public Zan01Rec() { + } + + /** 固定長行(80B) → 各項目. */ + public void fromRecordLine(final String line) { + applId = ConvUtil.safeSub(line, 0, 8).trim(); + empId = ConvUtil.safeSub(line, 8, 16).trim(); + applDate = ConvUtil.safeSub(line, 16, 24).trim(); + startTime = ConvUtil.safeSub(line, 24, 28).trim(); + endTime = ConvUtil.safeSub(line, 28, 32).trim(); + status = ConvUtil.safeSub(line, 32, 33); + ovtType = ConvUtil.safeSub(line, 33, 34); + filler = ConvUtil.safeSub(line, 34, 80); + } + + /** 各項目 → 固定長行(80B). */ + public String toRecordLine() { + return ConvUtil.padRight(applId, 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(empId, 0), 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(applDate, 0), 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(startTime, 0), 4) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(endTime, 0), 4) + + (status.isEmpty() ? " " : status.substring(0, 1)) + + (ovtType.isEmpty() ? " " : ovtType.substring(0, 1)) + + ConvUtil.padRight(filler, 46); + } + + public String getApplId() { + return applId; + } + + public void setApplId(String applId) { + this.applId = (applId == null) ? "" : applId; + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public String getApplDate() { + return applDate; + } + + public void setApplDate(String applDate) { + this.applDate = (applDate == null) ? "" : applDate; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = (startTime == null) ? "" : startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = (endTime == null) ? "" : endTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = (status == null) ? "" : status; + } + + public String getOvtType() { + return ovtType; + } + + public void setOvtType(String ovtType) { + this.ovtType = (ovtType == null) ? "" : ovtType; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = (filler == null) ? "" : filler; + } +} diff --git a/JavaSrc/src/dto/Zan02Rec.java b/JavaSrc/src/dto/Zan02Rec.java new file mode 100644 index 0000000..aa7a7d2 --- /dev/null +++ b/JavaSrc/src/dto/Zan02Rec.java @@ -0,0 +1,124 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】ZAN02REC マッチング結果レコード(80B, FB) + * ============================================================ + * 使用プログラム: ZAN04MAT(W01/OVT-MATCHED) ZAN05CAL(R01/OVT-SORTED2) + * レイアウト: APPL-ID X(8) / EMP-ID 9(8) / APPL-DATE 9(8) / + * START-TIME 9(4) / END-TIME 9(4) / STATUS X(1) / + * OVT-TYPE X(1) / PROC-SEQ 9(2) / FILLER X(44) + */ +public class Zan02Rec { + + private String applId = ""; + private String empId = ""; + private String applDate = ""; + private String startTime = ""; + private String endTime = ""; + private String status = ""; + private String ovtType = ""; + private String procSeq = ""; + private String filler = ""; + + public Zan02Rec() { + } + + public void fromRecordLine(final String line) { + applId = ConvUtil.safeSub(line, 0, 8).trim(); + empId = ConvUtil.safeSub(line, 8, 16).trim(); + applDate = ConvUtil.safeSub(line, 16, 24).trim(); + startTime = ConvUtil.safeSub(line, 24, 28).trim(); + endTime = ConvUtil.safeSub(line, 28, 32).trim(); + status = ConvUtil.safeSub(line, 32, 33); + ovtType = ConvUtil.safeSub(line, 33, 34); + procSeq = ConvUtil.safeSub(line, 34, 36).trim(); + filler = ConvUtil.safeSub(line, 36, 80); + } + + public String toRecordLine() { + return ConvUtil.padRight(applId, 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(empId, 0), 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(applDate, 0), 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(startTime, 0), 4) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(endTime, 0), 4) + + (status.isEmpty() ? " " : status.substring(0, 1)) + + (ovtType.isEmpty() ? " " : ovtType.substring(0, 1)) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(procSeq, 0), 2) + + ConvUtil.padRight(filler, 44); + } + + public String getApplId() { + return applId; + } + + public void setApplId(String applId) { + this.applId = (applId == null) ? "" : applId; + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public String getApplDate() { + return applDate; + } + + public void setApplDate(String applDate) { + this.applDate = (applDate == null) ? "" : applDate; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = (startTime == null) ? "" : startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = (endTime == null) ? "" : endTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = (status == null) ? "" : status; + } + + public String getOvtType() { + return ovtType; + } + + public void setOvtType(String ovtType) { + this.ovtType = (ovtType == null) ? "" : ovtType; + } + + public String getProcSeq() { + return procSeq; + } + + public void setProcSeq(String procSeq) { + this.procSeq = (procSeq == null) ? "" : procSeq; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = (filler == null) ? "" : filler; + } +} diff --git a/JavaSrc/src/dto/Zan03Rec.java b/JavaSrc/src/dto/Zan03Rec.java new file mode 100644 index 0000000..ef36a7c --- /dev/null +++ b/JavaSrc/src/dto/Zan03Rec.java @@ -0,0 +1,119 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】ZAN03REC 集約結果レコード(80B, FB) + * ============================================================ + * 使用プログラム: ZAN05CAL(W01/OVT-SUMMARY) ZAN06UPD(R01) + * レイアウト: APPL-ID X(8) / EMP-ID 9(8) / APPL-DATE 9(8) / + * START-TIME 9(4) / END-TIME 9(4) / OVT-HOURS 9(4)V9(1) / + * OVT-TYPE X(1) / FILLER X(42) + */ +public class Zan03Rec { + + private String applId = ""; + private String empId = ""; + private String applDate = ""; + private String startTime = ""; + private String endTime = ""; + private String ovtHours = ""; // 9(4)V9(1) → 5桁数字文字列 + private String ovtType = ""; + private String filler = ""; + + public Zan03Rec() { + } + + public void fromRecordLine(final String line) { + applId = ConvUtil.safeSub(line, 0, 8).trim(); + empId = ConvUtil.safeSub(line, 8, 16).trim(); + applDate = ConvUtil.safeSub(line, 16, 24).trim(); + startTime = ConvUtil.safeSub(line, 24, 28).trim(); + endTime = ConvUtil.safeSub(line, 28, 32).trim(); + ovtHours = ConvUtil.safeSub(line, 32, 37).trim(); + ovtType = ConvUtil.safeSub(line, 37, 38); + filler = ConvUtil.safeSub(line, 38, 80); + } + + public String toRecordLine() { + return ConvUtil.padRight(applId, 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(empId, 0), 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(applDate, 0), 8) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(startTime, 0), 4) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(endTime, 0), 4) + + ConvUtil.padLeftZero(ConvUtil.parseIntSafe(ovtHours, 0), 5) + + (ovtType.isEmpty() ? " " : ovtType.substring(0, 1)) + + ConvUtil.padRight(filler, 42); + } + + /** OVT-HOURS を0.1h単位の数値(double)から9(4)V9(1)文字列に設定(切捨). */ + public void setOvtHoursFromDouble(final double hrs) { + long tenth = (long) (hrs * 10.0); + this.ovtHours = ConvUtil.padLeftZero(tenth, 5); + } + + public String getApplId() { + return applId; + } + + public void setApplId(String applId) { + this.applId = (applId == null) ? "" : applId; + } + + public String getEmpId() { + return empId; + } + + public void setEmpId(String empId) { + this.empId = (empId == null) ? "" : empId; + } + + public String getApplDate() { + return applDate; + } + + public void setApplDate(String applDate) { + this.applDate = (applDate == null) ? "" : applDate; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = (startTime == null) ? "" : startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = (endTime == null) ? "" : endTime; + } + + public String getOvtHours() { + return ovtHours; + } + + public void setOvtHours(String ovtHours) { + this.ovtHours = (ovtHours == null) ? "" : ovtHours; + } + + public String getOvtType() { + return ovtType; + } + + public void setOvtType(String ovtType) { + this.ovtType = (ovtType == null) ? "" : ovtType; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = (filler == null) ? "" : filler; + } +} diff --git a/JavaSrc/src/dto/Zan04Rec.java b/JavaSrc/src/dto/Zan04Rec.java new file mode 100644 index 0000000..f530e5d --- /dev/null +++ b/JavaSrc/src/dto/Zan04Rec.java @@ -0,0 +1,45 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】ZAN04REC DBCLEAN対象レコード(80B, FB) + * ============================================================ + * 使用プログラム: ZAN04MAT(W02/OVT-DBCLEAN) ZAN06UPD(R02) + * レイアウト: APPL-ID X(8) / FILLER X(72) + */ +public class Zan04Rec { + + private String applId = ""; + private String filler = ""; + + public Zan04Rec() { + } + + public void fromRecordLine(final String line) { + applId = ConvUtil.safeSub(line, 0, 8).trim(); + filler = ConvUtil.safeSub(line, 8, 80); + } + + public String toRecordLine() { + return ConvUtil.padRight(applId, 8) + + ConvUtil.padRight(filler, 72); + } + + public String getApplId() { + return applId; + } + + public void setApplId(String applId) { + this.applId = (applId == null) ? "" : applId; + } + + public String getFiller() { + return filler; + } + + public void setFiller(String filler) { + this.filler = (filler == null) ? "" : filler; + } +} diff --git a/JavaSrc/src/dto/Zan05Rec.java b/JavaSrc/src/dto/Zan05Rec.java new file mode 100644 index 0000000..d63ce22 --- /dev/null +++ b/JavaSrc/src/dto/Zan05Rec.java @@ -0,0 +1,47 @@ +package dto; + +import util.ConvUtil; + +/** + * ============================================================ + * 【COBOL→Java】ZAN05REC エラーログレコード(VB 200B) + * ============================================================ + * 使用プログラム: ZAN01CHK(W03) ZAN02CHK(W02) ZAN03CHK(W02) + * ZAN04MAT(W03) ZAN06UPD(W01) + * レイアウト: ERR-CATEGORY 9(2) / ERR-DETAIL X(198) + */ +public class Zan05Rec { + + private String errCategory = ""; + private String errDetail = ""; + + public Zan05Rec() { + } + + public void fromRecordLine(final String line) { + errCategory = ConvUtil.safeSub(line, 0, 2).trim(); + errDetail = ConvUtil.safeSub(line, 2, 200); + } + + /** VBレコード出力用(200B): カテゴリ(2) + 詳細(198). */ + public String toRecordLine() { + return ConvUtil.padLeftZero(ConvUtil.parseIntSafe(errCategory, 0), 2) + + ConvUtil.padRight(errDetail, 198); + } + + public String getErrCategory() { + return errCategory; + } + + public void setErrCategory(String errCategory) { + this.errCategory = (errCategory == null) ? "" : errCategory; + } + + public String getErrDetail() { + return errDetail; + } + + public void setErrDetail(String errDetail) { + this.errDetail = (errDetail == null) ? "" : errDetail; + } +} diff --git a/JavaSrc/src/dto/ZanchkacPar.java b/JavaSrc/src/dto/ZanchkacPar.java new file mode 100644 index 0000000..15cc54f --- /dev/null +++ b/JavaSrc/src/dto/ZanchkacPar.java @@ -0,0 +1,53 @@ +package dto; + +/** + * ============================================================ + * 【COBOL→Java】COPY ZANCHKAC(SUB04CHK 項目チェック用連絡領域) + * ============================================================ + * 01 C01CHKPAR. + * 03 C01CHKTYP PIC X(008). → c01ChkTyp(チェックタイプ DATE/TIME/NUM/EMPID) + * 03 C01CHKDAT PIC X(080). → c01ChkDat(チェック対象データ) + * 03 C01CHKRRC PIC 9(004). → c01ChkRrc(復帰コード 0=正常) + */ +public class ZanchkacPar { + + /** 03 C01CHKTYP PIC X(008). */ + private String c01ChkTyp = ""; + + /** 03 C01CHKDAT PIC X(080). */ + private String c01ChkDat = ""; + + /** 03 C01CHKRRC PIC 9(004). */ + private int c01ChkRrc; + + /** INITIALIZE C01CHKPAR 相当。 */ + public void reset() { + c01ChkTyp = ""; + c01ChkDat = ""; + c01ChkRrc = 0; + } + + public String getC01ChkTyp() { + return c01ChkTyp; + } + + public void setC01ChkTyp(String c01ChkTyp) { + this.c01ChkTyp = (c01ChkTyp == null) ? "" : c01ChkTyp; + } + + public String getC01ChkDat() { + return c01ChkDat; + } + + public void setC01ChkDat(String c01ChkDat) { + this.c01ChkDat = (c01ChkDat == null) ? "" : c01ChkDat; + } + + public int getC01ChkRrc() { + return c01ChkRrc; + } + + public void setC01ChkRrc(int c01ChkRrc) { + this.c01ChkRrc = c01ChkRrc; + } +} diff --git a/JavaSrc/src/dto/ZandatacPar.java b/JavaSrc/src/dto/ZandatacPar.java new file mode 100644 index 0000000..159f2ee --- /dev/null +++ b/JavaSrc/src/dto/ZandatacPar.java @@ -0,0 +1,40 @@ +package dto; + +/** + * ============================================================ + * 【COBOL→Java】COPY ZANDATAC(SUB01DAT 運用日付取得用連絡領域) + * ============================================================ + * 01 D01UBSPAR. + * 03 D01FKICOD PIC S9(004) COMP. → d01FkiCod(復帰コード 0=正常) + * 03 D01UBSUDATE PIC 9(008). → d01UbsUdate(運用日付 YYYYMMDD) + */ +public class ZandatacPar { + + /** 03 D01FKICOD PIC S9(004) COMP. */ + private int d01FkiCod; + + /** 03 D01UBSUDATE PIC 9(008). */ + private int d01UbsUdate; + + /** INITIALIZE D01UBSPAR 相当。 */ + public void reset() { + d01FkiCod = 0; + d01UbsUdate = 0; + } + + public int getD01FkiCod() { + return d01FkiCod; + } + + public void setD01FkiCod(int d01FkiCod) { + this.d01FkiCod = d01FkiCod; + } + + public int getD01UbsUdate() { + return d01UbsUdate; + } + + public void setD01UbsUdate(int d01UbsUdate) { + this.d01UbsUdate = d01UbsUdate; + } +} diff --git a/JavaSrc/src/dto/ZanendacPar.java b/JavaSrc/src/dto/ZanendacPar.java new file mode 100644 index 0000000..66b4508 --- /dev/null +++ b/JavaSrc/src/dto/ZanendacPar.java @@ -0,0 +1,27 @@ +package dto; + +/** + * ============================================================ + * 【COBOL→Java】COPY ZANENDAC(SUB03END ABEND処理用連絡領域) + * ============================================================ + * 01 E01ABDPAR. + * 03 E01ABDCOD PIC 9(003). → e01AbdCod(ABENDコード) + */ +public class ZanendacPar { + + /** 03 E01ABDCOD PIC 9(003). */ + private int e01AbdCod; + + /** INITIALIZE E01ABDPAR 相当。 */ + public void reset() { + e01AbdCod = 0; + } + + public int getE01AbdCod() { + return e01AbdCod; + } + + public void setE01AbdCod(int e01AbdCod) { + this.e01AbdCod = e01AbdCod; + } +} diff --git a/JavaSrc/src/dto/ZanmsgacPar.java b/JavaSrc/src/dto/ZanmsgacPar.java new file mode 100644 index 0000000..01fb2e3 --- /dev/null +++ b/JavaSrc/src/dto/ZanmsgacPar.java @@ -0,0 +1,150 @@ +package dto; + +/** + * ============================================================ + * 【COBOL→Java】COPY ZANMSGAC(SUB02MSG メッセージ出力用連絡領域) + * ============================================================ + * 01 M00MHOPAR. + * 03 M00MSGCOD PIC 9(003). → m00MsgCod(メッセージ番号) + * 03 M00UMKDATS22-01 PIC X(030). → m00Umkdats2201(置換項目1) + * ~ (置換項目2~10 同型) + * 03 M00UMKDATS22-10 PIC X(030). → m00Umkdats2210 + */ +public class ZanmsgacPar { + + /** 03 M00MSGCOD PIC 9(003). */ + private int m00MsgCod; + + /** 03 M00UMKDATS22-01 PIC X(030). */ + private String m00Umkdats2201 = ""; + + /** 03 M00UMKDATS22-02 PIC X(030). */ + private String m00Umkdats2202 = ""; + + /** 03 M00UMKDATS22-03 PIC X(030). */ + private String m00Umkdats2203 = ""; + + /** 03 M00UMKDATS22-04 PIC X(030). */ + private String m00Umkdats2204 = ""; + + /** 03 M00UMKDATS22-05 PIC X(030). */ + private String m00Umkdats2205 = ""; + + /** 03 M00UMKDATS22-06 PIC X(030). */ + private String m00Umkdats2206 = ""; + + /** 03 M00UMKDATS22-07 PIC X(030). */ + private String m00Umkdats2207 = ""; + + /** 03 M00UMKDATS22-08 PIC X(030). */ + private String m00Umkdats2208 = ""; + + /** 03 M00UMKDATS22-09 PIC X(030). */ + private String m00Umkdats2209 = ""; + + /** 03 M00UMKDATS22-10 PIC X(030). */ + private String m00Umkdats2210 = ""; + + /** INITIALIZE M00MHOPAR 相当:数値→0、文字→SPACE。 */ + public void reset() { + m00MsgCod = 0; + m00Umkdats2201 = ""; + m00Umkdats2202 = ""; + m00Umkdats2203 = ""; + m00Umkdats2204 = ""; + m00Umkdats2205 = ""; + m00Umkdats2206 = ""; + m00Umkdats2207 = ""; + m00Umkdats2208 = ""; + m00Umkdats2209 = ""; + m00Umkdats2210 = ""; + } + + public int getM00MsgCod() { + return m00MsgCod; + } + + public void setM00MsgCod(int m00MsgCod) { + this.m00MsgCod = m00MsgCod; + } + + public String getM00Umkdats2201() { + return m00Umkdats2201; + } + + public void setM00Umkdats2201(String v) { + this.m00Umkdats2201 = (v == null) ? "" : v; + } + + public String getM00Umkdats2202() { + return m00Umkdats2202; + } + + public void setM00Umkdats2202(String v) { + this.m00Umkdats2202 = (v == null) ? "" : v; + } + + public String getM00Umkdats2203() { + return m00Umkdats2203; + } + + public void setM00Umkdats2203(String v) { + this.m00Umkdats2203 = (v == null) ? "" : v; + } + + public String getM00Umkdats2204() { + return m00Umkdats2204; + } + + public void setM00Umkdats2204(String v) { + this.m00Umkdats2204 = (v == null) ? "" : v; + } + + public String getM00Umkdats2205() { + return m00Umkdats2205; + } + + public void setM00Umkdats2205(String v) { + this.m00Umkdats2205 = (v == null) ? "" : v; + } + + public String getM00Umkdats2206() { + return m00Umkdats2206; + } + + public void setM00Umkdats2206(String v) { + this.m00Umkdats2206 = (v == null) ? "" : v; + } + + public String getM00Umkdats2207() { + return m00Umkdats2207; + } + + public void setM00Umkdats2207(String v) { + this.m00Umkdats2207 = (v == null) ? "" : v; + } + + public String getM00Umkdats2208() { + return m00Umkdats2208; + } + + public void setM00Umkdats2208(String v) { + this.m00Umkdats2208 = (v == null) ? "" : v; + } + + public String getM00Umkdats2209() { + return m00Umkdats2209; + } + + public void setM00Umkdats2209(String v) { + this.m00Umkdats2209 = (v == null) ? "" : v; + } + + public String getM00Umkdats2210() { + return m00Umkdats2210; + } + + public void setM00Umkdats2210(String v) { + this.m00Umkdats2210 = (v == null) ? "" : v; + } +} diff --git a/JavaSrc/src/dto/ZantimacPar.java b/JavaSrc/src/dto/ZantimacPar.java new file mode 100644 index 0000000..6bde058 --- /dev/null +++ b/JavaSrc/src/dto/ZantimacPar.java @@ -0,0 +1,51 @@ +package dto; + +/** + * ============================================================ + * 【COBOL→Java】COPY ZANTIMAC(SUB05TIM 時刻丸め用連絡領域) + * ============================================================ + * 01 T01TIMPAR. + * 03 T01TIMHRS PIC 9(004)V9(001). → t01Timhrs(入力時間、1桁小数) + * 03 T01TIMOUT PIC 9(004)V9(001). → t01Timout(出力時間、1桁小数) + * 03 T01TIMRRC PIC 9(004). → t01Timrrc(丸めモード) + */ +public class ZantimacPar { + + /** 03 T01TIMHRS PIC 9(004)V9(001). */ + private double t01Timhrs; + /** 03 T01TIMOUT PIC 9(004)V9(001). */ + private double t01Timout; + /** 03 T01TIMRRC PIC 9(004). */ + private int t01Timrrc; + + /** INITIALIZE T01TIMPAR 相当。 */ + public void reset() { + t01Timhrs = 0; + t01Timout = 0; + t01Timrrc = 0; + } + + public double getT01Timhrs() { + return t01Timhrs; + } + + public void setT01Timhrs(double v) { + this.t01Timhrs = v; + } + + public double getT01Timout() { + return t01Timout; + } + + public void setT01Timout(double v) { + this.t01Timout = v; + } + + public int getT01Timrrc() { + return t01Timrrc; + } + + public void setT01Timrrc(int v) { + this.t01Timrrc = v; + } +} diff --git a/JavaSrc/src/util/ConvUtil.java b/JavaSrc/src/util/ConvUtil.java new file mode 100644 index 0000000..dfb05db --- /dev/null +++ b/JavaSrc/src/util/ConvUtil.java @@ -0,0 +1,144 @@ +package util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * ============================================================ + * 【COBOL→Java 标准例】PIC 数据转换工具类 + * ============================================================ + * COBOL 的 PIC 子句与 Java 类型之间的转换统一收敛到本类, + * 保证全项目转换规则一致。 + * + * 基本类型对应规则: + * PIC X(n) → String (定长字符串,不足右补空格) + * PIC 9(n) → int / long (无小数数值) + * PIC 9(n)V9(m) → BigDecimal (带隐含小数点的金额等) + * PIC S9(...) → 同上(符号由 Java 类型的符号承载) + * 88 级条件名 → static final 常量 + isXxx() 判定方法 + */ +public final class ConvUtil { + + private ConvUtil() { + } + + /** + * PIC X(n):字符串左对齐、右侧补空格到指定长度。 + * COBOL: MOVE "ABC" TO WS-X-10 (WS-X-10 PIC X(10)) + */ + public static String padRight(final String val, final int len) { + String s = (val == null) ? "" : val; + if (s.length() >= len) { + return s.substring(0, len); + } + StringBuilder sb = new StringBuilder(s); + while (sb.length() < len) { + sb.append(' '); + } + return sb.toString(); + } + + /** + * PIC 9(n):数值左对齐(左侧补零)到指定长度。 + * COBOL: MOVE 123 TO WS-N-5 (WS-N-5 PIC 9(5)) → "00123" + * 超长时高位截断,与 COBOL 高位丢失的行为一致。 + */ + public static String padLeftZero(final long val, final int len) { + return padLeftZeroStr(Long.toString(val), len); + } + + /** + * PIC 9(n):数字文字列左对齐(左侧补零)到指定长度。 + * COBOL: MOVE "20260610" TO WS-DATE (WS-DATE PIC 9(8)) → "20260610" + * null / 空文字はそのまま(呼出側が満桁ゼロ埋めを保証しない場合の安全性)。 + */ + public static String padLeftZero(final String val, final int len) { + return padLeftZeroStr(val, len); + } + + private static String padLeftZeroStr(final String digits, final int len) { + String s = (digits == null) ? "" : digits; + if (s.length() >= len) { + return s.substring(s.length() - len); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len - s.length(); i++) { + sb.append('0'); + } + return sb.append(s).toString(); + } + + /** + * PIC 9(n)V9(m) → BigDecimal:外部数据是"无小数点的数字串",按隐含小数位还原。 + * COBOL: WS-SALARY PIC 9(7)V99,文件中为 "003500000" → 35000.00 + * SPACE / LOW-VALUE / 非数字按 0 处理。 + */ + public static BigDecimal toDecimal(final String zoned, final int scale) { + String s = (zoned == null) ? "" : zoned.trim(); + if (!isNumeric(s)) { + return BigDecimal.ZERO; + } + return new BigDecimal(s).movePointLeft(scale); + } + + /** + * BigDecimal → PIC 9(n)V9(m) 定长数字串:四舍五入(ROUNDED)、去小数点、左补零。 + * COBOL: COMPUTE WS-X ROUNDED = ... (ROUNDED 对应 HALF_UP) + */ + public static String formatZoned(final BigDecimal val, final int intLen, final int scale) { + BigDecimal v = (val == null ? BigDecimal.ZERO : val) + .setScale(scale, RoundingMode.HALF_UP) + .movePointRight(scale) + .setScale(0, RoundingMode.DOWN); + return padLeftZeroStr(v.toBigInteger().toString(), intLen + scale); + } + + /** + * NUMERIC 检查。 + * COBOL: IF WS-X IS NUMERIC ... + */ + public static boolean isNumeric(final String s) { + if (s == null || s.isEmpty()) { + return false; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + + /** + * 固定長行から [start, end) を安全に切り出し(省略時は空文字)。 + * COBOL の参照変更(WRK-X(m:n))の読取り相当。 + */ + public static String safeSub(final String line, final int start, final int end) { + if (line == null) { + return ""; + } + int s = Math.max(0, start); + int e = Math.min(line.length(), end); + if (s >= e) { + return ""; + } + return line.substring(s, e); + } + + /** + * 数字文字列を int に変換(非数字/空は def を返す)。 + * COBOL: MOVE '0000123' TO WS-N9 相当の安全変換。 + */ + public static int parseIntSafe(final String s, final int def) { + String t = (s == null) ? "" : s.trim(); + if (!isNumeric(t) || t.isEmpty()) { + return def; + } + try { + return Integer.parseInt(t); + } catch (NumberFormatException e) { + return def; + } + } +}