Toolkit Labs

Why two identical-looking strings are not equal

Because equality is defined on codepoints and the screen is not. Two strings can render identically and hold different codepoints, and the standard fixes for that - Unicode normalisation and case folding - each solve one slice of it. On 300 constructed strings, 26 are not already in NFC form, 80 change under NFKC, and normalisation fixes 0 of the 30 lookalike cases.

The three problems, kept apart

Normalisation is about the same character having more than one encoding: an accented letter as one codepoint or as a base plus a combining mark. NFC composes, NFD decomposes; both are lossless and reversible in the sense that they round-trip to the same normal form. Compatibility normalisation (NFKC/NFKD) goes further and rewrites characters that are merely similar - a ligature into two letters, a full-width letter into an ASCII one, a superscript digit into a digit. It is lossy on purpose. Confusables are different characters that merely look alike - a Cyrillic letter shaped exactly like an ASCII one. Nothing in normalisation touches those.

What the measurement says

questioncases out of 300
not already NFC - normalize("NFC", s) != s26
changed by NFKC80
whose length changes under NFKC47
whose length changes under casefold()25
where s.upper() has a different length19
where s.lower() has a different length3
where upper-then-lower does not return the original27
lookalike cases NFKC folds away0 of 30
zero-width cases NFKC removes0 of 30

The three traps in those rows

What to do instead

Where these numbers come from

Every count above is computed with Python's unicodedata on UCD 15.0.0 over UNICODE-300, a corpus of 300 labelled strings built to break naive text handling, in 10 categories of 30 cases - combining marks, joined emoji, astral codepoints, direction controls, zero-width characters, lookalikes, compatibility folds, case mapping and exotic whitespace. Twenty cases and the checker are free and public domain; the full corpus is a paid download. The corpus and the free sample are here.

Where these 300 strings come from

Every number on this page was computed from UNICODE-300: 300 labelled strings, each property measured with unicodedata rather than asserted. The full corpus is EUR 19, one-time; a 20-case slice and the checker are free on the product page, public domain, no account.