Skip to content

0.8.0-beta: contract tests, validator docs, Amount::toWords() - #28

Merged
navidkashani merged 5 commits into
mainfrom
improve/0.8
Oct 1, 2026
Merged

navidkashani merged 5 commits into
mainfrom
improve/0.8

Conversation

@navidkashani

Copy link
Copy Markdown
Contributor

What & why

0.8.0-beta: no breaking changes. It adds a cleanup pass, contract tests built from more of the vendored persian-tools specs, reference docs for every validator, Amount::toWords(), and better discoverability. Each commit covers one part, so you can review them one at a time.

1. Cleanup and mutation testing (23cd171)

  • Infection now skips the random ::fake() generators (mutators.global-ignore). That alone took full-repo MSI from 81% to 86%.
  • New tests kill the real survivors:
    • anchored length checks: a stray letter beside the digits, for NationalId / LegalId / PostalCode / BillId payment IDs;
    • the middle-zero window, asserted by error code;
    • one valid LegalId per check digit. Before, every fixture had check digit 0, so deleting the checksum loop passed the suite;
    • WordsToNumber edge cases, including the PHP_INT_MAX boundary.
  • The tests also showed a branch in Currency::convert() that did nothing (integer / already stays int on exact division). It's removed and output is unchanged.
  • MSI is now 88%. minMsi / minCoveredMsi and composer mutate enforce 88, and the CI job reads the thresholds from the config instead of overriding them with 0. The job is still advisory (continue-on-error).

2. persian-tools contract tests (720b676)

  • A new abstract PersianToolsFixtureTestCase holds the shared fixtures-present skip.
  • Four new classes cover legal ID, number words, formatting and text: LegalIdContractTest, NumberWordsContractTest, FormattingContractTest and TextContractTest.
  • Each class has a divergences() registry of [api, input, upstream, abzar, reason]. Every entry asserts abzar's output and that it still differs from upstream's. docs/en/persian-tools-parity.md lists them all.
  • The specs drove two code changes:
    • WordsToNumber reads شیش, چارصد, بیلیون and کوآدریلیون, so it parses every cardinal upstream writes.
    • Bug fix: HalfSpaceFixer ignored a suffix followed by ), ], », a quote or a colon. (آبی تر) now becomes (آبی‌تر), like آبی تر. already did.

3. Docs (e4bf08d)

  • New reference pages for national ID, legal ID, card number, IBAN, phone number and plate number.
  • docs/en/error-codes.md lists every ErrorCode with its Persian message, emitting class and kind (error, warning or thrown).
  • The new ErrorCodeDocsTest checks that each code has a row carrying its current message.

4. Amount::toWords(Unit $unit = Unit::TOMAN) (1d726cd)

  • 12,345 rials → یک هزار و دویست و سی و چهار تومان و پنج ریال. The rial remainder is spelled out, never truncated.
  • It covers the full int range and never throws.

5. Release prep (d8c0469)

  • composer.json has a new description and keywords.
  • README.fa.md is a Persian landing page, linked from the top of the README.
  • The install pins move to ^0.8@beta.
  • UPGRADE.md gets a "0.7 → 0.8" section.

