core/tests/test_adr_0128_loader.py
Shay 452e3bb9f5 feat(packs): ADR-0128.1+0128.2 — en_numerics_v1 + loader
Exhaustive English linguistic-form ontology for quantities:
cardinals (0..20 + tens + magnitudes + compound rule), ordinals
(1st..31st + decade/magnitude forms), named fractions (1/2..1/10
+ sixteenth/thirty-second) + symbol forms (½ ¼ ¾ ⅓ ⅔ ⅛ ⅜ ⅝ ⅞),
multipliers (double/triple/twice/half), quantifiers with
semantic_type (indefinite triggers refusal at parse time —
preserves wrong==0), comparison anchors migrated for
ratifiability, number-format regexes with positive/negative
corpora.

Loader API in language_packs/numerics_loader.py (sibling module
to be merged into main loader after Gemini's ADR-0127 loader
lands, to avoid concurrent merge conflict).

Ratification invariants gated: cardinal/ordinal/fraction
exhaustiveness, quantifier semantic-type closed set, format-regex
test corpora (10+ positive/negative per format, ambiguity
refused), manifest checksums = SHA-256 of bytes-on-disk,
self-sealing mastery report.

Cross-references en_units_v1 (Gemini ADR-0127): fraction symbols
authoritative here; en_units_v1 symbol-affix table will point to
these entries.

No parser changes (deferred to 0128.3-0128.6). No train-sample
re-run (joint exit gate with ADR-0127 runs after both packs land).

Total: 130 lexicon entries across 7 kinds.
Lanes: smoke 67/0/0, packs 6/0/0, ADR-0128 suite 243/0/0.
2026-05-23 07:02:09 -07:00

209 lines
7.2 KiB
Python

