fix(verify_pdf): exclude currency amounts from ASCII date checks (#724)

* fix(verify-pdf): distinguish currency amounts from date ranges

* docs: reference PR 724 in unreleased fix

---------

Co-authored-by: Mads Lorentzen <madslorentzen17@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-11-10 20:03:21 +01:00
1 parent c75041c012
commit ec447a8073
2 files changed
+28 -2

No files matched your search

+10
View File
@@ -121,6 +121,16 @@ class FindNonAsciiDateRangesTests(unittest.TestCase):
with self.subTest(text=text): with self.subTest(text=text):
self.assertEqual(find_non_ascii_date_ranges(text), []) self.assertEqual(find_non_ascii_date_ranges(text), [])
def test_attached_currency_symbols_make_numeric_ranges_quantities(self):
# The real PDF CLI previously reported these budget amounts as dates.
# Currency symbols identify amounts even when both values resemble years.
for symbol in ("€", "£", "$", "¥"):
for text in (f"Budget {symbol}2000–{symbol}5000 per month",
f"Budget 1500{symbol}–2000{symbol} per month",
f"Budget {symbol}2000–{symbol}2024 per month"):
with self.subTest(text=text):
self.assertEqual(find_non_ascii_date_ranges(text), [])
def test_numerically_formatted_date_ranges_are_still_caught(self): def test_numerically_formatted_date_ranges_are_still_caught(self):
# A composite token (slashed, dotted, hyphenated) is not a bare number, # A composite token (slashed, dotted, hyphenated) is not a bare number,
# so the quantity rule does not apply and the year rule still fires. # so the quantity rule does not apply and the year rule still fires.
+18 -2
View File
@@ -155,6 +155,18 @@ def _bare_number(token):
return core return core
def _currency_amount(token):
"""A numeric amount with an attached Unicode currency symbol, or None."""
core = token.strip(_OUTER_PUNCTUATION)
if core and unicodedata.category(core[0]) == "Sc":
core = core[1:]
elif core and unicodedata.category(core[-1]) == "Sc":
core = core[:-1]
else:
return None
return core if _BARE_NUMBER.fullmatch(core) else None
_TWO_DIGIT_YEAR = re.compile(r"\d{2}") _TWO_DIGIT_YEAR = re.compile(r"\d{2}")
@@ -165,13 +177,17 @@ def _quantity_range(line, dash_index):
(`2016-17`, `2019-20`): the short form is how academic years and seasons (`2016-17`, `2019-20`): the short form is how academic years and seasons
are written, and it was caught before the quantity rule existed. Anything are written, and it was caught before the quantity rule existed. Anything
else with bare numbers on both sides is a quantity (`1500-2000`, else with bare numbers on both sides is a quantity (`1500-2000`,
`2000-5000`) and is left alone. `2000-5000`) and is left alone. An attached currency symbol makes a
numeric pair an amount range, even when both amounts resemble years.
""" """
before = _TOKEN_BEFORE.search(line[:dash_index]) before = _TOKEN_BEFORE.search(line[:dash_index])
after = _TOKEN_AFTER.search(line[dash_index + 1 :]) after = _TOKEN_AFTER.search(line[dash_index + 1 :])
if not before or not after: if not before or not after:
return False return False
left, right = _bare_number(before.group(1)), _bare_number(after.group(1)) left_money, right_money = _currency_amount(before.group(1)), _currency_amount(after.group(1))
left, right = left_money or _bare_number(before.group(1)), right_money or _bare_number(after.group(1))
if left_money is not None or right_money is not None:
return left is not None and right is not None
if left is None or right is None: if left is None or right is None:
return False return False
if _BARE_YEAR.fullmatch(left) and (_BARE_YEAR.fullmatch(right) or _TWO_DIGIT_YEAR.fullmatch(right)): if _BARE_YEAR.fullmatch(left) and (_BARE_YEAR.fullmatch(right) or _TWO_DIGIT_YEAR.fullmatch(right)):