🔄

Unicode Normalization: NFC, NFD, NFKC, NFKD DemystifiedUnicode正規化完全ガイド: NFC/NFD/NFKC/NFKD の違い

Why the same-looking text can have different bytes, when each normalization form matters, and how to see the differences visually.同じに見えるテキストがなぜ異なるバイト列になるのか。4つの正規化形式の使い分けを視覚的に解説。

The Problem: "café" Encoded Two Ways

Type café on two different computers and you might get two completely different byte sequences — even though the text looks identical on screen. The letter é can be represented as a single code point U+00E9 (LATIN SMALL LETTER E WITH ACUTE), or as two code points: U+0065 (e) followed by U+0301 (COMBINING ACUTE ACCENT).

This means 'caf\u00E9' === 'cafe\u0301' evaluates to false in JavaScript, even though both strings render identically. File systems, databases, and search engines all face this problem: the same human-readable text can have multiple valid binary representations. A user searching for “café” might miss results stored in the other form.

Unicode normalization exists to solve exactly this problem. It defines deterministic algorithms for converting between equivalent representations, ensuring that text which looks the same actually compares as equal.

Canonical Equivalence: NFC and NFD

The two most common normalization forms handle canonical equivalence — sequences that represent the exact same abstract character:

FormNameStrategycafé
NFCCanonical Decomposition + CompositionCompose into fewest code pointsc a f é (4 CPs)
NFDCanonical DecompositionDecompose into base + combining marksc a f e ◌́ (5 CPs)

NFD breaks every precomposed character into its base character plus combining marks. The é (U+00E9) becomes e (U+0065) + combining acute accent (U+0301). Characters that are already in their most decomposed form remain unchanged. NFD also applies canonical ordering to ensure combining marks appear in a deterministic order.

NFC first performs the full NFD decomposition, then recomposes the result using canonical composition rules. The net effect is that each character is represented by the fewest possible code points. NFC is by far the most common normalization form on the web: the W3C recommends NFC for all web content, and most modern operating systems use NFC by default. The notable exception is macOS, whose HFS+ file system historically stored filenames in a variant of NFD.

// NFC and NFD produce the same visual result
const nfc = "caf\u00E9";           // "café" — 4 code points
const nfd = "cafe\u0301";          // "café" — 5 code points

nfc === nfd;                        // false!
nfc.normalize("NFC") === nfd.normalize("NFC");  // true
nfc.normalize("NFD") === nfd.normalize("NFD");  // true

Compatibility Equivalence: NFKC and NFKD

Beyond canonical equivalence, Unicode defines compatibility equivalence: characters that are semantically similar but not identical. These are characters that were given separate code points for historical or formatting reasons but are considered “the same” for many practical purposes.

OriginalNFKC/NFKD resultRelationship
fi (U+FB01)fiLigature → component letters
ffl (U+FB04)fflLigature → component letters
㍻ (U+337B)平成Square composition → characters
㌔ (U+3314)キロSquare composition → characters
㍑ (U+3351)リットルSquare composition → characters
① (U+2460)1Enclosed numeral → digit
H (U+FF28)HFullwidth → ASCII
ℌ (U+210C)HLetterlike symbol → letter

NFKD (Compatibility Decomposition) performs canonical decomposition plus compatibility decomposition, breaking apart ligatures, square compositions, and other compatibility characters. NFKC (Compatibility Decomposition + Composition) does the same decomposition and then recomposes using canonical composition — like NFC applied after NFKD.

Compatibility normalization is lossy: the round-trip 平成 is impossible because NFKC/NFKD destroys the information that the original was a single square composition character. Use compatibility normalization for search and matching, but preserve the original form for display.

The 4-Form Comparison Matrix

The four normalization forms can be organized along two axes: canonical vs. compatibility decomposition, and whether recomposition is applied:

Canonical onlyCanonical + Compatibility
ComposedNFCNFKC
DecomposedNFDNFKD