"""ADR-0128 sub-phase 0128.2 — loader API + compound composition.
Covers the public lookup surface plus the deterministic compound rules
(cardinal composition, compound-fraction composition, article-bound
fraction forms, case-insensitivity)."""
from __future__ import annotations
import pytest
from language_packs.numerics_loader import (
lookup_cardinal,
lookup_comparison_anchor,
lookup_comparison_anchors,
lookup_fraction,
lookup_multiplier,
lookup_ordinal,
lookup_quantifier,
parse_compound_cardinal,
)
# ---------------------------------------------------------------------------
# Cardinal lookup + compound composition
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,value", [
("zero", 0), ("seven", 7), ("seventeen", 17), ("twenty", 20),
("fifty", 50), ("ninety", 90), ("hundred", 100), ("thousand", 1000),
("million", 1_000_000),
])
def test_lookup_cardinal_simple(word, value):
entry = lookup_cardinal(word)
assert entry is not None and entry.numeric_value == value
def test_lookup_cardinal_case_insensitive():
assert lookup_cardinal("Seventeen").numeric_value == 17
assert lookup_cardinal("SEVENTEEN").numeric_value == 17
def test_lookup_cardinal_unknown_returns_none():
assert lookup_cardinal("seventeenish") is None
assert lookup_cardinal("") is None
@pytest.mark.parametrize("text,expected", [
("twenty-one", 21),
("twenty one", 21),
("ninety-nine", 99),
("one hundred", 100),
("two hundred", 200),
("two hundred and fifty", 250),
("three hundred and seventeen", 317),
("one thousand", 1000),
("two thousand five hundred", 2500),
("two thousand five hundred and seventeen", 2517),
("seventeen", 17),
("fifty", 50),
("thirty-two", 32),
("one million", 1_000_000),
])
def test_parse_compound_cardinal(text, expected):
assert parse_compound_cardinal(text) == expected
def test_parse_compound_cardinal_rejects_unknown_token():
assert parse_compound_cardinal("twenty banana") is None
assert parse_compound_cardinal("") is None
assert parse_compound_cardinal(" ") is None
# ---------------------------------------------------------------------------
# Ordinal lookup
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,pos", [
("first", 1), ("second", 2), ("third", 3), ("tenth", 10),
("twelfth", 12), ("twentieth", 20), ("twenty-first", 21),
("thirty-first", 31), ("hundredth", 100),
])
def test_lookup_ordinal(word, pos):
entry = lookup_ordinal(word)
assert entry is not None and entry.position == pos
def test_lookup_ordinal_case_insensitive():
assert lookup_ordinal("First").position == 1
# ---------------------------------------------------------------------------
# Fraction lookup + compound + article-bound
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("token,num,den", [
("half", 1, 2),
("a half", 1, 2),
("third", 1, 3),
("a third", 1, 3),
("quarter", 1, 4),
("the quarter", 1, 4),
("sixteenth", 1, 16),
("thirty-second", 1, 32),
])
def test_lookup_fraction_named_and_article_bound(token, num, den):
entry = lookup_fraction(token)
assert entry is not None
assert entry.numerator == num and entry.denominator == den
@pytest.mark.parametrize("token,num,den", [
("two-thirds", 2, 3),
("two thirds", 2, 3),
("three-quarters", 3, 4),
("three quarters", 3, 4),
("three-fourths", 3, 4), # American "fourths" alias
("five eighths", 5, 8),
("seven tenths", 7, 10),
("nine sixteenths", 9, 16),
])
def test_lookup_fraction_compound(token, num, den):
entry = lookup_fraction(token)
assert entry is not None
assert entry.numerator == num and entry.denominator == den
assert entry.decimal_value == pytest.approx(num / den)
@pytest.mark.parametrize("symbol,num,den", [
("½", 1, 2), ("¼", 1, 4), ("¾", 3, 4),
("", 1, 3), ("", 2, 3), ("", 1, 8),
("", 3, 8), ("", 5, 8), ("", 7, 8),
])
def test_lookup_fraction_symbol(symbol, num, den):
entry = lookup_fraction(symbol)
assert entry is not None
assert entry.numerator == num and entry.denominator == den
def test_lookup_fraction_unknown_returns_none():
assert lookup_fraction("zillionth") is None
assert lookup_fraction("") is None
# ---------------------------------------------------------------------------
# Multipliers
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,factor", [
("double", 2.0), ("triple", 3.0), ("quadruple", 4.0),
("quintuple", 5.0), ("twice", 2.0), ("thrice", 3.0), ("half", 0.5),
])
def test_lookup_multiplier(word, factor):
entry = lookup_multiplier(word)
assert entry is not None and entry.factor == factor
# ---------------------------------------------------------------------------
# Quantifiers
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,sem_type,det", [
("all", "total", None),
("none", "empty", 0),
("some", "indefinite", None),
("both", "paired", 2),
("each", "distributive", None),
("every", "distributive", None),
("many", "indefinite", None),
("few", "indefinite", None),
("several", "indefinite", None),
("most", "partial", None),
("any", "indefinite", None),
("no", "empty", 0),
("single", "total", 1),
])
def test_lookup_quantifier(word, sem_type, det):
entry = lookup_quantifier(word)
assert entry is not None
assert entry.semantic_type == sem_type
assert entry.determinate_value == det
def test_indefinite_quantifier_triggers_refusal_signal():
# The loader doesn't refuse on its own — it surfaces the signal that
# the parser will use to refuse. Test the signal is correctly tagged.
assert lookup_quantifier("some").is_indefinite is True
assert lookup_quantifier("all").is_indefinite is False
# ---------------------------------------------------------------------------
# Comparison anchors
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("word,anchor_class", [
("more", "additive"), ("fewer", "additive"), ("less", "additive"),
("additional", "additive"), ("extra", "additive"),
("missing", "additive"), ("remaining", "additive"),
("times", "multiplicative"), ("thrice", "multiplicative"),
("quadruple", "multiplicative"),
])
def test_lookup_comparison_anchor_single(word, anchor_class):
entry = lookup_comparison_anchor(word)
assert entry is not None and entry.anchor_class == anchor_class
def test_comparison_anchor_dual_class_for_half():
# 'half' / 'double' / 'twice' / 'triple' / 'third' / 'quarter' are
# multiplicative anchors but may also exist as multipliers/fractions.
# The comparison-anchor table puts them in multiplicative.
anchors = lookup_comparison_anchors("half")
assert any(a.anchor_class == "multiplicative" for a in anchors)
def test_comparison_anchor_unknown_returns_none():
assert lookup_comparison_anchor("zonk") is None
assert lookup_comparison_anchors("zonk") == ()