Unicode等價性

由於歷史因素,Unicode需要相容許多既有標準,其中包括了許多字元,有些可能會與其它字元或字元序列等價。例如 U+00EAê(預組)與 U+0065e U+0302◌̂(結合序列)都是 ê,Unicode規定需要將它們視為等價進行比較、搜尋和排序。

標準等價與相容等價

Unicode又規定了兩種等價概念:標準等價(canonical)與相容等價(compatibility)。

標準等價是指兩者完全相同的,如前述的 ê,使用者在任何情況下,都不該假設 U+00EAêU+0065e U+0302◌̂ 兩種序列被視為不同。

相容等價則是指兩種序列基於某些性質,一般被視為不同。但或許在搜尋、排序上,可視為等價進行操作。例如「㍻」與「平成」相容等價。


Unicode資料庫對於每個字,定義有其「分解映射(decomposition mapping)」。

若分解序列為標準(canonical),則分解映射必須由 1 或 2 個字所構成。此時,分解映射的第 1 個字還有可能可以繼續再分解;第 2 個字則否。

非標準的分解映射則為相容等價,相容等價的分解映射則沒有長度限制。

未定義有「分解映射」的字,則無法再分解。即此字本分身即為標準分解形式。(諺文除外,見後述。)

Unicode正規化形式

為了要判斷是否等價,首先需要將字串進行正規化,才能方便比較。正規化形式有下列4種:

標準分解 NFD

將字串以標準分解映射分解到無法分解為止,再進行標準順序調整。如 U+1EC5 的標準分解是 U+0065e U+0302◌̂ U+0303◌̃U+304C 的標準分解是 U+304B U+3099◌゙

標準組合 NFC

先將字串進行標準分解後,再進行標準組合。

雖然 Unicode 定義有大量組合用字元,但基於歷史因素,幾乎所有既有編碼標準所收錄的都是預組文字(例如 U+304C)。Unicode 若僅以標準分解視為正規形式,則既有的大量資料、編碼都是非正規形式,這並不是理想情形。所以 Unicode 定義標準組合也是一種正規化形式。

相容分解 NFKD

同時以標準、相容分解映射,將字串所有文字分解到無法分解為止,再進行標準順序調整。

相容組合 NFKC

將字串進行相容分解後,再進行標準組合。

正規化處理

Singleton

一般來說,單一文字進行標準分解後,再進行標準組合,應該要等於原來的文字。但標準分解映射僅定義單一字元的情形除外。例如歐姆符號 U+2126 但正規分解是希臘文字母 U+03A9Ω,正規組合後仍會是 U+03A9Ω

這種情形稱為 Singleton,一般是用來修正傳統規格裡因故被分為兩個編碼,但其完全等義的情形,故正規化後都應使用較理想的字元。

CJK漢字相容區裡多數的漢字也都是 Singleton。如 U+FA0C 的正規形式是 U+5140

正規化安定性與組合排除

為了確保Unicode正規化形式不受版本干擾,Unicode規定每個字的正規化形式(包括NFC與NFD),在之後的新版本也必須盡可能確保它永遠正規。

例如目前 e̍ 並沒有收錄預組形式,NFD與NFC都是 e U+030D◌̍。但如果未來收了預組的 e̍,這一來就會讓NFC形式改變。原來NFC的 e U+030D◌̍ 不再是正規序列。

由於這會影響Unicode正規化形式安定性,所以又另外規定了組合排除的特例。例如 U+1D15E𝅗𝅥 的 NFD 雖然是 U+1D157𝅗 U+1D165◌𝅥,但規定這個字組合排除,NFC 仍是 U+1D157𝅗 U+1D165◌𝅥,確保安定性。


適用於組合排除的文字相當少,多數情況,為了確保Unicode正規化的安定性,已經可以用組合方式組出的文字,會被拒絕在新版本另外收錄預組文字。

標準順序

因為有很多組合用的修飾字元在視覺上沒有順序性,為了確保正規化後的形式固定,故Unicode又定義了標準順序,規定組合用字元以一定的順序排列。進行正規化計算時,都必須進行標準順序調整。


U+00E9é U+0323◌̣ →(分解) e U+0301◌́ U+0323◌̣ →(標準排序) e U+0323◌̣ U+0301◌́ →(標準組合) U+1EB9 U+0301◌́


以此例而言,可知 NFD 是 e U+0323◌̣ U+0301◌́,NFC 是 U+1EB9 U+0301◌́。原序列 U+00E9é U+0323◌̣ 並非正規形式,然這各種序列都是標準等價。


為了計算此標準順序,Unicode資料庫對組合用字元定義了組合類別(Combining Classes)。若字元可做為基底字元使用,通常不會再跟左方文字組合,則組合類別為0(大多數的文字組合類別都是0)。

