| from __future__ import annotations |
|
|
| import re |
| from typing import List, Optional |
|
|
| from db.parsers.bns.legal_models import Explanation |
|
|
|
|
| class ExplanationParser: |
| """ |
| BNS Explanation Parser |
| |
| Supports: |
| |
| Explanation.— |
| |
| Explanation 1.— |
| |
| Explanation 2.— |
| |
| Explanation I.— |
| |
| Explanation II.— |
| """ |
|
|
| |
| |
| |
|
|
| EXPLANATION_RE = re.compile( |
| r"(?im)^Explanation\s*([0-9IVXLCDM]*)\s*[\.\-—:]" |
| ) |
| STOP_RE = re.compile( |
| r"(?im)^(" |
| r"Explanation\s*" |
| r"|Illustration[s]?\." |
| r"|Exception\s*[.—:-]" |
| r"|\(\d+[A-Za-z]?\)" |
| r")" |
| ) |
| NEXT_BLOCK_RE = re.compile( |
| r"(?im)^(" |
| r"Explanation\s*" |
| r"|Illustration[s]?\." |
| r"|Exception\s*[.—:-]" |
| r"|\(\d+[A-Za-z]?\)" |
| r")" |
| ) |
|
|
| |
| |
| |
|
|
| ROMAN_MAP = { |
| "I": "1", |
| "II": "2", |
| "III": "3", |
| "IV": "4", |
| "V": "5", |
| "VI": "6", |
| "VII": "7", |
| "VIII": "8", |
| "IX": "9", |
| "X": "10", |
| "XI": "11", |
| "XII": "12", |
| "XIII": "13", |
| "XIV": "14", |
| "XV": "15", |
| "XVI": "16", |
| "XVII": "17", |
| "XVIII": "18", |
| "XIX": "19", |
| "XX": "20", |
| } |
|
|
| def normalize_explanation_no( |
| self, |
| explanation_no: str |
| ) -> Optional[str]: |
|
|
| explanation_no = explanation_no.strip().upper() |
|
|
| if not explanation_no: |
| return None |
|
|
| return self.ROMAN_MAP.get( |
| explanation_no, |
| explanation_no |
| ) |
|
|
| |
| |
| |
|
|
| def extract_explanations( |
| self, |
| text: str |
| ) -> List[Explanation]: |
|
|
| matches = list( |
| self.EXPLANATION_RE.finditer(text) |
| ) |
|
|
| if not matches: |
| return [] |
|
|
| explanations = [] |
|
|
| for i, match in enumerate(matches): |
|
|
| start = match.start() |
|
|
| if i + 1 < len(matches): |
|
|
| end = matches[i + 1].start() |
|
|
| else: |
|
|
| end = len(text) |
|
|
| remaining = text[start:] |
|
|
| |
|
|
| next_block = list( |
| self.NEXT_BLOCK_RE.finditer( |
| remaining |
| ) |
| ) |
|
|
| if len(next_block) > 1: |
|
|
| end = ( |
| start + |
| next_block[1].start() |
| ) |
|
|
| explanation_no = ( |
| self.normalize_explanation_no( |
| match.group(1) |
| ) |
| ) |
|
|
| explanation_text = ( |
| text[start:end] |
| .strip() |
| ) |
|
|
| explanations.append( |
| Explanation( |
| document="bns", |
| explanation_no= |
| explanation_no, |
|
|
| text= |
| explanation_text |
| ) |
| ) |
|
|
| return explanations |
| |
| |
| |
|
|
| def validate_explanations( |
| self, |
| section_no, |
| explanations: List[Explanation] |
| ) -> List[str]: |
|
|
| errors = [] |
|
|
| for explanation in explanations: |
|
|
| if not explanation.text.strip(): |
|
|
| label = ( |
| explanation.explanation_no |
| if explanation.explanation_no |
| else "Unnumbered" |
| ) |
|
|
| errors.append( |
| f"Empty Explanation {label}" |
| ) |
|
|
| return errors |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
|
|
| sample = """ |
| Whoever commits an offence shall be punished. |
| |
| Explanation.— |
| This is an unnumbered explanation. |
| |
| Explanation I.— |
| Roman numeral explanation. |
| |
| Explanation 2.— |
| Second explanation. |
| |
| Explanation II.— |
| Duplicate of Explanation 2. |
| """ |
|
|
| parser = ExplanationParser() |
|
|
| explanations = parser.extract_explanations( |
| sample |
| ) |
|
|
| print( |
| "Explanations:", |
| len(explanations) |
| ) |
|
|
| print() |
|
|
| for e in explanations: |
|
|
| print( |
| "Explanation No:", |
| e.explanation_no |
| ) |
|
|
| print( |
| e.text |
| ) |
|
|
| print( |
| "-" * 60 |
| ) |
|
|
| errors = parser.validate_explanations( |
| explanations |
| ) |
|
|
| print() |
|
|
| print( |
| "Errors:", |
| len(errors) |
| ) |
|
|
| for error in errors: |
| print(error) |