Here is how a single example character transforms under each form:

InputNFCNFDNFKCNFKD
é (U+00E9)é (U+00E9)e◌́ (U+0065 U+0301)é (U+00E9)e◌́ (U+0065 U+0301)
fi (U+FB01)fi (U+FB01)fi (U+FB01)fi (U+0066 U+0069)fi (U+0066 U+0069)
㍻ (U+337B)㍻ (U+337B)㍻ (U+337B)平成平成
Å (U+00C5)Å (U+00C5)A◌̊ (U+0041 U+030A)Å (U+00C5)A◌̊ (U+0041 U+030A)
Å (U+212B)Å (U+00C5)A◌̊ (U+0041 U+030A)Å (U+00C5)A◌̊ (U+0041 U+030A)

Notice the last two rows: U+00C5 (LATIN CAPITAL LETTER A WITH RING ABOVE) and U+212B (ANGSTROM SIGN) are canonically equivalent. NFC maps both to U+00C5, NFD maps both to U+0041 U+030A, and the compatibility forms follow the same pattern. This is a case where two distinct code points represent the same abstract character.

Practical Impact: Comparison, Databases, and Security

Normalization issues surface in surprisingly many real-world systems:

  • String comparison: Without normalization, === fails on visually identical strings. Always normalize before comparing user input.
  • Database uniqueness: A UNIQUE constraint on a username field will allow both café (NFC) and café (NFD) as separate entries. PostgreSQL and SQLite do not normalize by default.
  • Search and indexing: Elasticsearch and other search engines typically apply NFKC normalization in their analyzers to ensure find matches find.
  • Security: Homoglyph attacks can exploit normalization differences. An attacker might register a domain using compatibility characters that normalize to a known brand name. NFKC is used in PRECIS (RFC 8264) for username and password preparation.
  • File systems: macOS HFS+ stores filenames in a modified NFD form, while most Linux systems store bytes as-is. Copying files between systems can create duplicates that look identical but differ at the byte level.
// Always normalize before comparing
function safeEquals(a: string, b: string): boolean {
  return a.normalize("NFC") === b.normalize("NFC");
}

// For search: use NFKC to catch compatibility variants
function normalizeForSearch(s: string): string {
  return s.normalize("NFKC").toLowerCase();
}

normalizeForSearch("find");  // "find"
normalizeForSearch("㍻");   // "平成"

When to Use Which Form

Choosing the right normalization form depends on your use case:

Use caseRecommended formWhy
Web content / HTMLNFCW3C recommendation; smallest representation
Database storageNFCConsistent canonical form; preserves formatting characters
String comparisonNFC or NFDEither works as long as both sides agree
Search / indexingNFKCCatches ligatures, fullwidth, and other compatibility variants
Username / passwordNFKC (PRECIS)RFC 8264 requires NFKC for identifier normalization
Display / renderingPreserve originalDo not normalize display text; you lose formatting intent

The golden rule: normalize early, normalize consistently. Pick one form for your system and apply it at the boundary where text enters. NFC is the safe default for most applications. Switch to NFKC only when you specifically need to collapse compatibility variants (search, security checks).

Be cautious with NFKC/NFKD in contexts where formatting matters. Converting (a single-character representation of the Japanese era name “Heisei”) to 平成 is helpful for search, but the original compact form carries distinct semantic meaning in Japanese typography. Similarly, converting to fi is correct for search but may affect typographic rendering in some fonts.

問題: 「café」の2つのエンコード方法

2台の異なるコンピュータで café と入力すると、画面上は同じに見えるにもかかわらず、まったく異なるバイト列が生成されることがあります。文字 é は単一のコードポイント U+00E9(LATIN SMALL LETTER E WITH ACUTE)としても、2つのコードポイント U+0065(e)+ U+0301(COMBINING ACUTE ACCENT)としても表現できます。

