YorozuyaIT
Japanese Text & Encoding

Japanese Text That Looks Identical but Won't Match: NFC, NFD, and Half-Width Characters

Written by the Yorozuya IT editorial team, based in Japan. Menus, apps, and rules change, so check the date.

Short answer

You search for ガイドライン and the file is right there, but your program says no match. Or two database rows that look identical are counted as different people. In Japanese text this is very often a normalization problem, not a typo.

One character, two encodings

Voiced kana such as ガ can be encoded two ways:

Comparison of NFC and NFD for ガ, with bar charts of code points and bytes for ガイドライン

On screen they are the same. In code they are not:

import unicodedata as u

nfc = "ガイドライン"
nfd = u.normalize("NFD", nfc)

print(nfc == nfd)                  # False
print(len(nfc), len(nfd))          # 6 8
print(len(nfc.encode()), len(nfd.encode()))   # 18 24
print(u.normalize("NFC", nfd) == nfc)         # True

Where does the decomposed form come from? Historically, macOS file systems stored filenames decomposed, and text copied from some Mac apps, ZIP files made on macOS, and file names pulled through some sync tools can still arrive that way. On Windows or Linux, the same text typed from a keyboard is composed.

The fix: normalize at the boundary

Pick one form and convert incoming text right where it enters your system: file listings, uploads, form input, imports.

def norm(s: str) -> str:
    return unicodedata.normalize("NFC", s)

Compare and store the NFC form. This one step fixes most "invisible mismatch" bugs.

Half-width kana and full-width letters

Japanese data also contains half-width katakana (ガ, パソコン) from old systems, and full-width Latin letters and digits (ABC123). These are different code points from the characters you expect, so they will not match either. NFKC folds them:

Input After NFKC
ガイド ガイド
ABC123 ABC123
㈱ (株)
① 1
㌔ キロ

The last three rows are the warning. NFKC is a compatibility normalization and it rewrites characters, so do not use it to store the original text. Use it to build a key for searching or deduplicating:

def search_key(s: str) -> str:
    return unicodedata.normalize("NFKC", s).casefold().strip()

Which form to use where

Task Use
Storing text, comparing filenames NFC
Search index, deduplication, user-typed lookups NFKC (+ casefold) on a copy
Writing to a legacy encoding Check the target first (see the Shift_JIS vs CP932 guide)

Quick checklist

FAQ

Why does my search miss a filename that is clearly there?

The stored name is probably NFD while your query is NFC, or the reverse. Normalize both sides to the same form before comparing.

Should I normalize everything to NFKC?

No. NFKC is lossy: it turns ㈱ into (株) and ① into 1. Keep the original text and use NFKC only to build a search or matching key.

Does this affect Chinese and Korean text?

Normalization affects any script with combining marks. Korean Hangul is a well-known case. The tools are the same.

Keep reading