Skip to content

feat(text-normalizer): add property tests, NFC documentation, benchmarks and edge cases - #4

Merged
mohaelmrabet merged 2 commits into
mainfrom
feat/v0.1.x-hardening
Aug 14, 2026
Merged

feat(text-normalizer): add property tests, NFC documentation, benchmarks and edge cases#4
mohaelmrabet merged 2 commits into
mainfrom
feat/v0.1.x-hardening

Conversation

@mohaelmrabet

@mohaelmrabet mohaelmrabet commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardening pass on TextNormalizer for the v0.1.x line: no behaviour change, only additional test coverage, a benchmark harness, and documentation accuracy fixes.

Changes

  • Tests — three new cases in TextNormalizerTest:
    • testPropertyIdempotencenormalize(normalize(x)) === normalize(x) across six diverse samples (French, Arabic with diacritics, mixed Latin diacritics, whitespace/punctuation noise, emoji, Arabic yeh variants).
    • testNegativeCollisions — asserts distinct words stay distinct (hamza preservation ماء vs ما, and cote vs code).
    • testUnicodeEdgeCases — malformed UTF-8 (\x80\xA0\xA1) is returned untouched rather than silently emptied; mixed Arabic + Latin + emoji input normalizes as expected.
  • Benchmark — new tests/benchmark.php, wired as the composer benchmark script.
  • Docs — README now states that NFC output is guaranteed when Unicode normalization succeeds, and documents the malformed-UTF-8 passthrough; typo fix in the TextNormalizer doc block.

Test plan

  • composer test — 47 tests, 68 assertions, green on PHP 8.2 / 8.3 / 8.4 (lowest and highest deps)
  • composer analyse — no errors
  • composer benchmark

@mohaelmrabet
mohaelmrabet force-pushed the feat/v0.1.x-hardening branch from 3fc8ba4 to fb1b878 Compare August 14, 2026 18:12
testUnicodeEdgeCases asserted that invalid UTF-8 comes back untouched,
as both the README and the decompose() doc block promise, but it came
back as an empty string.

The guard in decompose() only ever covered Normalizer::normalize. It was
never reached: mb_strtolower runs first and replaces every malformed byte
with '?', which the separator pass then folds to spaces and trim removes.
Checking the encoding up front is the only place the input is still
intact, so normalize() now returns it as it is.

The mixed-script assertion in the same test was wrong on its own terms
and had never run, the malformed-UTF-8 assertion failing before it: this
profile folds 'à' to 'a' like any other Latin diacritic, and the emoji's
variation selector (U+FE0F) is a combining mark the profile does not
claim, so it survives by the same rule that keeps Arabic harakat.
@mohaelmrabet
mohaelmrabet force-pushed the feat/v0.1.x-hardening branch from fb1b878 to 1227391 Compare August 14, 2026 18:13
@mohaelmrabet
mohaelmrabet merged commit 98d5feb into main Aug 14, 2026
8 checks passed
@mohaelmrabet
mohaelmrabet deleted the feat/v0.1.x-hardening branch August 14, 2026 18:16
mohaelmrabet added a commit that referenced this pull request Aug 14, 2026
The working tree had reverted testUnicodeEdgeCases to the expectation
that shipped broken on the v0.1.x branch: 'à' kept its accent and the
emoji left no trace. Neither holds. This profile folds 'à' to 'a' like
any other Latin diacritic, and the variation selector trailing the emoji
(U+FE0F) is a combining mark the profile does not claim, so it survives
by the same rule that keeps Arabic harakat.
mohaelmrabet added a commit that referenced this pull request Aug 14, 2026
The working tree had reverted testUnicodeEdgeCases to the expectation
that shipped broken on the v0.1.x branch: 'à' kept its accent and the
emoji left no trace. Neither holds. This profile folds 'à' to 'a' like
any other Latin diacritic, and the variation selector trailing the emoji
(U+FE0F) is a combining mark the profile does not claim, so it survives
by the same rule that keeps Arabic harakat.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant