mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
fix(verify_pdf): --ascii-dates treats a numeric range as a date only when both ends are bare years (#533)
month" and "2000-5000 requests per second" still failed /apply Step 5d: each has a four-digit number next to the dash that happens to fall in 1900-2099, and the guide tells users to keep "--" in exactly those prose ranges. When both sides of the dash are bare numbers - the whole whitespace-delimited token, outer punctuation stripped, matching \d+(?:[.,]\d+)*[kKmM%]? with no year group among its parts - count the match only if both are years. The salary band and the throughput range pass; 2016-2024, 1990-2010 and "2016 - Present" still fail as before, and a numerically formatted range (03/2016-07/2024, 01.2016 - 12.2024, 2016-03-2024-07, 2016/17-2018/19) is not a bare number on either side and is still caught. On the stock CV with all three prose ranges on one line, master reports three hits and this branch reports the one real date. Three new cases; the quantity one fails on master.
This commit is contained in:
1 parent
36771d03a8
commit
7fe4ea3781
3 files changed
+92
-3
No files matched your search
@@ -68,6 +68,17 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **`verify_pdf.py --ascii-dates` no longer reads a numeric range with year-shaped ends as a
|
||||||
|
date** - #512 bounded the year to four standalone digits, but `EUR 1500–2000 per month` and
|
||||||
|
`2000–5000 requests per second` still failed `/apply` Step 5d: each has a four-digit number
|
||||||
|
next to the dash that happens to fall in 1900-2099. A range whose two ends are both bare
|
||||||
|
numbers (the whole whitespace-delimited token, outer punctuation stripped) now counts as a
|
||||||
|
date only when both are years, so the salary band and the throughput range pass, while
|
||||||
|
`2016–2024`, `1990–2010` and `2016 – Present` still fail as before and a numerically
|
||||||
|
formatted range (`03/2016–07/2024`, `01.2016 – 12.2024`, `2016-03–2024-07`, `2016/17–2018/19`)
|
||||||
|
is not a bare number on either side and is still caught. Three new `test_verify_pdf.py`
|
||||||
|
cases; the quantity one fails on master.
|
||||||
|
|
||||||
- **`upstream_triage` forces UTF-8 stdout and decodes git output as UTF-8** (#544,
|
- **`upstream_triage` forces UTF-8 stdout and decodes git output as UTF-8** (#544,
|
||||||
`tools/upstream_triage.py`, `tests/test_tools_utf8_output.py`) - like the other Python tools,
|
`tools/upstream_triage.py`, `tests/test_tools_utf8_output.py`) - like the other Python tools,
|
||||||
the triage report now calls `_force_utf8_output()` on startup and decodes git subprocess
|
the triage report now calls `_force_utf8_output()` on startup and decodes git subprocess
|
||||||
|
|||||||
@@ -106,6 +106,39 @@ 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_numeric_range_with_year_shaped_ends_is_a_quantity_not_a_date(self):
|
||||||
|
# Four-digit numbers that are not both years: #512 bounded the year but
|
||||||
|
# a salary band or a throughput range still read as a date range.
|
||||||
|
for text in (
|
||||||
|
"Salary EUR 1500\u20132000 per month",
|
||||||
|
"Scaled the API to serve 2000\u20135000 requests per second",
|
||||||
|
"Grew users from 2000\u20135000",
|
||||||
|
"Budget 1.5\u20132.0M, headcount 20\u201325",
|
||||||
|
):
|
||||||
|
with self.subTest(text=text):
|
||||||
|
self.assertEqual(find_non_ascii_date_ranges(text), [])
|
||||||
|
|
||||||
|
def test_numerically_formatted_date_ranges_are_still_caught(self):
|
||||||
|
# A composite token (slashed, dotted, hyphenated) is not a bare number,
|
||||||
|
# so the quantity rule does not apply and the year rule still fires.
|
||||||
|
# These are the Workday-drops-the-date shape the flag exists for.
|
||||||
|
for text in (
|
||||||
|
"03/2016\u201307/2024",
|
||||||
|
"01.2016 \u2013 12.2024",
|
||||||
|
"2016-03\u20132024-07",
|
||||||
|
"2016/17\u20132018/19",
|
||||||
|
"2016/03\u20132024/07",
|
||||||
|
"01.03.2016\u201331.07.2024",
|
||||||
|
"2016\u201307/2024",
|
||||||
|
):
|
||||||
|
with self.subTest(text=text):
|
||||||
|
self.assertEqual(len(find_non_ascii_date_ranges(text)), 1)
|
||||||
|
|
||||||
|
def test_two_years_joined_by_a_dash_are_still_a_date_range(self):
|
||||||
|
for text in ("2016\u20132024", "1990\u20132010 programme", "Roles 2000\u20132024"):
|
||||||
|
with self.subTest(text=text):
|
||||||
|
self.assertEqual(len(find_non_ascii_date_ranges(text)), 1)
|
||||||
|
|
||||||
def test_a_standalone_year_next_to_a_longer_number_is_still_caught(self):
|
def test_a_standalone_year_next_to_a_longer_number_is_still_caught(self):
|
||||||
self.assertEqual(find_non_ascii_date_ranges("2016\u20132024 (120000 users)")[0][1], "\u2013")
|
self.assertEqual(find_non_ascii_date_ranges("2016\u20132024 (120000 users)")[0][1], "\u2013")
|
||||||
|
|
||||||
|
|||||||
+48
-3
@@ -103,6 +103,45 @@ _YEAR = r"(?<!\d)(?:19|20)\d{2}(?!\d)"
|
|||||||
NON_ASCII_DATE_RANGE = re.compile(
|
NON_ASCII_DATE_RANGE = re.compile(
|
||||||
rf"{_YEAR}[^\S\n]*[{NON_ASCII_DASHES}]|[{NON_ASCII_DASHES}][^\S\n]*{_YEAR}"
|
rf"{_YEAR}[^\S\n]*[{NON_ASCII_DASHES}]|[{NON_ASCII_DASHES}][^\S\n]*{_YEAR}"
|
||||||
)
|
)
|
||||||
|
# The whitespace-delimited tokens touching the dash on either side. A range whose
|
||||||
|
# two ends are both BARE numbers is a date only when both are years: `EUR 1500-2000`
|
||||||
|
# and `2000-5000 requests/s` are quantities that happen to contain a year-shaped
|
||||||
|
# number, and the guide tells users to keep `--` in exactly those. A composite
|
||||||
|
# token (`03/2016`, `01.2016`, `2016-03`, `2016/17`) is not a bare number, so a
|
||||||
|
# numerically formatted date range still falls through to the year rule.
|
||||||
|
_TOKEN_BEFORE = re.compile(r"(\S+)[^\S\n]*$")
|
||||||
|
_TOKEN_AFTER = re.compile(r"^[^\S\n]*(\S+)")
|
||||||
|
_BARE_NUMBER = re.compile(r"\d+(?:[.,]\d+)*[kKmM%]?")
|
||||||
|
_BARE_YEAR = re.compile(r"(?:19|20)\d{2}")
|
||||||
|
_OUTER_PUNCTUATION = ".,;:!?()[]{}\"'"
|
||||||
|
|
||||||
|
|
||||||
|
def _bare_number(token):
|
||||||
|
"""The token with outer punctuation stripped, if it is a bare number, else None.
|
||||||
|
|
||||||
|
A dotted or comma-separated token whose groups include a year (`01.2016`,
|
||||||
|
`31.07.2024`) is a date written numerically, not a quantity, and is left to
|
||||||
|
the year rule; `1,234.56` and `2.0M` have no year group and stay numbers.
|
||||||
|
"""
|
||||||
|
core = token.strip(_OUTER_PUNCTUATION)
|
||||||
|
if not _BARE_NUMBER.fullmatch(core):
|
||||||
|
return None
|
||||||
|
groups = re.split(r"[.,]", core.rstrip("kKmM%"))
|
||||||
|
if len(groups) > 1 and any(_BARE_YEAR.fullmatch(g) for g in groups):
|
||||||
|
return None
|
||||||
|
return core
|
||||||
|
|
||||||
|
|
||||||
|
def _quantity_range(line, dash_index):
|
||||||
|
"""True when both sides of the dash are bare numbers and not both years."""
|
||||||
|
before = _TOKEN_BEFORE.search(line[:dash_index])
|
||||||
|
after = _TOKEN_AFTER.search(line[dash_index + 1 :])
|
||||||
|
if not before or not after:
|
||||||
|
return False
|
||||||
|
left, right = _bare_number(before.group(1)), _bare_number(after.group(1))
|
||||||
|
if left is None or right is None:
|
||||||
|
return False
|
||||||
|
return not (_BARE_YEAR.fullmatch(left) and _BARE_YEAR.fullmatch(right))
|
||||||
|
|
||||||
|
|
||||||
def find_non_ascii_date_ranges(text):
|
def find_non_ascii_date_ranges(text):
|
||||||
@@ -112,15 +151,21 @@ def find_non_ascii_date_ranges(text):
|
|||||||
maps U+2013 back to `-` so `--contains "2016-2024"` can match what the
|
maps U+2013 back to `-` so `--contains "2016-2024"` can match what the
|
||||||
template renders, which is exactly why `--contains` cannot see this defect.
|
template renders, which is exactly why `--contains` cannot see this defect.
|
||||||
A year on either side of the dash is enough ("Mar 2016 - Jul 2016",
|
A year on either side of the dash is enough ("Mar 2016 - Jul 2016",
|
||||||
"2016 - Present"), so the check is locale-agnostic; a numeric range with
|
"2016 - Present"), so the check is locale-agnostic. A range whose two ends
|
||||||
no year ("EUR 600k-1M") is not a date and is left alone.
|
are both bare numbers is a date only when both are years: "EUR 600k-1M",
|
||||||
|
"EUR 1500-2000" and "2000-5000 requests/s" are quantities and are left
|
||||||
|
alone, while a numerically formatted date ("03/2016-07/2024", "2016-03-2024-07")
|
||||||
|
is not a bare number on either side and is still caught.
|
||||||
"""
|
"""
|
||||||
hits = []
|
hits = []
|
||||||
for match in NON_ASCII_DATE_RANGE.finditer(text):
|
for match in NON_ASCII_DATE_RANGE.finditer(text):
|
||||||
dash = next(c for c in match.group(0) if c in NON_ASCII_DASHES)
|
dash = next(c for c in match.group(0) if c in NON_ASCII_DASHES)
|
||||||
line_start = text.rfind("\n", 0, match.start()) + 1
|
line_start = text.rfind("\n", 0, match.start()) + 1
|
||||||
line_end = text.find("\n", match.end())
|
line_end = text.find("\n", match.end())
|
||||||
line = text[line_start : line_end if line_end != -1 else len(text)]
|
line_end = line_end if line_end != -1 else len(text)
|
||||||
|
line = text[line_start:line_end]
|
||||||
|
if _quantity_range(line, match.start() + match.group(0).index(dash) - line_start):
|
||||||
|
continue
|
||||||
hits.append((" ".join(line.split()), dash))
|
hits.append((" ".join(line.split()), dash))
|
||||||
return hits
|
return hits
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user