Checklist

  • composer test passes: 982 tests, also run on PHP 8.1 in Docker.
  • composer phpstan is clean at level 8.
  • composer cs-check (php-cs-fixer) is clean.
  • Added / updated tests covering the change.
  • Updated CHANGELOG.md. The entries are already under [0.8.0-beta] — 2026-10-01, since this PR is the release.
  • Documentation (README.md / docs/en/*) updated. Every documented example output was checked against what the code actually returns.
  • Full-repo infection passes at the new floor (MSI 88.8%, run in Docker on php:8.2-cli with pcov).
  • composer validate --strict passes, and git archive HEAD includes README.fa.md.

Type of change

  • Bug fix (HalfSpaceFixer before closing brackets)
  • New feature (Amount::toWords(), the new WordsToNumber spellings)
  • Documentation

Notes for reviewers

  • Two outputs widen. WordsToNumber now parses spellings that returned null before, and HalfSpaceFixer binds suffixes in more places. Both are noted in UPGRADE.md in case anyone asserts on the old output.

  • Divergences beyond the planned list:

    • abzar has no removeCommas; withSeparators($s, '') returns a string;
    • DigitConverter folds every digit set into its target;
    • HalfSpaceFixer has no بی/هم prefix rules, no fixed compounds, and doesn't normalise whitespace.

    Each is recorded with its reason.

  • LegalIdContractTest has no divergences() registry. All 9 vectors match, and an empty data provider would fail under failOnWarning.

  • Still to do after merge: tag 0.8.0-beta. The release workflow finds the changelog section by that name.

Ignore the random fixture generators (*::fake, BillId::fakePaymentId) via
mutators.global-ignore and add tests for the real survivors:

- anchored length checks: NationalId, LegalId, PostalCode and BillId
  payment IDs with a stray leading/trailing letter
- middle-zero substr window and checksum codes asserted by ErrorCode
- LegalId: one valid ID per check digit, including the sum-10 fold
- WordsToNumber: outer trim, a second ممیز, empty tokens in the fraction,
  bare صد after هزار, the quadrillion key and the PHP_INT_MAX boundary

Currency::convert() drops a redundant int branch: integer `/` already
stays int on exact division, so its mutants were all equivalent.

Full-repo MSI is 88% (was 81%). minMsi / minCoveredMsi and the mutate
script now enforce 88, and the CI job reads the thresholds from the
config instead of overriding them with 0.
…ting and text

Extract the fixtures-present skip into an abstract PersianToolsFixtureTestCase
and add one contract class per domain, with vectors hand-lifted from the
vendored specs (each citing spec.ts:line):

- LegalIdContractTest: verifyIranianLegalId, all 9 vectors match
- NumberWordsContractTest: NumberToWords, addOrdinalSuffix, wordsToNumber,
  including upstream's UNITS / TEN / MAGNITUDE tables
- FormattingContractTest: addCommas, removeCommas, digits
- TextContractTest: isPersian, isArabic, toPersianChars, halfSpace

Each class has a divergences() registry of [api, input, upstream, abzar,
reason] that asserts abzar's output and that it still differs from
upstream's. docs/en/persian-tools-parity.md lists every entry.

Code changes driven by the specs:
- WordsToNumber reads شیش, چارصد, بیلیون and کوآدریلیون, so it parses
  everything upstream's numberToWords() writes.
- HalfSpaceFixer ends a suffix at a closing bracket, quote or colon, so
  `(آبی تر)` is bound like `آبی تر.` already was.
Add national-id, card-number, iban, legal-id, phone-number and
plate-number pages, following the postal-code / bill-id template:
usage, rules, warnings, error codes, details and accessors, plus
extractAll / masked / fake where the class has them.

docs/en/error-codes.md lists every ErrorCode case with its Persian
message, emitting class and kind (error, warning or thrown). The new
ErrorCodeDocsTest asserts each case has a row carrying its current
message, mirroring the completeness check in ErrorCodeMessageSnapshotTest.

Everything is linked from docs/en/README.md and the README's "Further
reading" section. Every documented output was checked against real output.
Amount::toWords(Unit $unit = Unit::TOMAN) reuses NumberToWords::convert()
and Unit::persianName():

  1,200,000 rials, RIAL  → یک میلیون و دویست هزار ریال
  1,200,000 rials, TOMAN → یکصد و بیست هزار تومان
  12,345 rials,    TOMAN → یک هزار و دویست و سی و چهار تومان و پنج ریال
  5 rials,         TOMAN → پنج ریال
  0,               TOMAN → صفر تومان

It defaults to Toman like Currency::format(), spells a sub-toman
remainder out instead of truncating it, and covers the full int range
without throwing. Documented in docs/en/currency.md and the README's
Money section.
- composer.json: description names every validator, money and text
  tools; keywords add card-number, sheba, postal-code, license-plate,
  bill-id, toman, rial, money, words-to-number, keyboard-layout,
  persian-digits, zwnj and ordinal.
- README.fa.md: Persian landing page (what abzar is, install, ten short
  examples, links to the English docs and UPGRADE.md), linked from the
  top of the README.
- CHANGELOG: 0.8.0-beta section. Install pins in README, installation and
  api-stability move to ^0.8@beta. UPGRADE.md gains a "0.7 → 0.8: no
  breaking changes" section noting the two widened outputs.
@navidkashani
navidkashani merged commit 1cbf980 into main Oct 1, 2026
9 checks passed
@navidkashani
navidkashani deleted the improve/0.8 branch October 1, 2026 12:31
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