"""条件层:COBOL条件表达式解析 + MC/DC枚举 + 约束合并""" import re from .models import CondLeaf, CondAnd, CondOr, CondNot, PicInfo # ── 条件解析 ── def _split_at_operator(text, operator): """Split text on operator word, respecting parentheses.""" result = [] current = [] depth = 0 # Normalize so parentheses are space-delimited tokens normalized = text.replace('(', ' ( ').replace(')', ' ) ') for token in normalized.split(): if not token: continue if token == '(': depth += 1 current.append(token) elif token == ')': depth -= 1 current.append(token) elif token == operator and depth == 0: result.append(' '.join(current).strip()) current = [] else: current.append(token) result.append(' '.join(current).strip()) return result def parse_single_condition(text, fields=None): """Parse 'AMOUNT > 1000' into ('AMOUNT', '>', '1000'). Also handles subscripted fields: 'WS-ITEM(SUB) = 'A''. Also resolves 88-level condition names (e.g. STATUS-APPROVED → WS-TRAN-STATUS = 'A'). Returns None if the condition contains AND/OR (compound). """ if ' AND ' in text or ' OR ' in text: return None # Check if text is an 88-level condition name if fields: for f in fields: if f.get('is_88') and f['name'] == text.upper(): return (f.get('parent', ''), '=', f.get('value', '')) m = re.match( r"^(\w[\w-]*(?:\s*\([^)]*\))?)\s*(>=|<=|<>|>|<|=)\s*(.+)$", text ) if m: field = re.sub(r'\s*([(),])\s*', r'\1', m.group(1)) return (field, m.group(2), m.group(3).strip().strip("'").strip('"')) # Try arithmetic expression: e.g. A + B > C m = re.match( r"^(\w[\w\s+\-*/().-]+?)\s*(>=|<=|<>|>|<|=)\s*(.+)$", text ) if m: field = re.sub(r'\s*([(),])\s*', r'\1', m.group(1)).strip() return (field, m.group(2), m.group(3).strip().strip("'").strip('"')) return None def parse_compound_condition(text, fields=None): """Parse a COBOL condition into a condition tree (AND/OR/LEAF). Handles AND > OR precedence and parentheses. """ text = text.strip() if not text: return None # Normalize parentheses to be space-delimited for reliable tokenization text = text.replace('(', ' ( ').replace(')', ' ) ') text = re.sub(r'\s+', ' ', text).strip() # Strip outer parentheses if text.startswith('(') and text.endswith(')'): depth = 0 wrapped = True for i, c in enumerate(text): if c == '(': depth += 1 elif c == ')': depth -= 1 if depth == 0 and i < len(text) - 1: wrapped = False break if wrapped: inner = parse_compound_condition(text[1:-1], fields) if inner: return inner # Split on OR (lowest precedence) parts = _split_at_operator(text, 'OR') if len(parts) > 1: node = parse_compound_condition(parts[0], fields) for p in parts[1:]: node = CondOr(node, parse_compound_condition(p, fields)) return node # Split on AND parts = _split_at_operator(text, 'AND') if len(parts) > 1: node = parse_compound_condition(parts[0], fields) for p in parts[1:]: node = CondAnd(node, parse_compound_condition(p, fields)) return node # NOT prefix (highest precedence, after AND/OR splitting) if text.upper().startswith('NOT '): inner = parse_compound_condition(text[4:].strip(), fields) return CondNot(inner) if inner else None # Leaf condition parsed = parse_single_condition(text, fields) if parsed: return CondLeaf(*parsed) return None def collect_leaves(tree): """Return list of all CondLeaf nodes in the tree.""" if isinstance(tree, CondLeaf): return [tree] elif isinstance(tree, CondNot): return collect_leaves(tree.child) elif isinstance(tree, (CondAnd, CondOr)): return collect_leaves(tree.left) + collect_leaves(tree.right) return [] def evaluate_tree(tree, assignment): """Evaluate condition tree given leaf→bool assignment dict.""" if isinstance(tree, CondLeaf): return assignment[tree] elif isinstance(tree, CondNot): return not evaluate_tree(tree.child, assignment) elif isinstance(tree, CondAnd): return evaluate_tree(tree.left, assignment) and evaluate_tree(tree.right, assignment) elif isinstance(tree, CondOr): return evaluate_tree(tree.left, assignment) or evaluate_tree(tree.right, assignment) return False def is_field(name, fields): # Strip subscript: WS-ITEM-STATUS(WS-INDEX-VAR) -> WS-ITEM-STATUS bare = re.sub(r'\s*\(.*\)\s*$', '', name).strip() for f in fields: if f['name'] == bare.upper(): return True return False # ── MC/DC ── def mcdc_sets(tree, fields=None): """Generate MC/DC constraint sets. Returns list of (constraints_list, decision_outcome) or None for simple conditions. Each constraint is (field, op, value, want_true). """ leaves = collect_leaves(tree) n = len(leaves) if n <= 1: return None # Evaluate all 2^n truth assignments all_results = [] for bits in range(1 << n): assignment = {} for i, leaf in enumerate(leaves): assignment[leaf] = bool(bits & (1 << i)) result = evaluate_tree(tree, assignment) all_results.append((assignment, result)) # For each leaf, find a pair showing independent effect on decision needed_pairs = {} for leaf in leaves: for a1, r1 in all_results: if leaf in needed_pairs: break for a2, r2 in all_results: if a1[leaf] != a2[leaf] and r1 != r2: if all(a1[o] == a2[o] for o in leaves if o != leaf): needed_pairs[leaf] = (dict(a1), r1, dict(a2), r2) break # Convert leaf assignments to constraint tuples result = [] added = set() for leaf, (a1, r1, a2, r2) in needed_pairs.items(): for assignment, decision in [(a1, r1), (a2, r2)]: key = frozenset((l, assignment[l]) for l in leaves) if key not in added: added.add(key) constraints = [] for l in leaves: want = assignment[l] constraints.append((l.field, l.op, l.value, want)) result.append((constraints, decision)) return result # ── 值计算 ── def satisfying_value(field_info: dict, operator: str, value, want_true: bool) -> str: ftype = field_info.get('type', 'unknown') digits = field_info.get('digits', 0) decimal = field_info.get('decimal', 0) total = digits + decimal if ftype == 'numeric': try: val_str = str(value) val_float = float(val_str) val_int = int(val_float * (10 ** decimal) + 0.5) except (ValueError, TypeError): val_int = 0 if want_true: if operator == '>': val_int = val_int + 1 elif operator in ('>=', '=', '<='): pass elif operator == '<': val_int = max(0, val_int - 1) elif operator == '<>': val_int = (val_int + 1) % (10 ** total) else: if operator in ('>', '>='): val_int = 0 elif operator == '=': val_int = (val_int + 1) % (10 ** total) elif operator == '<': pass elif operator == '<=': val_int = val_int + 1 elif operator == '<>': pass val_int = val_int % (10 ** total) int_part = str(val_int // (10 ** decimal)).zfill(digits) dec_part = str(val_int % (10 ** decimal)).zfill(decimal) if decimal == 0: return int_part return int_part + dec_part elif ftype in ('alphanumeric', 'alphabetic'): length = field_info.get('length', 1) base_chr = value[0].upper() if isinstance(value, str) and value else 'A' if want_true: if operator in ('=', '=='): return base_chr.ljust(length, base_chr) elif operator in ('<>', '!='): other = chr(65 + (ord(base_chr) - 64) % 26) return other.ljust(length, other) else: if operator in ('=', '=='): other = chr(65 + (ord(base_chr) - 64) % 26) return other.ljust(length, other) elif operator in ('<>', '!='): return base_chr.ljust(length, base_chr) return '0'.zfill(total)