Skip to content

Fix modulus 97 exact multiple - #44

Merged
risktoparkrageram merged 4 commits into
mainfrom
fix-modulus-97-exact-multiple
Aug 7, 2026
Merged

Fix modulus 97 exact multiple#44
risktoparkrageram merged 4 commits into
mainfrom
fix-modulus-97-exact-multiple

Conversation

@risktoparkrageram

Copy link
Copy Markdown
Collaborator

This PR fixes a bug and updates dev dependencies and CI

The reduction used `97 - (sum % 97)`, which returns 97 when the sum is an
exact multiple of 97. The reference algorithm stops subtracting at zero, so
the expected check digits in that case are 00.

The consequence was that no VAT number with check digits 00 could ever
validate, while its "...97" counterpart was wrongly accepted. Both branches
were affected: the modulus 97 branch when the weighted sum was a multiple of
97, and the 9755 branch when the weighted sum was congruent to 42 mod 97
(i.e. once the 55 offset is added). For example GB200394900 and GB200006400
are valid but were rejected; GB200394997 and GB200006497 are invalid but
were accepted.

Adding an outer modulo fixes both. `abs()` is now dead, since the result is
always in 0..96.

Verified exhaustively over the checksum's whole input domain (weighted sums
0..315 by check digits 00..99, both branches) and end to end against a
transcription of the reference implementation over 300,000 random numbers.
Refresh uv.lock to current releases, notably mypy 1.20 -> 2.3, pytest 9.0 ->
9.1, ruff 0.15 -> 0.16, coverage 7.13 -> 7.15 and ipython 9.13 -> 9.16. The
dev group floors move up to match what is actually tested, since mypy>=1 and
pytest>=8 no longer described the versions in the lock.

ruff 0.16 formats Python inside Markdown, which the README example did not
satisfy, so reflow it. The "#>" output marker becomes a plain "#" comment,
which the formatter leaves alone.

Bump actions/checkout v4 -> v7 and astral-sh/setup-uv v5 -> v9.0.0. setup-uv
publishes no floating v9 tag, so that one is pinned exactly. Drop the
python-version-file input from the release workflow: setup-uv has never
accepted it, so it was being silently ignored, and uv reads .python-version
by itself.

Use uv sync --locked in both workflows so a stale lockfile fails CI rather
than being silently re-resolved.

Python support is unchanged at 3.11-3.14. 3.15 is still at 3.15.0a8 and
cannot be added yet: both mypy's and coverage's compiled extensions abort
with "SystemError: unknown slot ID 85" on it.

Verified by running the workflow commands (uv sync --locked --dev, uv run
./run-tests.sh) against 3.11, 3.12, 3.13 and 3.14.
@risktoparkrageram risktoparkrageram self-assigned this Aug 6, 2026
@risktoparkrageram
risktoparkrageram requested a review from a team August 6, 2026 17:03
get_digits_from_string filtered with str.isdigit(), which is true for the
whole Unicode digit property, and then converted with int(), which only
accepts decimal digits. Characters such as the superscript "²" and the
circled "①" therefore passed the filter and raised

    ValueError: invalid literal for int() with base 10: '²'

out of both get_digits_from_string and validate_vat_number, where every
other unexpected character is simply ignored.

Filter with str.isdecimal() instead, which is true for exactly the category
Nd characters that int() will convert. Superscripts, circled digits and
vulgar fractions ("½") are now stripped as junk like emoji and punctuation
already were, so "GB²83092723①1" validates rather than raising, and "GB²²²"
is merely too short. Decimal digits outside ASCII, such as the Arabic-Indic
"٣", keep their existing behaviour of converting, since int() handles them.
@risktoparkrageram

Copy link
Copy Markdown
Collaborator Author

thank you @janrito

@risktoparkrageram
risktoparkrageram merged commit 127653f into main Aug 7, 2026
5 checks passed
@risktoparkrageram
risktoparkrageram deleted the fix-modulus-97-exact-multiple branch August 7, 2026 09:52
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.

2 participants