Files
cobol-java-v3/docs/detailed-design/06-comparator.md
T

359 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06 - 比对模块详细设计
## 1. 模块概述
比对模块(`comparator/`)是 COBOL 迁移验证平台 V3 的核心验证引擎,负责将 COBOL 原始输出与 Java(Spark)迁移后输出进行逐字段对比,判断迁移正确性。
模块提供五项核心能力:
1. **记录对齐**aligner):按主键将 COBOL 记录集与 Java 记录集配对
2. **二进制读取**cobol_binary_reader):解析 COBOL 二进制输出文件为字典
3. **数据标准化**normalizer):处理 EBCDIC 编码、COMP-3 压缩十进制、日期格式
4. **字段比对**field_compare):按类型(数值/日期/字符串)进行字段级比较
5. **舍入检测**rounding_detect):判断数值差异是否由 COBOL ROUNDED 子句引起
## 2. 文件清单
| 文件 | 职责 | 依赖 |
|------|------|------|
| `__init__.py` | 包入口,公开 API 导出 | 所有子模块 |
| `aligner.py` | COBOL 与 Java 记录对齐 | 无 |
| `cobol_binary_reader.py` | 二进制 COBOL 输出解析 | `data.field_tree` |
| `field_compare.py` | 字段级比较(decimal/string/date | `data.diff_result` |
| `normalizer.py` | COMP-3/EBCDIC 解码、IR 记录构造 | 无 |
| `rounding_detect.py` | 舍入差异检测 | `decimal` |
## 3. 记录对齐算法(aligner.py
### 3.1 职责
将 COBOL 输出记录集和 Java 输出记录集按主键字段进行配对,输出对齐的记录对列表及匹配状态。
### 3.2 函数签名
```python
def align_records(
cobol_records: list[dict],
java_records: list[dict],
key_field: str = "CUST-ID"
) -> list[tuple]
```
### 3.3 算法描述
1. **分组**:分别按 `key_field` 对 COBOL 和 Java 记录建立 `{key: [records]}` 索引
2. **合并键集**:取两侧键的并集,按字符串排序
3. **逐键配对**:对每个键,取两侧记录列表的对应位置进行配对
4. **状态标记**
| 状态 | 含义 |
|------|------|
| `MATCHED` | 两侧均有记录,成功配对 |
| `MISSING_IN_SPARK` | COBOL 有记录,Java 侧缺失 |
| `EXTRA_IN_SPARK` | Java 有记录,COBOL 侧缺失 |
### 3.4 返回值
```python
list[tuple[dict | None, dict | None, str]]
# (cobol_record, java_record, status)
```
### 3.5 设计要点
- 使用 `setdefault` 聚合同键多记录,支持一对多场景
- 排序保证输出顺序稳定
- 键值转为字符串处理,兼容数值键和字符串键
- 空记录集直接返回空列表
## 4. 二进制读取器(cobol_binary_reader.py
### 4.1 职责
读取 COBOL 程序生成的二进制输出文件,根据 `FieldTree` 定义的字段布局逐记录解析为字典。
### 4.2 核心类
```python
class CobolBinaryReader:
def read(self, path: str, tree: FieldTree) -> list[dict]
def _record_size(self, tree: FieldTree) -> int
def _parse(self, record_bytes: bytes, tree: FieldTree) -> dict
def _comp3(self, raw: bytes, signed: bool, decimal: int) -> str
```
### 4.3 记录大小计算
```python
def _record_size(self, tree):
return max((f.offset + f.length for f in tree.fields), default=0)
```
取所有顶层字段的最大 `offset + length` 作为记录大小。
### 4.4 字段解析策略
| USAGE 类型 | 解析方式 |
|-----------|----------|
| `COMP-3` | 半字节解码 + 符号位处理 + 小数点定位 |
| `COMP` / `COMP-5` | `int.from_bytes(raw, "big", signed=...)` |
| 其他(DISPLAY 等) | ASCII 解码 + 去尾空格 |
### 4.5 COMP-3 解码算法
```
输入: N 字节压缩十进制数据
1. 将每字节拆为两个半字节(高4位 + 低4位)
2. 弹出最后一个半字节作为符号位
3. 逐半字节累加为数值(权值 = 10^(len-1-i)
4. 符号位 0xD/0xB -> 取反
5. 按 decimal 定位小数点
输出: 字符串形式数值(如 "1234.56"
```
### 4.6 错误处理
- 文件为空或记录大小为 0 -> 返回空列表
- 记录不完整(字节数 < record_size-> 跳过该记录
- COMP-3 空数据 -> 返回 "0"
- 非 ASCII 字符 -> `errors="replace"` 替换为 `?`
## 5. 数据标准化器(normalizer.py
### 5.1 职责
提供 COBOL 数据到中间表示(IR)的标准化转换,包括 EBCDIC 解码、COMP-3 解码、日期格式统一、IR 记录构造。
### 5.2 核心类
```python
class Normalizer:
def normalize_encoding(self, raw: bytes, encoding: str) -> str
def normalize_comp3(self, raw: bytes) -> str
def normalize_date(self, s: str) -> str
def to_ir_record(self, name, hex_, val, enc, ft, length=0, scale=0, signed=False) -> IRRecord
def to_null_ir(self, name: str, side: str = "java") -> IRRecord
```
### 5.3 EBCDIC 编码转换
内置 EBCDIC 037 代码页映射表(`EBCDIC_037`),覆盖:
- 空格、标点、运算符
- 小写 a-z0x81-0xA9
- 大写 A-Z0xC1-0xE9
- 数字 0-90xF0-0xF9
不可映射字符处理:
- 可打印 ASCII32-126-> 保留原字符
- 其他 -> 替换为 `?`
### 5.4 COMP-3 标准化
`CobolBinaryReader._comp3` 算法一致,但返回整数字符串(无小数点处理),适用于无需小数定位的场景。
### 5.5 日期标准化
- 8 位纯数字字符串(如 `20260822`-> 格式化为 `YYYY-MM-DD`
- 其他格式 -> 原样返回
### 5.6 中间表示(IR)模型
```python
@dataclass
class CobolIRField:
raw_hex: str # 原始十六进制
decoded_value: str # 解码后值
encoding: str # 编码类型(EBCDIC/ASCII
field_type: str # 字段类型
length: int # 字节长度
scale: int # 小数位数
signed: bool # 是否带符号
@dataclass
class JavaIRField:
raw_value: str # 原始值
decoded_value: str # 解码后值
field_type: str # 字段类型
nullable: bool # 是否可空
@dataclass
class IRRecord:
field_name: str
cobol: CobolIRField | None
java: JavaIRField | None
```
### 5.7 空值 IR 构造
`to_null_ir` 为缺失侧构造空 IR 记录(`JavaIRField("", "", "null", True)`),用于只有一侧有数据的场景。
## 6. 字段比对算法(field_compare.py
### 6.1 职责
对单个字段的 COBOL 值和 Java 值进行类型感知的比较,返回结构化的比对结果。
### 6.2 函数签名
```python
def compare_field(
name: str,
c: str, # COBOL 值
j: str, # Java 值
field_type: str = "decimal", # 字段类型
tolerance: float = 0.01 # 容忍度
) -> FieldResult
```
### 6.3 比对策略
| 字段类型 | 比对方式 | 说明 |
|----------|----------|------|
| `decimal` / `numeric` | 数值差 <= tolerance -> TOLERATED | 使用 `Decimal` 精确计算 |
| `date` | 8 位数字 -> `YYYY-MM-DD` 格式化后比较 | 统一格式消除表示差异 |
| `string` | strip 后直接比较 | 去除首尾空白 |
| 其他 | 字符串直接比较 | 兜底策略 |
### 6.4 状态判定
| 状态 | 条件 |
|------|------|
| `PASS` | 完全一致 |
| `TOLERATED` | 数值差在容忍度范围内 |
| `MISMATCH` | 不一致 |
| `NOT_SET` | 两侧均为空/None |
### 6.5 数值解析(_num
```python
def _num(v) -> Decimal | None
```
- `None` / `"None"` -> `None`
- 空字符串 -> `Decimal("0")`
-`\x00` -> 去除后解析
- 非数字字符串 -> `None`
使用 Python `Decimal` 进行精确十进制运算,避免浮点精度问题。
### 6.6 默认容忍度
```python
DEFAULT_TOLERANCE = 0.01
```
可通过 `Config.tolerance` 全局配置调整。
## 7. 舍入检测(rounding_detect.py
### 7.1 职责
判断两个数值之间的差异是否由 COBOL `ROUNDED` 子句引起,识别舍入模式并给出置信度。
### 7.2 函数签名
```python
def detect_rounding(c: str, j: str) -> RoundingResult
```
### 7.3 RoundingResult 数据类
```python
@dataclass
class RoundingResult:
mode: str = "EXACT" # 舍入模式
confidence: float = 1.0 # 置信度(0-1
suggestion: str = "" # 建议文本
```
### 7.4 检测算法
```
1. 解析 COBOL 值 (cv) 和 Java 值 (jv) 为 Decimal
2. 任一解析失败 -> UNKNOWN (confidence=0)
3. cv == jv -> EXACT (confidence=1.0)
4. 计算绝对差 diff = |cv - jv|
5. 判定模式:
- diff < 2 -> TRUNCATE (confidence=0.6)
- diff < 100 -> ROUNDING (confidence=0.4)
- diff >= 100 -> SIGNIFICANT (confidence=0.9)
```
### 7.5 舍入模式说明
| 模式 | 含义 | 置信度 | 建议 |
|------|------|--------|------|
| `EXACT` | 完全一致 | 1.0 | 无 |
| `TRUNCATE` | 截断差异(diff < 2 | 0.6 | 可能是 COBOL 截断行为 |
| `ROUNDING` | 舍入差异(diff < 100 | 0.4 | 可能存在舍入方式差异 |
| `SIGNIFICANT` | 显著差异(diff >= 100 | 0.9 | 需要关注的较大差异 |
### 7.6 数值解析(_d
```python
def _d(v) -> Decimal | None
```
- 使用 `Decimal(str(v).strip())` 解析
- 异常时返回 `None`
## 8. 接口定义
### 8.1 模块公开 API
```python
# comparator/__init__.py
align_records(cobol_records, java_records, key_field) -> list[tuple]
compare_field(name, c, j, field_type, tolerance) -> FieldResult
CobolBinaryReader # class
Normalizer # class
detect_rounding(c, j) -> RoundingResult
```
### 8.2 典型调用流程
```
cobol_binary_reader.read(path, field_tree) -> list[dict]
|
v
align_records(cobol_records, java_records, key_field) -> list[tuple]
|
v (对每个 MATCHED 对)
compare_field(name, cobol_val, java_val, field_type, tolerance) -> FieldResult
|
v (可选)
detect_rounding(cobol_val, java_val) -> RoundingResult
```
### 8.3 数据模型依赖
| 模型 | 定义位置 | 用途 |
|------|----------|------|
| `FieldTree` / `Field` | `data.field_tree` | 字段布局定义 |
| `FieldResult` | `data.diff_result` | 字段比对结果 |
| `IRRecord` / `CobolIRField` / `JavaIRField` | `comparator.normalizer` | 中间表示 |
| `RoundingResult` | `comparator.rounding_detect` | 舍入检测结果 |
## 9. 错误处理
### 9.1 错误策略
模块采用**严格解析 + 宽容比对**策略:
| 异常场景 | 处理方式 | 影响 |
|----------|----------|------|
| 二进制文件为空 | 返回空列表 | 无记录可比 |
| 记录不完整 | 跳过该记录 | 部分数据丢失 |
| COMP-3 空数据 | 返回 "0" | 默认零值 |
| EBCDIC 未知字符 | 替换为 `?` | 可能导致 MISMATCH |
| 数值解析失败 | 返回 `None` | NOT_SET 状态 |
| 两侧均无值 | NOT_SET | 不计入匹配统计 |
### 9.2 已知局限
1. `aligner.py` 仅支持单一主键,不支持复合主键
2. `CobolBinaryReader` 假定所有记录等长,不支持变长记录
3. `Normalizer.normalize_comp3` 不处理小数位(与 `CobolBinaryReader._comp3` 重复实现)
4. `detect_rounding` 的阈值(2/100)为经验值,未基于统计分析
5. `field_compare` 的字符串比较未处理 EBCDIC 与 ASCII 的编码差异