agenda-parser / cornell_lii /citations.py
rdubwiley's picture
Deploy Agenda Parser
459bdc8 verified
Raw
History Blame Contribute Delete
2.35 kB
"""Parse and normalize US legal citations (CFR regulations, USC statutes).
Pure and network-free. Tolerant of the common written forms::
29 CFR 1604.11 29 C.F.R. § 1604.11 29/1604.11
42 USC 1983 42 U.S.C. § 1983 42/1983
A parsed citation carries the corpus ("cfr"/"usc"), the title number, and the
section as written (subsection parentheticals are preserved here and only dropped
when building the per-section page URL in ``urls``).
"""
from __future__ import annotations
import re
from dataclasses import dataclass
__all__ = ["Citation", "CitationError", "parse_cfr", "parse_usc"]
class CitationError(ValueError):
"""Raised when a citation string cannot be parsed."""
@dataclass(frozen=True)
class Citation:
corpus: str # "cfr" | "usc"
title: str
section: str
@property
def label(self) -> str:
word = "CFR" if self.corpus == "cfr" else "USC"
return f"{self.title} {word} {self.section}"
# Match the corpus token in any common spelling (CFR / C.F.R. / USC / U.S.C.) and
# the section sign, so they can be stripped before pulling out the numbers.
_CORPUS_RE = re.compile(r"(?i)\b(?:c\.?\s*f\.?\s*r\.?|u\.?\s*s\.?\s*c\.?)\b|§")
def _split_title_section(text: str, *, corpus: str) -> tuple[str, str]:
cleaned = _CORPUS_RE.sub(" ", text)
cleaned = cleaned.replace("/", " ")
tokens = [t.strip(".,;") for t in cleaned.split() if t.strip(".,;")]
nums = [t for t in tokens if any(ch.isdigit() for ch in t)]
word = "CFR" if corpus == "cfr" else "USC"
if len(nums) < 2:
raise CitationError(
f"Could not read a {word} citation from {text!r}. "
f"Expected something like '29 {word} 1604.11'."
)
title, section = nums[0], nums[1]
if not title.isdigit():
raise CitationError(f"{title!r} is not a valid {word} title number (from {text!r}).")
return title, section
def parse_cfr(text: str) -> Citation:
"""Parse a Code of Federal Regulations citation, e.g. ``29 CFR 1604.11``."""
title, section = _split_title_section(text, corpus="cfr")
return Citation("cfr", title, section)
def parse_usc(text: str) -> Citation:
"""Parse a US Code citation, e.g. ``42 USC 1983``."""
title, section = _split_title_section(text, corpus="usc")
return Citation("usc", title, section)