了不起的Unicode
前言
提出一個小小的問題。大家按照自己的開發語言的特性,想想結果是啥?
"????♂?"這個Emoji的長度是多少?
如果,現在你用電腦閱讀本文,你可以輕松的打開xx PlayGround(xx可以為Js/Java/Rust等)。然后會得到屬于自己語言的結果。
如果,你現在手頭沒電腦,無法親自驗證,我來直接告訴你答案。上述Emoji在每種語言環境下的結果都不統一。(當然,有些語言內核使用的機制一樣,結果可能也一樣)。
也就是說,在編程層面,這不是一種 「所見即所得」的表現形式。大家這里可能會納悶了,我要知道這個有啥?現在舉一個例子,在前端頁面中,我們總是會有統計用戶字數的輸入框,但是由于用戶輸入了Emoji,從用戶的角度來看,這就是一個字符,但是在編程層面,如果不做一次解析的話,我們會得到千奇百怪的答案。
然后,我們再來一個讓人匪夷所思的例子。在瀏覽器中,嘗試復制如下代碼,然后進行觀察答案。結果是不是又再一次顛覆你的所學。
"A?" === "?";
平時,我們時不時的會提到UTF-8/UTF-16/UTF-32它們到底是個啥?又有啥關系和區別呢?
還有其他的例子就不一一列舉了。之所以會出現這么多讓人匪夷所思的結果。一切的根源都是Unicode的鬧的。
所以,今天我們就來談談這是何方神圣。
在2000多年前,我們那迷人的老祖宗,秦始皇,就實現了「車同軌,書同文」,劃破「地域障礙」,從而給不同地方的人在交流上開辟了新的空間。雖然,有些地方還存在「十里不同音,百里不通俗」的情況(我老家山西就是這種情況)。但是,在官方層面或者書面層面上,大家可以溝通無阻。
好了,天不早了,干點正事哇。
我們能所學到的知識點
- 前置知識點
- Unicode 是個啥?
- UTF-8 又是什么?
- UTF-32 問題
- Unicode 病癥
- 如何檢測擴展形素簇
- "A?" !== "?" !== "?"
- Unicode 取決于區域設置
1. 前置知識點
「前置知識點」,只是做一個概念的介紹,不會做深度解釋。因為,這些概念在下面文章中會有出現,為了讓行文更加的順暢,所以將本該在文內的概念解釋放到前面來。「如果大家對這些概念熟悉,可以直接忽略」同時,由于閱讀我文章的群體有很多,所以有些知識點可能「我視之若珍寶,爾視只如草芥,棄之如敝履」。以下知識點,請「酌情使用」。
ASCll
ASCII[1](American Standard Code for Information Interchange)的縮寫,發音為ask-key。ASCII是一種用于表示字符的7位標準編碼,其中包括字母、數字和標點符號。
圖片
7 位編碼允許計算機編碼總共128個字符,包括數字 0-9、大寫和小寫字母 A-Z 以及一些標點符號。然而,這 128 位編碼僅適用于英語用戶。
ASCII 的功能
- ASCII的建立旨在實現各種數據處理設備之間的「兼容性」,從而使這些組件能夠成功地相互通信。
- ASCII使制造商能夠生產可以確保在計算機中正確運行的組件。
- ASCII使人機互動。
ASCII 在計算機系統中的工作原理
當我們按下鍵盤上的鍵,例如字母D時,電子信號被發送到計算機的CPU進行處理和存儲在內存中。「每個字符都被轉換為其對應的二進制形式」。計算機將字母處理為一個字節,實際上是一系列電子狀態的開和關。當計算機完成處理字節后,系統中安裝的軟件將字節轉換回,并在屏幕上顯示。字母 D 被轉換為01000100。
TextEncoder 和 TextDecoder
TextEncoder 和 TextDecoder 是 JavaScript 中用于處理字符編碼的「內置對象」。它們通常用于在不同字符編碼之間進行文本的編碼和解碼。
TextEncoder
- TextEncoder 是用于「將字符串文本編碼為字節數組」(通常是 UTF-8 編碼)的對象。
- 它提供了一個 encode() 方法,接受一個字符串作為參數,并返回一個包含字節的 Uint8Array 對象。
- TextEncoder 用于將文本數據轉換為字節數據,以便在網絡傳輸、文件讀寫或其他需要字節數據的情況下使用。
示例:
const encoder = new TextEncoder();
const text = "前端柒八九!";
const bytes = encoder.encode(text); // 將文本編碼為字節數組
TextDecoder
- TextDecoder 是用于將字節數組解碼為字符串文本的對象。
- 它提供了一個 decode() 方法,接受一個包含字節的 Uint8Array 對象,并返回相應的字符串。
- TextDecoder 用于將字節數據還原為文本,通常用于處理來自網絡請求或文件的字節數據。
示例:
const decoder = new TextDecoder("UTF-8");
const bytes = new Uint8Array([
72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33,
]);
const text = decoder.decode(bytes); // 將字節數組解碼為字符串
這些對象在處理「多語言文本」、「字符編碼轉換」和處理「國際化內容」時非常有用,使 JavaScript 能夠處理不同字符編碼之間的數據轉換。
Emoji
Emoji 是可以插入文字的圖形符號。
圖片
它是一個日語詞,e表示"絵",moji表示"文字"。連在一起,就是"絵文字"。
2010 年,Unicode 開始為 Emoji 分配碼點。也就是說,「現在的 Emoji 符號就是一個文字」,它會被渲染為圖形。
圖片
想了解更多,可以翻閱Emoji 簡介[2]
2. Unicode 是個啥?
Unicode是一個旨在統一所有人類語言(包括過去和現在的語言)并使它們與計算機兼容的標準。
Unicode 是一個將「不同字符分配給唯一編號的表格」。
例如:
- 拉丁字母 A 被分配編號 65。
- 阿拉伯字母 Seen ?是 1587。
- 片假名字母 Tu ツ 是 12484
- 音樂符號 G 調號 ?? 是 119070。
- ?? 是 128169。
Unicode 將這些編號稱為「碼位」(code points)。
由于這套準則是全球都認準的,所以我們采用這套規則,就可以達到「書同文」的情況,來自不同語言環境下的人,可以閱讀彼此的文本。
有如下的關系鏈子。 一個Unicode對應著一個字符,并且該字符擁有幾乎唯一的碼位。
Unicode === 字符 ? 碼位。
Unicode 有多大?
目前,「最大的已定義碼位」是0x10FFFF。(0x10FFFF 是一個十六進制數,將其轉換為十進制,其值為 1,114,111。)這給我們提供了大約 110 萬個碼位的空間。
目前已定義了約 15%(約 170,000 個),另外 11%(為私人使用)已被保留。其余約 800,000 個碼位目前尚未分配,它們可能在未來成為字符。
大致如下圖所示:
圖片
- 大正方形 包含 65,536 個字符。
- 小正方形 包含 256 個字符。
- 整個 ASCII 字符集僅占位于左上角的小紅色正方形的一半。
私人使用區(Private Use)
私人使用區是為應用程序開發人員保留的碼位,不會由 Unicode 本身定義。
例如,Unicode 中沒有為蘋果標志保留位置,因此蘋果將它放在了 U+F8FF,這位于私人使用區。在任何其他字體中,它將呈現為缺失的字符 ??,但在與 macOS 一起提供的字體中,我們將看到蘋果圖標。
私人使用區主要用于「圖標字體」:
上面的圖標都是文本格式
U+1F4A9 是什么意思?
這是一種寫碼位值的約定。前綴 U+表示 Unicode,而 1F4A9 是一個「十六進制的碼位編號」。
U+1F4A9 具體表示的是 ??。(是不是我們多了一種很委婉的"表揚別人"方式)
3. UTF-8 又是什么?
UTF-8 是一種「編碼方式」。
編碼是我們將碼位存儲在內存中的方法。在互聯網和許多操作系統中,UTF-8是「默認的文本編碼」。
最簡單的 Unicode 編碼是 UTF-32。它將碼位簡單地「存儲為 32 位整數」。因此,U+1F4A9 變成了 00 01 F4 A9,占用了「四個字節」。UTF-32 中的「任何其他碼位也將占用四個字節」。由于最高定義的碼位是 U+10FFFF,因此任何碼位都能夠容納。
- UTF-8通常用于存儲和傳輸文本
- UTF-16用于某些操作系統和編程語言
- UTF-16被許多系統采用。其中包括 Microsoft Windows、Objective-C、Java、JavaScript、.NET、Python 2等
- UTF-32適用于需要直接操作Unicode代碼點的情況
UTF-8 有多少字節?
UTF-8 是一種「可變長度」的編碼方式。
一個碼位可能被編碼為「一個到四個字節」的序列。
以下是 UTF-8 編碼的表示形式,「根據不同的碼位范圍使用不同數量的字節」
碼位范圍 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
U+0000..007F | 0xxxxxxx | |||
U+0080..07FF | 110xxxxx | 10xxxxxx | ||
U+0800..FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | |
U+10000..10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
這些規則描述了如何將不同碼位范圍內的 Unicode 字符編碼為 UTF-8 字節序列。
如果將這些內容與 Unicode 表結合起來,我們將看到
- 英語使用 1 個字節進行編碼,
- 西里爾字母、拉丁歐洲語言、希伯來語和阿拉伯語需要 2 個字節,
- 中文、日語、韓語、其他亞洲語言和表情符號需要 3 或 4 個字節。
以下是一些重要的要點:
首先,UTF-8 與 ASCII 是「字節兼容」的。碼位 0..127,即舊的 ASCII 字符,使用一個字節進行編碼,而且它們的字節表示完全相同。例如,U+0041(A,拉丁大寫字母 A)就是 41,一個字節。
任何純 ASCII 文本也是有效的 UTF-8 文本,而且「只使用碼位 0..127 的 UTF-8 文本可以直接讀取為 ASCII」。
其次,UTF-8 對于基本拉丁字符來說是「空間高效」的。
- 對于像 HTML 標簽或 JSON 這樣的技術字符串來說,這是有意義的。
第三,UTF-8 內置了「錯誤檢測」和「恢復功能」。
- 第一個字節的前綴總是與第 2 到第 4 個字節不同。這樣,我們始終可以確定是否正在查看完整和有效的 UTF-8 字節序列,或者是否有遺漏。
- 然后,我們可以通過向前或向后移動,直到找到正確序列的開頭來進行糾正。
還有一些重要的結論:
- 我們「無法通過計算字節來確定字符串的長度」。
- 我們「無法隨機跳到字符串的中間并開始閱讀」。
- 我們無法通過在任意字節偏移處進行「切割來獲取子字符串」,可能會切斷字符的一部分。
如果硬要這么做的話,系統會給你一個?。
“?”是什么?
U+FFFD,即「替換字符」(Replacement Character),只是 Unicode 表中的另一個碼位。應用程序和庫可以在檢測到 Unicode 錯誤時使用它。
如果將碼位的一半切掉,那么另一半也就沒什么用了,除了顯示錯誤。這時就會使用?。
JS 版本
const text = "前端柒八九";
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
const partial = bytes.slice(0, 11);
const decoder = new TextDecoder("UTF-8");
const result = decoder.decode(partial);
console.log(result); // 輸出 "前端柒?"
Rust 版本
fn main() {
let text = "前端柒八九";
let bytes = text.as_bytes();
let partial = &bytes[0..11];
let result = String::from_utf8_lossy(partial);
println!("{}", result); // 輸出 "前端柒?"
}
在 JavaScript 中使用 TextEncoder 和 TextDecoder 來處理編碼,而在 Rust 中使用 String::from_utf8_lossy 來處理字節。它們的目標是在 UTF-8 編碼中處理文本并「截取部分字節」。
4. UTF-32 問題
UTF-32 非常適用于處理碼位。它的編碼方式中,「每個碼位始終是 4 個字節」,那么strlen(s) == sizeof(s) / 4,substring(0, 3) == bytes[0, 12](上面代碼為偽代碼)等等。
問題在于,我們不想處理碼位。一個碼位即「不是一個書寫單位」,又并「不總是代表一個字符」。我們應該處理的是擴展形素簇(extended grapheme clusters),或簡稱為形素(graphemes)。
形素是在特定書寫系統的上下文中的「最小可區分」的書寫單位。
例如,? 是一個形素,e?也是一個形素。還有像?這樣的形素。基本上,「形素是用戶認為是一個字符的單元」。
問題是,在 Unicode 中,一些形素是由「多個碼位編碼」的!
圖片
例如,e?(一個單一的形素)在 Unicode 中編碼為 e(U+0065 拉丁小寫字母 E)+ ′(U+0301 連接重音符)。兩個碼位!
它也可能不止兩個:
- ?? 是 U+2639 + U+FE0F
- ???? 是 U+1F468 + U+200D + U+1F3ED
- ????♀? 是 U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F
- y?????????? 是 U+0079 + U+0316 + U+0320 + U+034D + U+0318 + U+0347 + U+0357 + U+030F + U+033D + U+030E + U+035E
即使在最寬的編碼 UTF-32 中,???? 仍需要「三個 4 字節單元」來進行編碼。它仍然需要被「視為一個單獨的字符」。
我們可以將 Unicode 本身(沒有任何編碼)視為「可變長度」的。
擴展形素簇(Extended Grapheme Cluster)是「一個或多個 Unicode 碼位的序列」,必須將其視為「一個單獨的、不可分割的字符。
因此,在「碼位級別」上:「不能只取序列的一部分,它總是應該作為一個整體選擇、復制、編輯或刪除」。
不正確使用形素簇會導致像這樣的錯誤:
無論是否選擇UTF-32還是UTF-8在處理形素上遇到相似的問題。所以如何使用形素才是我們應該關心的。
5. Unicode 病癥
上面的例子中大部分都是涉及到表情符號,這會給人一種錯覺。Unicode只有在表示表情符號時,會遇到問題。--其實不是。
擴展形素簇也用于常見的語言。
例如:
- ?(德語)是一個單一字符,但包含多個碼位(U+006F U+0308)。
- ??(立陶宛語)是 U+00E1 U+0328。
- ?(韓語)是 U+1100 U+1161 U+11A8。
所以,問題不僅僅是表情符號。
"????♂?".length 是多少?
不同的編程語言給出了不同的結果。
Python 3:
>>> len("????♂?")
5
JavaScript / Java / C#:
>> "????♂?".length
7
Rust:
println!("{}", "????♂?".len());
// => 17
不同的語言使用不同的「內部字符串」表示(UTF-32、UTF-16、UTF-8),并以存儲字符的單位(整數、短整數、字節)來報告長度。
但是!如果你問任何不懂編程理論的人,他們會給你一個明確的答案:????♂? 字符串的長度是 1。
這就是擴展形素簇的意義:「人們視為單一字符的內容」。在這種情況下,????♂? 顯然是一個單一字符。
????♂? 由 5 個碼位組成(U+1F926 U+1F3FB U+200D U+2642 U+FE0F)僅僅是「實現細節」。它不應該被分開,「不應該被計為多個字符」,文本光標不應該定位在其中,不應該被部分選擇,等等。
這是「文本的一個不可分割的單位」。在內部,它可以被編碼為任何形式,但對于面向用戶的 API,應該將其視為一個整體。
唯一正確處理此問題的現代語言是 Swift:
print("????♂?".count)
// => 1
而對于我們比較熟悉的JS和Rust,我們可以使用一些方式做一下封裝。
function visibleLength(str) {
return [...new Intl.Segmenter().segment(str)].length;
}
visibleLength("????♂?"); // 輸出結果為1
當然,我們還可以校驗其他的形素。
visibleLength("?"); // => 1
visibleLength("????"); // => 1
visibleLength("????????????"); // => 2
visibleLength("と日本語の文章"); // => 7
但是呢,Intl.Segmenter的兼容性不是很好。
如果,我們要實現多瀏覽器適配,我們可以找一些第三方的庫。
- graphemer[3]
- text-segmentation[4]
如果想了解更多細節,可以參考JS 如何正確處理 Unicode[5]
對于Rust我們可以使用unicode_segmentation[6]crate。
extern crate unicode_segmentation; // "1.9.0"
use std::collections::HashSet;
use unicode_segmentation::UnicodeSegmentation;
fn count_unique_grapheme_clusters(s: &str) -> usize {
let is_extended = true;
s.graphemes(is_extended).collect::<HashSet<_>>().len()
}
fn main() {
assert_eq!(count_unique_grapheme_clusters(""), 0);
assert_eq!(count_unique_grapheme_clusters("????♂?"), 1);
assert_eq!(count_unique_grapheme_clusters("????"), 1);
}
6. 如何檢測擴展形素簇
大多數編程語言選擇了簡單的方式,允許我們迭代字符串時使用 1-2-4 字節的塊,但「不支持直接處理擴展形素簇」。
由于它是默認方式,結果我們看到了損壞的字符串:
圖片
如果遇到這種問題,我們首先的就是應該想到使用Unicode 庫。
使用庫
即使是像 strlen、indexOf 或 substring 這樣的基本操作也應該使用 Unicode 庫!
例如:
- C/C++/Java:使用 ICU[7]。這是 Unicode 自身發布的庫,包含了關于文本分割的所有規則。
- Swift:只需使用標準庫。Swift 默認情況下會正確處理。
- Javascript的話,我們上面提到過,可以使用瀏覽器內置功能Intl.Segmenter或者graphemer/text-segmentation
- Rust而言,我們可以使用unicode_segmentation
不管選擇哪種方式,確保它使用的是「新版本」的 Unicode,因為形素的定義會隨版本而變化。
Unicode 規則更新
從大約 2014 年開始,Unicode 每年都會發布其標準的重大修訂版本。
每年更新
圖片
隨之而來的不良反映就是,定義形素簇的規則每年也會發生變化。今天被認為是由兩個或三個獨立碼位組成的序列,明天可能會成為一個形素簇!這種朝令夕改的做法,很是讓人深惡痛絕。
更糟糕的是,我們自己的應用程序的不同版本可能運行在不同的 Unicode 標準上,并報告不同的字符串長度!
7. "A?" !== "?" !== "?"
將其中任何一個復制到你的 JavaScript 控制臺:
"A?" === "?";
"?" === "?";
"A?" === "?";
你會得到讓你匪夷所思的答案。沒錯,它們的打印結果都是false。
還記得之前的,? 是由兩個碼位組成,U+006F U+0308 。基本上,Unicode 提供了「多種」編寫字符如 ? 或 ? 的方式。
- 通過將普通的拉丁字母 A 與一個組合字符組合成 ?,
- 或者使用已經預先組合的碼位 U+00C5。
因為,它們「看起來是相同」的(A? 與 ?),所以從用戶的角度,我們就「認為它們應該是相同」的,但結果卻和我們的想法大相徑庭。
這就是為什么我們需要規范化。有四種形式:
這里先從NFD和NFC介紹。
- NFD(Normalization Form C) 嘗試將一切都分解為最小可能的部分,并如果存在多個部分,則按照規范順序對這些部分進行排序。
它消除任何規范化差異,并生成一個「分解的結果」
- NFC(Normalization Form C),嘗試將一切組合成已經預先組合的形式(如果存在)
它消除任何規范化差異,通常生成一個「合成的結果」
不同的形式用于不同的用例,以確保文本在不同的方式下都保持一致。所以,盡管"A?" !== "?" !== "?",但通過適當的規范化,我們可以使它們等同。
圖片
對于某些字符,Unicode 中還存在多個版本。例如,有 U+00C5 帶有上面環圈的拉丁大寫字母 A,但還有外觀相同的 U+212B ?ngstr?m 符號。
這些字符在規范化過程中也會被替換,以確保它們的一致性。
圖片
NFD 和 NFC 被稱為“規范化規范”(canonical normalization)。另外兩種形式是“兼容規范化”(compatibility normalization):
- NFKD 試圖將「所有內容分解」,并使用默認形式替換視覺變體。
它消除規范化和兼容性差異,并生成一個分解的結果
- NFKC 試圖將「所有內容組合」在一起,同時用默認形式替換視覺變體。
它消除規范化和兼容性差異,并通常生成一個合成的結果
圖片
視覺變體是表示相同字符的獨立 Unicode 碼位,但它們應該呈現不同的方式。比如,①、? 或 ??。
圖片
所有這些字符都有自己的碼位,但它們也都是Xs。
在比較字符串或搜索子字符串之前,進行規范化!
`Unicode`規范化[8]傳送 ??
在JavaScript 中,我們可以使用 normalize() 方法來實現 NFC(Normalization Form C)和 NFD(Normalization Form D)。
const str1 = "A?";
const str2 = "?";
const normalizedStr1 = str1.normalize("NFC"); // NFC 形式
const normalizedStr2 = str2.normalize("NFC"); // NFC 形式
console.log(normalizedStr1 === normalizedStr2); // true
上述代碼首先使用 normalize('NFC') 方法將兩個字符串都轉換為 NFC 形式,然后比較它們是否相等。這將使 "A?" 和 "?" 的比較結果為 true。
如果使用 NFD 形式,只需將 normalize('NFC') 更改為 normalize('NFD') 即可。
8. Unicode 取決于區域設置
俄羅斯名字「尼古拉」
圖片
在Unicode 中編碼為 U+041D 0438 043A 043E 043B 0430 0439。
保加利亞名字「尼古拉」
圖片
也寫成 U+041D 0438 043A 043E 043B 0430 0439。
它們的Unicode值完全一樣,但是所顯示的字體信息卻不盡相同。是不是有種小腦萎縮的感覺。
然后心中有一個 ??,計算機如何知道何時呈現保加利亞風格的字形,何時使用俄羅斯的字形?
其實,計算機也不知。Unicode 并不是一個完美的系統,它有很多不足之處。其中一個問題是「將本應呈現不同外觀的字形分配給相同的碼位」,比如西里爾字母的小寫字母 K 和保加利亞的小寫字母 K(都是 U+043A)。
針對一些表音語言這塊還能好點,但是到了我們大亞洲,很多國家的文字都是「表意」的。許多漢字、日語和韓語表意字形的寫法都截然不同,但被分配了相同的碼位。
圖片
Unicode 的動機是為了「節省碼位空間」。渲染信息應該在字符串外部以區域設置/語言元數據的方式傳遞。
在實踐中,依賴于區域設置帶來了許多問題:
- 作為元數據,區域設置通常會丟失。
- 人們不限于使用「單一區域設置」。例如,我們可以閱讀和寫作中文,美國英語、英國英語、德語和俄語。
- 難以混合和匹配。比如在保加利亞文本中使用俄羅斯名字,反之亦然。
- 沒有地方可以指定區域設置。即使制作上面的兩個屏幕截圖也不容易,因為在大多數軟件中,沒有下拉菜單或文本輸入來更改區域設置。
9. 處理特殊語言
另一個不幸的例子是土耳其語中無點 i 的 Unicode 處理。
與英語不同,土耳其語有兩種 I 變體:有點和無點。
Unicode 決定重用 ASCII 中的 I 和 i,并只添加了兩個新的碼位:? 和 ?。
這導致了在相同輸入上 toLowerCase/toUpperCase 表現不同:
var en_US = Locale.of("en", "US");
var tr = Locale.of("tr");
System.out.println("I".toLowerCase(en_US)); // => "i"
System.out.println("I".toLowerCase(tr)); // => "?"
System.out.println("i".toUpperCase(en_US)); // => "I"
System.out.println("i".toUpperCase(tr)); // => "?"
所以,我們在不知道字符串是用哪種語言編寫的情況下將字符串轉換為小寫,會出現問題。
如果我們項目中涉及到土耳其語的字符轉換,在 JS 中toLowerCase是達不到上面的要求的。因為,在JavaScript中,toLowerCase方法默認使用Unicode規范進行轉換,根據Unicode的規范,大寫 I 被轉換為小寫 i,而不是 ?。這是因為JavaScript的toLowerCase方法按照Unicode的標準工作。
要想使用JS正確處理上面的問題,我們就需要額外的 API.
"I".toLocaleLowerCase("tr-TR"); // => "?"
"i".toLocaleUpperCase("tr-TR"); // => "?"
我們也可以通過對String.prototype上做一層封裝。
String.prototype.turkishToUpper = function () {
var string = this;
var letters = { i: "?", ?: "?", ?: "?", ü: "ü", ?: "?", ?: "?", ?: "I" };
string = string.replace(/(([i???ü??]))+/g, function (letter) {
return letters[letter];
});
return string.toUpperCase();
};
String.prototype.turkishToLower = function () {
var string = this;
var letters = { ?: "i", I: "?", ?: "?", ?: "?", ü: "ü", ?: "?", ?: "?" };
string = string.replace(/(([?I??ü??]))+/g, function (letter) {
return letters[letter];
});
return string.toLowerCase();
};
// 代碼演示
"D?N?".turkishToLower(); // => din?
"DIN?".turkishToLower(); // => d?n?
這樣就可以正確規避JS針對土耳其語言中的準換問題。
在Rust中,我們可以使用如下代碼:
fn turkish_to_upper(input: &str) -> String {
let letters = [
('i', "?"),
('?', "?"),
('?', "?"),
('ü', "ü"),
('?', "?"),
('?', "?"),
('?', "I"),
];
let mut result = String::new();
for c in input.chars() {
let mut found = false;
for &(source, target) in &letters {
if c == source {
result.push_str(target);
found = true;
break;
}
}
if !found {
result.push(c);
}
}
result.to_uppercase()
}
fn turkish_to_lower(input: &str) -> String {
let letters = [
('?', "i"),
('I', "?"),
('?', "?"),
('?', "?"),
('ü', "ü"),
('?', "?"),
('?', "?"),
];
let mut result = String::new();
for c in input.chars() {
let mut found = false;
for &(source, target) in &letters {
if c == source {
result.push_str(target);
found = true;
break;
}
}
if !found {
result.push(c);
}
}
result.to_lowercase()
}
fn main() {
let input = "???ü???";
let upper_result = turkish_to_upper(input);
let lower_result = turkish_to_lower(input);
println!("Upper: {}", upper_result); //Upper: ???ü??I
println!("Lower: {}", lower_result); // Lower: i??ü???
}
Reference
[1]ASCII:https://cikgucandoit.wordpress.com/what-is-ascll/
[2]Emoji 簡介:https://www.ruanyifeng.com/blog/2017/04/emoji.html
[3]graphemer:https://github.com/flmnt/graphemer
[4]text-segmentation:https://github.com/niklasvh/text-segmentation
[5]JS 如何正確處理 Unicode:https://flaviocopes.com/javascript-unicode/
[6]unicode_segmentation:https://docs.rs/unicode-segmentation/latest/unicode_segmentation/
[7]ICU:https://github.com/unicode-org/icu
[8]Unicode規范化:https://www.unicode.org/glossary/