つまり JavaScript では 'caf\u00E9' === 'cafe\u0301'false になります。両方の文字列がまったく同じに見えるにもかかわらずです。ファイルシステム、データベース、検索エンジンはすべてこの問題に直面します:同じ人間可読テキストが複数の有効なバイナリ表現を持ちうるのです。「café」を検索したユーザーが、別の形式で保存された結果を見逃す可能性があります。

Unicode正規化はまさにこの問題を解決するために存在します。等価な表現間の変換を決定論的なアルゴリズムで定義し、同じに見えるテキストが実際に等しいと比較されることを保証します。

正準等価性: NFC と NFD

最も一般的な2つの正規化形式は正準等価性(canonical equivalence)を扱います。これはまったく同じ抽象文字を表すシーケンスのことです:

形式名称戦略café
NFC正準分解 + 正準合成最少コードポイントに合成c a f é(4 CP)
NFD正準分解基底文字 + 結合文字に分解c a f e ◌́(5 CP)

NFD はすべての合成済み文字を基底文字と結合マークに分解します。é(U+00E9)は e(U+0065)+ 結合アキュートアクセント(U+0301)になります。既に最も分解された形式にある文字はそのままです。NFDは正準順序付け(canonical ordering)も適用し、結合マークが決定論的な順序で出現することを保証します。

NFC はまず完全なNFD分解を行い、次に正準合成規則で再合成します。結果として各文字が最少のコードポイントで表現されます。NFCはウェブで最も一般的な正規化形式です:W3CはすべてのWebコンテンツにNFCを推奨し、ほとんどの最新OSはデフォルトでNFCを使用します。注目すべき例外はmacOSで、HFS+ファイルシステムは歴史的にNFDの変種でファイル名を保存していました。

// NFC と NFD は同じ視覚的結果を生成する
const nfc = "caf\u00E9";           // "café" — 4コードポイント
const nfd = "cafe\u0301";          // "café" — 5コードポイント

nfc === nfd;                        // false!
nfc.normalize("NFC") === nfd.normalize("NFC");  // true
nfc.normalize("NFD") === nfd.normalize("NFD");  // true

互換等価性: NFKC と NFKD

正準等価性を超えて、Unicodeは互換等価性(compatibility equivalence)を定義します。意味的に類似しているが同一ではない文字のことです。歴史的またはフォーマット上の理由で個別のコードポイントが割り当てられたものの、多くの実用的な目的では「同じ」と見なされる文字です。

元の文字NFKC/NFKD の結果関係
fi (U+FB01)fi合字 → 構成文字
ffl (U+FB04)ffl合字 → 構成文字
㍻ (U+337B)平成組文字 → 文字列
㌔ (U+3314)キロ組文字 → 文字列
㍑ (U+3351)リットル組文字 → 文字列
① (U+2460)1囲み数字 → 数字
H (U+FF28)H全角 → ASCII
ℌ (U+210C)H記号文字 → 文字

NFKD(互換分解)は正準分解に加えて互換分解を実行し、合字、組文字、その他の互換文字を分解します。NFKC(互換分解 + 正準合成)は同じ分解を行った後、正準合成で再合成します。NFC を NFKD の後に適用したようなものです。

互換正規化は不可逆です:平成 のラウンドトリップは不可能です。元が単一の組文字だったという情報が NFKC/NFKD で失われるからです。互換正規化は検索やマッチングに使い、表示には元の形式を保持してください。

4つの正規化形式の比較マトリクス

4つの正規化形式は、正準分解 vs 互換分解、および再合成の有無という2つの軸で整理できます:

正準のみ正準 + 互換
合成NFCNFKC
分解NFDNFKD

各形式で文字がどのように変換されるかの具体例:

