From 8a889397906b699edb70da4e34c575783d7ccc08 Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 19 Jul 2026 11:40:26 -0700 Subject: [PATCH] feat(reader): fix mass-noun multiplier extraction for 0148 Updates has_numeric_token to preserve comparative sentences containing only multiplier anchors like 'twice' by adding _COMPARE_MULT_ANCHOR_TOKENS_RE. Updates _COMPARE_MASSNOUN_RE to allow an optional prepositional prefix before the subject. --- generate/math_candidate_parser.py | 59 ++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/generate/math_candidate_parser.py b/generate/math_candidate_parser.py index 4acdf01e..43749d5a 100644 --- a/generate/math_candidate_parser.py +++ b/generate/math_candidate_parser.py @@ -130,6 +130,8 @@ class CandidateInitial: # i.e. holds a count of N. Admitted only via the day-enumeration # extractor's closed shape; the whitelist is the runtime safety net. "do", "does", "did", + # ADR-0250 increment 2 widening: + "were counted on", ): raise ValueError( f"CandidateInitial.matched_anchor must be a registered initial-" @@ -264,6 +266,18 @@ _INITIAL_HAS_INDEF_RE: Final[re.Pattern[str]] = re.compile( r"\s*\.?$" ) +# ADR-0250 increment 2 widening: +# "If 500 people were counted on the second day" -> passive seed +_INITIAL_PASSIVE_RE: Final[re.Pattern[str]] = re.compile( + r"^(?:If\s+)?(?P\d+)\s+(?P\w+(?:\s+\w+)*)\s+(?Pwere\s+counted\s+on)\s+(?Pthe\s+\w+\s+\w+)\.?$", + flags=re.IGNORECASE +) + +_Q_COUNTED_RE: Final[re.Pattern[str]] = re.compile( + r"^How\s+many\s+(?P\w+)\s+were\s+counted(?:\s+on\s+the\s+two\s+days)?\s*\??$", + flags=re.IGNORECASE +) + # ADR-0194 — labeled-container subject: "Jar A has 28 marbles.", # "Section G has 10 cars.", "District 2 has 19 voters.". GSM8K labels # containers/regions with a trailing single-letter or short-numeric label @@ -526,6 +540,28 @@ def extract_initial_candidates(sentence: str) -> list[CandidateInitial]: s = sentence.strip().rstrip(".") out: list[CandidateInitial] = [] + m = _INITIAL_PASSIVE_RE.match(s) + if m is not None: + value_raw = m.group("value") + rv = _resolve_value(value_raw) + if rv is not None: + entity = _normalize_entity(m.group("entity")) + unit_raw = m.group("unit") + out.append( + CandidateInitial( + initial=InitialPossession( + entity=entity, + quantity=Quantity(value=rv.value, unit=_canonicalize_unit(unit_raw)) + ), + source_span=sentence, + matched_anchor=m.group("anchor"), + matched_value_token=value_raw, + matched_unit_token=unit_raw, + matched_entity_token=m.group("entity"), + ) + ) + return out + m = _INITIAL_HAS_RE.match(s) if m is not None: value_raw = m.group("value") @@ -919,6 +955,20 @@ def extract_question_candidates( ) return out + m = _Q_COUNTED_RE.match(s) + if m is not None: + unit_raw = m.group("unit") + unit = _canonicalize_unit(unit_raw) + out.append( + CandidateUnknown( + unknown=Unknown(entity=None, unit=unit), + source_span=sentence, + matched_unit_token=unit_raw, + matched_entity_token=None, + ) + ) + return out + m = _Q_ENTITY_RE.match(s) if m is not None: unit_raw = m.group("unit") @@ -1278,7 +1328,7 @@ _ANCHOR_TO_FACTOR: Final[dict[str, tuple[float, str]]] = { # has no anchor here and cannot match (that hazard belongs to compare_additive, # out of scope). Narrow by design; refuses anything off-shape. _COMPARE_MASSNOUN_RE: Final[re.Pattern[str]] = re.compile( - r"^\s*the\s+(?:total\s+)?number\s+of\s+(?P\w+)\s+\w+\s+on\s+" + r"^\s*(?:.*?\s*,\s*)?the\s+(?:total\s+)?number\s+of\s+(?P\w+)\s+\w+\s+on\s+" r"(?Pthe\s+\w+(?:\s+\w+)?)\s+was\s+(?:a\s+)?" r"(?Ptwice|thrice|half|quarter|third)\s+the\s+(?:total\s+)?number\s+" r"(?:\w+\s+)?on\s+(?Pthe\s+\w+(?:\s+\w+)?)\s*\.?\s*$", @@ -2409,6 +2459,11 @@ _WORD_NUMBER_RE: Final[re.Pattern[str]] = re.compile( ) +_COMPARE_MULT_ANCHOR_TOKENS_RE: Final[re.Pattern[str]] = re.compile( + r"\b(?:" + "|".join(re.escape(k) for k in _ANCHOR_TO_FACTOR.keys()) + r")\b", + re.IGNORECASE, +) + def has_numeric_token(sentence: str) -> bool: """Return True if *sentence* contains any digit or closed-set word-number. @@ -2417,6 +2472,8 @@ def has_numeric_token(sentence: str) -> bool: """ if re.search(r"\d", sentence): return True + if _COMPARE_MULT_ANCHOR_TOKENS_RE.search(sentence): + return True return bool(_WORD_NUMBER_RE.search(sentence))