| from __future__ import annotations |
|
|
| import re |
| from typing import List |
|
|
| from db.parsers.bsa.legal_models import ( |
| Clause, |
| SubClause, |
| RomanClause |
| ) |
|
|
|
|
| class ClauseParser: |
|
|
| |
| |
| |
|
|
| CLAUSE_RE = re.compile( |
| r"(?m)^\s*\((\d+[A-Za-z]?)\)" |
| ) |
|
|
| SUBCLAUSE_RE = re.compile( |
| r"(?m)^\s*\(([a-z])\)" |
| ) |
|
|
| ROMAN_RE = re.compile( |
| r"(?m)^\s*\(([ivxlcdm]+)\)" |
| ) |
|
|
| ROMAN_VALUES = { |
| "i", "ii", "iii", "iv", "v", |
| "vi", "vii", "viii", "ix", "x", |
| "xi", "xii", "xiii", "xiv", "xv", |
| "xvi", "xvii", "xviii", "xix", "xx" |
| } |
|
|
| STOP_WORDS = [ |
| "Explanation.", |
| "Explanation.—", |
| "Explanation.-", |
| "Illustration.", |
| "Illustrations.", |
| "Exception.", |
| "Exception.—", |
| "Provided that", |
| "Provided further that" |
| ] |
|
|
| |
| |
| |
|
|
| def split_sections( |
| self, |
| text: str, |
| pattern: re.Pattern |
| ): |
|
|
| matches = list( |
| pattern.finditer(text) |
| ) |
|
|
| if not matches: |
| return [] |
|
|
| sections = [] |
|
|
| for i, match in enumerate(matches): |
|
|
| start = match.start() |
|
|
| end = ( |
| matches[i + 1].start() |
| if i + 1 < len(matches) |
| else len(text) |
| ) |
|
|
| sections.append( |
| ( |
| match.group(1), |
| text[start:end].strip() |
| ) |
| ) |
|
|
| return sections |
|
|
| |
| |
| |
|
|
| def clean_clause_text( |
| self, |
| text: str |
| ) -> str: |
|
|
| cutoff = len(text) |
|
|
| for word in self.STOP_WORDS: |
|
|
| pos = text.find(word) |
|
|
| if pos != -1: |
|
|
| cutoff = min( |
| cutoff, |
| pos |
| ) |
|
|
| return text[:cutoff] |
|
|
| |
| |
| |
|
|
| def parse_roman_clauses( |
| self, |
| text: str |
| ) -> List[RomanClause]: |
|
|
| romans = [] |
|
|
| roman_sections = self.split_sections( |
| text, |
| re.compile( |
| r"(?m)^\s*\((i|ii|iii|iv|v|vi|vii|viii|ix|x|xi|xii|xiii|xiv|xv|xvi|xvii|xviii|xix|xx)\)" |
| ) |
| ) |
|
|
| for roman_no, roman_text in roman_sections: |
|
|
| romans.append( |
| RomanClause( |
| document="bsa", |
| roman_no=roman_no, |
| text=roman_text |
| ) |
| ) |
|
|
| return romans |
|
|
| |
| |
| |
|
|
| def parse_subclauses( |
| self, |
| text: str |
| ) -> List[SubClause]: |
|
|
| subclauses = [] |
|
|
| sub_sections = self.split_sections( |
| text, |
| self.SUBCLAUSE_RE |
| ) |
|
|
| for sub_no, sub_text in sub_sections: |
|
|
| if ( |
| sub_no.lower() |
| in self.ROMAN_VALUES |
| ): |
| continue |
|
|
| sub = SubClause( |
| document="bsa", |
| sub_clause_no=sub_no, |
| text=sub_text |
| ) |
|
|
| sub.roman_clauses.extend( |
| self.parse_roman_clauses( |
| sub_text |
| ) |
| ) |
|
|
| subclauses.append( |
| sub |
| ) |
|
|
| return subclauses |
|
|
| |
| |
| |
|
|
| def parse_clauses( |
| self, |
| section_text: str |
| ) -> List[Clause]: |
|
|
| section_text = re.sub( |
| r"([;:—])\(([a-z])\)", |
| r"\1\n(\2)", |
| section_text |
| ) |
|
|
| section_text = re.sub( |
| r"([.;:—])\((\d+[A-Za-z]?)\)", |
| r"\1\n(\2)", |
| section_text |
| ) |
|
|
| section_text = re.sub( |
| r"(\d+\.)\s*(\(\d+[A-Za-z]?\))", |
| r"\1\n\2", |
| section_text |
| ) |
|
|
| clauses = [] |
|
|
| |
| |
| |
|
|
| clause_sections = self.split_sections( |
| section_text, |
| self.CLAUSE_RE |
| ) |
|
|
| if clause_sections: |
|
|
| for clause_no, clause_text in clause_sections: |
|
|
| clause = Clause( |
| document="bsa", |
| clause_no=clause_no, |
| text=clause_text |
| ) |
|
|
| clean_text = self.clean_clause_text( |
| clause_text |
| ) |
|
|
| clause.sub_clauses.extend( |
| self.parse_subclauses( |
| clean_text |
| ) |
| ) |
|
|
| if not clause.sub_clauses: |
|
|
| clause.roman_clauses.extend( |
| self.parse_roman_clauses( |
| clean_text |
| ) |
| ) |
|
|
| clauses.append( |
| clause |
| ) |
|
|
| return clauses |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| alpha_sections = self.split_sections( |
| section_text, |
| self.SUBCLAUSE_RE |
| ) |
|
|
| if alpha_sections: |
|
|
| synthetic_clause = Clause( |
| document="bsa", |
| clause_no="0", |
| text=section_text |
| ) |
|
|
| for alpha_no, alpha_text in alpha_sections: |
|
|
| if ( |
| alpha_no.lower() |
| in self.ROMAN_VALUES |
| ): |
| continue |
|
|
| sub = SubClause( |
| document="bsa", |
| sub_clause_no=alpha_no, |
| text=alpha_text |
| ) |
|
|
| sub.roman_clauses.extend( |
| self.parse_roman_clauses( |
| alpha_text |
| ) |
| ) |
|
|
| synthetic_clause.sub_clauses.append( |
| sub |
| ) |
|
|
| clauses.append( |
| synthetic_clause |
| ) |
|
|
| return clauses |
|
|
| return [] |
|
|
| |
| |
| |
|
|
| def validate_clauses( |
| self, |
| clauses: List[Clause] |
| ) -> List[str]: |
|
|
| errors = [] |
|
|
| clause_seen = set() |
|
|
| for clause in clauses: |
|
|
| if ( |
| clause.clause_no |
| in clause_seen |
| ): |
|
|
| errors.append( |
| f"Duplicate Clause " |
| f"{clause.clause_no}" |
| ) |
|
|
| clause_seen.add( |
| clause.clause_no |
| ) |
|
|
| |
| |
| |
|
|
| roman_seen = set() |
|
|
| for roman in clause.roman_clauses: |
|
|
| if ( |
| roman.roman_no |
| in roman_seen |
| ): |
|
|
| errors.append( |
| f"Duplicate Roman " |
| f"{roman.roman_no}" |
| ) |
|
|
| roman_seen.add( |
| roman.roman_no |
| ) |
|
|
| |
| |
| |
|
|
| sub_seen = set() |
|
|
| for sub in clause.sub_clauses: |
|
|
| if ( |
| sub.sub_clause_no |
| in sub_seen |
| ): |
|
|
| errors.append( |
| f"Duplicate SubClause " |
| f"{sub.sub_clause_no}" |
| ) |
|
|
| sub_seen.add( |
| sub.sub_clause_no |
| ) |
|
|
| roman_seen = set() |
|
|
| for roman in sub.roman_clauses: |
|
|
| if ( |
| roman.roman_no |
| in roman_seen |
| ): |
|
|
| errors.append( |
| f"Duplicate Roman " |
| f"{roman.roman_no}" |
| ) |
|
|
| roman_seen.add( |
| roman.roman_no |
| ) |
|
|
| return errors |