| """Deterministic old/new Indian criminal-code section correspondences. |
| |
| The source dataset records verified direct correspondences from the new codes to |
| the repealed codes. This module builds the reverse index as well, without |
| turning a one-to-many correspondence into a claim of legal equivalence. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import re |
| from pathlib import Path |
| from typing import Any |
|
|
|
|
| ACT_NAMES = { |
| "IPC": "Indian Penal Code, 1860", |
| "BNS": "Bharatiya Nyaya Sanhita, 2023", |
| "CRPC": "Code of Criminal Procedure, 1973", |
| "BNSS": "Bharatiya Nagarik Suraksha Sanhita, 2023", |
| "IEA": "Indian Evidence Act, 1872", |
| "BSA": "Bharatiya Sakshya Adhiniyam, 2023", |
| } |
|
|
| ACT_ALIASES = { |
| "IPC": "IPC", |
| "INDIAN PENAL CODE": "IPC", |
| "BNS": "BNS", |
| "BHARATIYA NYAYA SANHITA": "BNS", |
| "CRPC": "CRPC", |
| "CRIMINAL PROCEDURE CODE": "CRPC", |
| "CODE OF CRIMINAL PROCEDURE": "CRPC", |
| "BNSS": "BNSS", |
| "BHARATIYA NAGARIK SURAKSHA SANHITA": "BNSS", |
| "IEA": "IEA", |
| "INDIAN EVIDENCE ACT": "IEA", |
| "BSA": "BSA", |
| "BHARATIYA SAKSHYA ADHINIYAM": "BSA", |
| } |
|
|
| PAIRS = { |
| "IPC": "BNS", |
| "BNS": "IPC", |
| "CRPC": "BNSS", |
| "BNSS": "CRPC", |
| "IEA": "BSA", |
| "BSA": "IEA", |
| } |
|
|
|
|
| def normalise_act(value: object) -> str: |
| text = str(value or "").upper() |
| text = re.sub(r"\bI\.?\s*P\.?\s*C\.?", "IPC", text) |
| text = re.sub(r"\bCR\.?\s*P\.?\s*C\.?", "CRPC", text) |
| text = re.sub(r"\b(?:18|19|20)\d{2}\b", " ", text) |
| text = re.sub(r"[^A-Z0-9]+", " ", text) |
| return ACT_ALIASES.get(re.sub(r"\s+", " ", text).strip(), "") |
|
|
|
|
| def normalise_section(value: object) -> str: |
| text = re.sub( |
| r"^\s*(?:sections?|secs?\.?|ss?\.?)\s*[-:]*\s*", |
| "", |
| str(value or ""), |
| flags=re.IGNORECASE, |
| ) |
| text = re.sub(r"\s+", "", text).upper().strip(".,;:") |
| return text if re.fullmatch(r"\d+[A-Z]*", text) else "" |
|
|
|
|
| class StatuteCrosswalk: |
| """Load and query a verified crosswalk in either direction.""" |
|
|
| def __init__(self, payload: dict[str, Any], *, source_path: Path | None = None): |
| mappings = payload.get("mappings") |
| if not isinstance(mappings, list): |
| raise ValueError("Crosswalk must contain a mappings list.") |
| self.schema_version = payload.get("schema_version") |
| self.description = str(payload.get("description") or "") |
| self.source = payload.get("source") if isinstance(payload.get("source"), dict) else {} |
| self.source_path = source_path |
| self._index: dict[tuple[str, str], list[dict[str, str]]] = {} |
| for item in mappings: |
| if not isinstance(item, dict): |
| continue |
| source = item.get("from") if isinstance(item.get("from"), dict) else {} |
| from_act = normalise_act(source.get("act")) |
| from_section = normalise_section(source.get("section")) |
| if not from_act or not from_section: |
| continue |
| for target in item.get("to") or []: |
| if not isinstance(target, dict): |
| continue |
| to_act = normalise_act(target.get("act")) |
| to_section = normalise_section(target.get("section")) |
| if not to_act or not to_section or PAIRS.get(from_act) != to_act: |
| continue |
| self._append(from_act, from_section, to_act, to_section) |
| self._append(to_act, to_section, from_act, from_section) |
|
|
| @classmethod |
| def from_file(cls, path: str | Path) -> "StatuteCrosswalk": |
| resolved = Path(path).expanduser().resolve() |
| return cls(json.loads(resolved.read_text(encoding="utf-8")), source_path=resolved) |
|
|
| def _append(self, from_act: str, from_section: str, to_act: str, to_section: str) -> None: |
| values = self._index.setdefault((from_act, from_section), []) |
| record = {"act": to_act, "section": to_section} |
| if record not in values: |
| values.append(record) |
|
|
| @property |
| def mapping_count(self) -> int: |
| return len(self._index) |
|
|
| def lookup(self, act: object, section: object) -> dict[str, Any]: |
| code = normalise_act(act) |
| number = normalise_section(section) |
| corresponding = [ |
| { |
| **item, |
| "act_name": ACT_NAMES[item["act"]], |
| "label": f'{item["act"]} section {item["section"]}', |
| } |
| for item in self._index.get((code, number), []) |
| ] |
| source_label = f"{code} {number}".strip() |
| target_label = ", ".join(f'{item["act"]} {item["section"]}' for item in corresponding) |
| table_key = "_".join(sorted((code, PAIRS.get(code, "")))) |
| source_tables = self.source.get("tables") if isinstance(self.source.get("tables"), dict) else {} |
| source_url = next( |
| ( |
| value |
| for key, value in source_tables.items() |
| if set(key.split("_")) == {code, PAIRS.get(code, "")} |
| ), |
| None, |
| ) |
| return { |
| "found": bool(corresponding), |
| "from": source_label, |
| "to": target_label or None, |
| "query": { |
| "act": code, |
| "act_name": ACT_NAMES.get(code), |
| "section": number, |
| "label": f"{code} section {number}" if code and number else source_label, |
| }, |
| "corresponding": corresponding, |
| "one_to_many": len(corresponding) > 1, |
| "direction": f"{code.lower()}_to_{PAIRS.get(code, '').lower()}" if code else None, |
| "source": { |
| "publisher": self.source.get("publisher"), |
| "url": source_url, |
| "table": table_key, |
| }, |
| "notice": ( |
| "These are direct statutory correspondences from the published crosswalk. " |
| "They are not a finding that the provisions are legally identical; compare the text and case law." |
| ), |
| } |
|
|
|
|
| def default_crosswalk_path() -> Path: |
| return Path(__file__).resolve().parents[1] / "section_crosswalk.json" |
|
|
|
|
| def load_default_crosswalk(path: str | Path | None = None) -> StatuteCrosswalk: |
| return StatuteCrosswalk.from_file(path or default_crosswalk_path()) |
|
|