Skip to main content

vi-textmatch

test

Vietnamese text normalization and type-aware comparison. Zero dependencies, standard library only.

Built for document verification — checking that a drafted contract matches the source record — where "close enough" is the right answer for a person's name and a catastrophically wrong answer for an ID number.


The problem

Two strings that look identical on screen are often not equal in memory:

"Huệ" == "Huệ"   # False

One is NFC (ệ = 1 codepoint), the other NFD (e + combining dot below + combining circumflex = 3). macOS filenames are NFD; most other sources are NFC.

And Vietnamese tone marks have two accepted positions in open syllables:

"hoà" == "hòa"   # False — same word, different bytes

Meanwhile, applying a naive oà → òa rewrite corrupts closed syllables: hoàn → hòan, suýt → súyt, huỳnh → hùynh — none of which are Vietnamese words. Worse, the corruption creates false matches between different words.

Install

pip install vi-textmatch

Python 3.11+. No third-party dependencies.

Usage

from vi_textmatch import normalize, compare, Kind

normalize("Nguyễn Thị  Hoà ")     # 'nguyễn thị hòa'
normalize("hoàn")                  # 'hoàn'  — closed syllable, untouched
normalize("suýt")                  # 'suýt'  — closed syllable, untouched

compare("Nguyễn Thị Hòa", "nguyễn thị hoà", kind=Kind.TEXT)
# Verdict(match=True,  level='IDENTICAL')

compare("Nguyễn Thị Hòa", "Nguyễn Thị Hoa",  kind=Kind.TEXT)
# Verdict(match=True,  level='TYPO')        — one character off, likely OCR/typing

compare("Nguyễn Thị Hòa", "Trần Văn Bình",   kind=Kind.TEXT)
# Verdict(match=False, level='DIFFERENT')   — different person

compare("031234567890", "031234567891", kind=Kind.ID)
# Verdict(match=False)   — one digit apart is a different person, not a typo

compare("2.500.000.000", "2,500,000,000", kind=Kind.MONEY)   # match
compare("2.500.000.000", "2.600.000.000", kind=Kind.MONEY)   # no match

kind is keyword-only and has no default. Calling compare(a, b) raises TypeError rather than silently falling back to fuzzy text matching.

Comparison kinds

Kind Method One character apart means
TEXT normalize, then difflib ratio possibly a typo — graded, see below
ID strip separators, then exact equality a different entity
MONEY parse to Decimal, compare values a different amount
DATE parse to date, compare values a different date

TEXT grades the difference so you can act on it:

ratio level Interpretation
1.0 IDENTICAL same string after normalization
≥ 0.85 TYPO likely a typing or OCR error — fix the text
< 0.85 DIFFERENT likely the wrong record — stop and re-check

Similarity is meaningless for identifiers and amounts, so ID, MONEY and DATE never use it. MONEY and DATE raise on input they cannot parse; they never silently degrade to string comparison.

Normalization pipeline

normalize() applies four steps, in this order:

  1. NFC — compose combining marks. Must come first: step 2 matches single codepoints.
  2. Tone placement — rewrite hoà → hòa only in open syllables.
  3. Whitespace — strip nbsp/zero-width, collapse runs, trim.
  4. casefold — must come last: step 2 needs the original case to preserve it.

Each step is also exported individually.

Tone placement rule

The tone mark sits on the syllable nucleus. Two written forms exist only for oa, oe, uy in an open syllable (nothing follows the pair). In a closed syllable there is exactly one correct form and it must never be rewritten:

Rewritten Left alone
open hoà → hòa, thuỷ → thủy, hoè → hòe —
closed — hoàn, suýt, huỳnh, khuỷu, khoét, huých
pre-accented vowel (ê ô ơ ă â ư) — huệ, thuở, chuồn, thuyền, giường
qu / gi initial cluster — quý, quỳnh, quán, già, giữ

This library deliberately avoids the labels "old style" and "new style": published sources use them in opposite directions. Positions are named explicitly instead.

See research/QUY-TAC.md for the derivation and sources.

Scope

This library knows orthography, not vocabulary.

It does: normalize spelling variants, compare by data type, grade differences.

It does not: know whether a string is a real Vietnamese word, find names inside text (that is NER — see underthesea), correct spelling, or understand meaning.

Development

PYTHONPATH=src python3 -m unittest discover tests

License

MIT

Metadata

Release files for vi-textmatch 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vi-textmatch 0.2.0
File Size Uploaded
vi_textmatch-0.2.0.tar.gz 20.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vi-textmatch 0.2.0
File Interpreter ABI Platform
vi_textmatch-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 34.7 kB

Release files / vi_textmatch-0.2.0.tar.gz

Download URL vi_textmatch-0.2.0.tar.gz
Size 20.4 kB
Tags Source
SHA-256 checksum
How to use checksums
d0201c3b0f10f652d0171a49a01d321f5753d712d538312b02902576769761f3
BLAKE2b-256 checksum
How to use checksums
264c250d2ff02407a9dd4e24c52ecae3e8be6681a2358fcb2ddca5ca359896b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / vi_textmatch-0.2.0-py3-none-any.whl

Download URL vi_textmatch-0.2.0-py3-none-any.whl
Size 14.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1210b180b1d6e95557d2d65ee9d91a688b187000e57a597860cd45ee7a093369
BLAKE2b-256 checksum
How to use checksums
7ab3bbe5bec4b620f8d3490bfcd62aa0427139557e4491ef033ce58c58bd4268
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page