組合位置不同的組合用字元會有不同的組合類別值,如 U+0323◌̣ 組合類別是 220(正下),U+0301◌́ 組合類別是 230(正上),故標準順序排序後 U+0323◌̣ 必須在 U+0301◌́ 前面。


因為相同位置的組合用字元順序不同時,組合結果並不等價,如 ǖ 與 ṻ,不應該有相同正規化結果,故標準順序排序演算法必須是穩定(stable)的,意即相同的組合類別在排序後順序必須保持不變。

實務情形

雖然Unicode規定了等價性與正規化,但實務上多數作業系統只在檔案系統的檔名等有限用途處理正規化,其餘情形運算責任是交給每個軟體與字型處理。

意即,雖然 U+00EAêU+0065e U+0302◌̂ 等價,互為NFC/NFD形式。但該去判斷它們等價的是每個軟體自己,實務上除了比較專業處理Unicode文書的軟體外,多數軟體可能沒有正確支援Unicode等價與正規化。

作業系統對字型檔取用字符時,也是未經正規化的。所以縱然 U+00EAêU+0065e U+0302◌̂ 標準等價,理想情況下應該呈現相同外形,但當字型檔沒有特別處理 U+0065e U+0302◌̂ 序列時,可能還是會取得與 U+00EAê 不同的外形。(即使如此,使用者還是不該期待這兩者被視為不同。)

CJK主要的Unicode正規化議題

諺文的Unicode正規化

諺文(韓文字; Hangul)的組合序列是例外,在Unicode資料庫裡沒有分解映射資訊,但標準分解仍需分解到韓文字母(jamo)層級。無論是韓文字母還是組好的諺文,組合類別都是 0。此分解、組合可以用Unicode編碼單純計算。


간 →(標準分解) U+1100 U+1161 U+11AB →(標準組合) U+AC04


複合母音、複合子音在韓文字母層級都視為無法再分解的字元,如 U+1101U+11AD 雖然實際上是雙子音構成,但在Unicode定義上視為無法再分解。

所有現代諺文的NFC都會是1個字元,NFD則可能是2~3個字元。但古諺文可能無法完全組合或是只能部分組合。

日文檔名

可能是基於減少檔名長度等歷史原因,Windows/UNIX環境一般以NFC形式儲存檔名;而macOS使用NFD形式儲存檔名。

故假名裡的濁音、半濁音,在macOS的檔名都會被分解。如 U+304C 在檔名會被分解成 U+304B U+3099◌゙

但使用網芳、FTP等檔案交換協定時,有時候無法正確處理此兩者Unicode等價。可能會造成相同檔名重複(一者為NFC,一者為NFD)的情形,進而造成檔案無法正確開啟、備分錯亂等問題。


此問題可能發生於任何其他NFC/NFD不一致的文字序列,只是在日文檔名最常發生,故描述於此。

台語拼音需要組合

無論台羅與白話字,第8聲調號在 Unicode 裡沒有任何預組字元,必須使用字母與 U+030D◌̍ 組合。

除了專為台語設計的字型之外,一般歐文字型不會預先造好第8聲字母,顯示時只能仰賴 GPOS 或定位組合。

但基於Unicode正規化安定性的原因,Unicode不太可能再收錄預組的第8聲母音字母。

白話字順序問題

例如 ó͘ ,白話字的 o͘ (o U+0358◌͘)是個母音字,上面再加調號時,邏輯上的順序應該是 o + U+0358◌͘ + U+0301◌́

然而根據組合類別,置於右上的 U+0358◌͘ 順序較後,所以NFD會是 o + U+0301◌́ + U+0358◌͘,而NFC是 ó (U+00F3ó) + U+0358◌͘

字型檔若要支援白話字GSUB最好完整支援這3種順序。

相容漢字

凡是統合漢字,本身就是正規化形式。無論兩個字之間是繁簡關係、異體關係,都不視為等價。


而相容漢字則非。因歷史上重複編碼之故,Unicode定義了相容漢字區塊,但相容漢字原則上只是為了跟傳統編碼保持互相映射,實際上不建議使用。

故相容漢字都是Singleton,正規化形式會變成統合漢字。

然而,部分字型檔可能基於語系或其它原因,在統合漢字與相容漢字設計成不同外形,造成諸多困擾。

U+FA12 的正規化形式是 U+6674,它們Unicode等價。所以在Unicode規定上,這兩者比較、排序時,都應該視為同一文字,理想上也應該是相同外形。只是部分字型可能基於歷史原因,將 U+FA12 右側設計為靑,造成用戶以為 U+FA12 專門用在傳統字形,與 U+6674 區分。但依照規定,此兩字應視為完全等價。


另外,相容漢字誤收了部分不應統合的部分文字,如 U+FA11 依照規則與 U+5D0E 並不等價。此類文字雖然留在相容漢字區塊,仍視為統合漢字,正規化形式仍為自己本身。

參考