diff --git a/CHANGES.md b/CHANGES.md index 0b6fcb17..23e9a57a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,11 @@ Fixes: - Fix 2 `TypeError`s raised in `get_citations(markup_text=...)` when `clean_steps` was omitted or lacked `"html"`. The documented fallback that prepends the `html` step was both unreachable and broken. +- Keep a statute's trailing `note` designator as part of the citation's + identity. A note is a different provision than the section it is filed + under, so `42 U.S.C. § 1983 note` now carries `groups["note"]` and no + longer resolves to the bare `42 U.S.C. § 1983`. Prose such as + "§ 1983 notes that ..." is not treated as a designator. #323 ## Current diff --git a/eyecite/helpers.py b/eyecite/helpers.py index 84213cb5..bae64f36 100644 --- a/eyecite/helpers.py +++ b/eyecite/helpers.py @@ -890,6 +890,10 @@ def add_law_metadata(citation: FullLawCitation, words: Tokens) -> None: return citation.full_span_end = citation.span()[1] + m.end() + # A note is a different provision than the section it is filed under, so it + # belongs to the citation's identity, which is built from groups. + if m["note"]: + citation.groups["note"] = m["note"] citation.metadata.pin_cite = clean_pin_cite(m["pin_cite"]) or None citation.metadata.publisher = m["publisher"] citation.metadata.day = m["day"] diff --git a/eyecite/models.py b/eyecite/models.py index bfff85a8..c92429b7 100644 --- a/eyecite/models.py +++ b/eyecite/models.py @@ -408,6 +408,8 @@ def corrected_citation_full(self): m = self.metadata if m.pin_cite: parts.append(f"{m.pin_cite}") + if note := self.groups.get("note"): + parts.append(f" {note}") publisher_date = " ".join( i for i in (m.publisher, m.month, m.day, m.year) if i ) diff --git a/eyecite/regexes.py b/eyecite/regexes.py index df1cdddf..f4e27e95 100644 --- a/eyecite/regexes.py +++ b/eyecite/regexes.py @@ -259,6 +259,22 @@ def reference_pin_cite_re(regexes): ) """ +# Law note regex: +# A trailing "note" points at material filed under a section rather than at the +# section itself, so it is a different target and not a pin cite. Unlike +# "et seq." the word is ordinary English, so it only counts as a designator +# when nothing continues it: "notes that ..." must not match. +LAW_NOTE_REGEX = r""" + (?: + \ (?Pnote) + (?= + [,.;)\]\\]| # ending punctuation + \ ?[(\[]| # space and start of parens + $ # end of text + ) + )? +""" + # Short cite antecedent regex: # What case does a short cite refer to? For now, we just capture the previous # word optionally followed by a comma. Example: Adarand, 515 U.S. at 241. @@ -363,6 +379,7 @@ def reference_pin_cite_re(regexes): # and then may be followed by a parenthetical: POST_LAW_CITATION_REGEX = rf""" {LAW_PIN_CITE_REGEX}? + {LAW_NOTE_REGEX} \ ? (?:\( # Consol., McKinney, Deering, West, LexisNexis, etc. diff --git a/tests/test_FindTest.py b/tests/test_FindTest.py index 9d03d354..32d80b53 100644 --- a/tests/test_FindTest.py +++ b/tests/test_FindTest.py @@ -1015,6 +1015,43 @@ def test_find_law_citations(self): [law_citation('Mass. Gen. Laws ch. 1, §§ 2-3', reporter='Mass. Gen. Laws', groups={'chapter': '1', 'section': '2-3'})]), + # note designator, a different provision than the section + ('42 U.S.C. § 1983 note', + [law_citation('42 U.S.C. § 1983 note', reporter='U.S.C.', + groups={'title': '42', 'section': '1983', + 'note': 'note'})]), + ('28 U.S.C. § 994 note', + [law_citation('28 U.S.C. § 994 note', reporter='U.S.C.', + groups={'title': '28', 'section': '994', + 'note': 'note'})]), + ('8 U.S.C. § 1101 note', + [law_citation('8 U.S.C. § 1101 note', reporter='U.S.C.', + groups={'title': '8', 'section': '1101', + 'note': 'note'})]), + # note after a subsection pin cite + ('Fla. Stat. § 120.68(2) note', + [law_citation('Fla. Stat. § 120.68(2) note', + reporter='Fla. Stat.', + metadata={'pin_cite': '(2)'}, + groups={'section': '120.68', 'note': 'note'})]), + # note before a year parenthetical + ('42 U.S.C. § 1983 note (1994)', + [law_citation('42 U.S.C. § 1983 note (1994)', reporter='U.S.C.', + groups={'title': '42', 'section': '1983', + 'note': 'note'}, + year=1994)]), + # et seq. is part of the section, so it stays a pin cite + ('42 U.S.C. § 1983 et seq.', + [law_citation('42 U.S.C. § 1983 et seq.', reporter='U.S.C.', + metadata={'pin_cite': 'et seq.'}, + groups={'title': '42', 'section': '1983'})]), + # "note" as prose, not a designator + ('42 U.S.C. § 1983 notes that liability attaches', + [law_citation('42 U.S.C. § 1983', reporter='U.S.C.', + groups={'title': '42', 'section': '1983'})]), + ('42 U.S.C. § 1983. Note that the court', + [law_citation('42 U.S.C. § 1983', reporter='U.S.C.', + groups={'title': '42', 'section': '1983'})]), ) # fmt: on self.run_test_pairs(test_pairs, "Law citation extraction") diff --git a/tests/test_ModelsTest.py b/tests/test_ModelsTest.py index b195530b..20c75de9 100644 --- a/tests/test_ModelsTest.py +++ b/tests/test_ModelsTest.py @@ -158,6 +158,18 @@ def test_unknown_citation_comparison(self): self.assertNotEqual(hash(citations[0]), hash(citations[1])) print("✓") + def test_law_note_comparison(self): + """Is a statutory note a different citation than the section it is + filed under?""" + citations = [ + get_citations("42 U.S.C. § 1983")[0], + get_citations("42 U.S.C. § 1983 note")[0], + ] + print("Testing law note comparison...", end=" ") + self.assertNotEqual(citations[0], citations[1]) + self.assertNotEqual(hash(citations[0]), hash(citations[1])) + print("✓") + def test_missing_page_cite_conversion(self): """Do citations with missing page numbers get their groups['page'] attribute set to None?""" @@ -222,6 +234,14 @@ def test_corrected_full_citation_includes_closing_parenthesis(self): "Meritor Sav. Bank v. Vinson, 477 U.S. 57, 60 (scotus 1986)", ) + def test_corrected_full_citation_includes_law_note(self): + """Does the corrected_citation_full method keep a note designator?""" + law_note_citation = get_citations("42 U.S.C. § 1983 note")[0] + self.assertEqual( + law_note_citation.corrected_citation_full(), + "42 U.S.C. § 1983 note", + ) + def test_page_correction(self): """Can we correct pages on citation.corrected_citation()?""" tests = [