入力NFCNFDNFKCNFKD
é (U+00E9)é (U+00E9)e◌́ (U+0065 U+0301)é (U+00E9)e◌́ (U+0065 U+0301)
fi (U+FB01)fi (U+FB01)fi (U+FB01)fi (U+0066 U+0069)fi (U+0066 U+0069)
㍻ (U+337B)㍻ (U+337B)㍻ (U+337B)平成平成
Å (U+00C5)Å (U+00C5)A◌̊ (U+0041 U+030A)Å (U+00C5)A◌̊ (U+0041 U+030A)
Å (U+212B)Å (U+00C5)A◌̊ (U+0041 U+030A)Å (U+00C5)A◌̊ (U+0041 U+030A)

最後の2行に注目してください:U+00C5(LATIN CAPITAL LETTER A WITH RING ABOVE)と U+212B(ANGSTROM SIGN)は正準等価です。NFC は両方を U+00C5 にマッピングし、NFD は両方を U+0041 U+030A にマッピングし、互換形式も同様のパターンに従います。2つの異なるコードポイントが同じ抽象文字を表すケースです。

実用的な影響: 文字列比較、データベース、セキュリティ

正規化の問題は驚くほど多くの実世界のシステムで表面化します:

  • 文字列比較: 正規化なしでは、見た目が同じ文字列でも === が失敗します。ユーザー入力の比較前には必ず正規化してください。
  • データベースの一意性: ユーザー名フィールドの UNIQUE 制約は、café(NFC)と café(NFD)を別のエントリとして許可してしまいます。PostgreSQL と SQLite はデフォルトで正規化を行いません。
  • 検索とインデックス: Elasticsearch などの検索エンジンは通常、アナライザで NFKC 正規化を適用し、findfind にマッチすることを保証します。
  • セキュリティ: ホモグリフ攻撃は正規化の差異を悪用できます。攻撃者が既知のブランド名に正規化される互換文字でドメインを登録する可能性があります。NFKC は PRECIS(RFC 8264)でユーザー名とパスワードの準備に使用されます。
  • ファイルシステム: macOS の HFS+ はファイル名を修正NFD形式で保存しますが、ほとんどの Linux システムはバイト列をそのまま保存します。システム間でファイルをコピーすると、見た目は同じだがバイトレベルで異なる重複が作られることがあります。
// 比較前に必ず正規化する
function safeEquals(a: string, b: string): boolean {
  return a.normalize("NFC") === b.normalize("NFC");
}

// 検索用: NFKC で互換バリアントを統一
function normalizeForSearch(s: string): string {
  return s.normalize("NFKC").toLowerCase();
}

normalizeForSearch("find");  // "find"
normalizeForSearch("㍻");   // "平成"

どの正規化形式をいつ使うか

正しい正規化形式の選択はユースケースによって異なります:

ユースケース推奨形式理由
Webコンテンツ / HTMLNFCW3C推奨。最小のコードポイント表現
データベース格納NFC一貫した正準形式。書式文字を保持
文字列比較NFC または NFD双方が同じ形式であればどちらでも可
検索 / インデックスNFKC合字、全角文字などの互換バリアントを統一
ユーザー名 / パスワードNFKC (PRECIS)RFC 8264 が識別子の正規化に NFKC を要求
表示 / レンダリング元の形式を保持表示テキストは正規化しない。書式の意図が失われる

黄金律は:早期に正規化し、一貫して正規化する。システムで1つの形式を選び、テキストが入力される境界で適用してください。ほとんどのアプリケーションでは NFC が安全なデフォルトです。互換バリアントを統合する必要がある場合(検索、セキュリティチェック)にのみ NFKC に切り替えてください。

フォーマットが重要な文脈では NFKC/NFKD に注意が必要です。(日本の元号「平成」の1文字表現)を 平成 に変換するのは検索には有用ですが、元のコンパクトな形式は日本語の組版で固有の意味を持ちます。同様に fi に変換するのは検索には正しいですが、一部のフォントでのタイポグラフィックレンダリングに影響する可能性があります。