這是個_龐大_的主題! 光是它就能寫成一本厚書,而且真的有好幾個人這麼做了(在 Amazon 搜尋「unicode book」就能看到一些例子)。
幾十年前,電腦處理字元要簡單得多,當時的程式設計師假設英文是唯一重要的語言。 於是:26 個字母(含大小寫)、10 個數字、幾個標點符號,再加上一個讓鈴聲響起的碼(0x07),全部塞進 7 個位元裡:這就是 ASCII 字元集。
很自然地,有人開始問那 à、ä 和 Ł 怎麼辦,接著又有人問起 ऄ、ஹ 和 ญ,然後年輕人想要 emoji 😱。 該怎麼辦呢?
長話短說,許多聰明又有耐心的人得在委員會裡待上好幾年,把 Unicode 字元集以及 UTF-8 之類編碼的細節一一敲定,而大量軟體也需要_極_複雜的改寫。 同時,也引入了許多新的 bug。
為了避免_所有東西_都壞掉,Unicode/UTF-8 的設計確保前 127 個編碼與 ASCII 完全相同_(連鈴聲也是)_。
大約 2005 年之後才設計的語言有個很大的優勢:當時已經有還算穩定的 Unicode 標準了。
Julia(2012 年首次發布)可以直接假設一切都會是 Unicode:字元、字串、變數與函式名稱、數學運算子……
引用手冊的說法:「Julia 讓處理純 ASCII 文字變得簡單又有效率,而處理 Unicode 也盡可能地簡單又有效率。」 注意「as possible」這幾個字,它是這段話很重要的一部分。
字元常值是寫在單引號裡,和寫在雙引號裡的字串不同。
對來自 C/C++ 世界的人來說這很明顯,但對 Python 和 Javascript 的程式設計師來說可能很容易搞混。
julia> a = 'a' # Roman alphabet
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
julia> typeof(a)
Char
julia> jha = 'झ' # Devanagari alphabet
'झ': Unicode U+091D (category Lo: Letter, other)
julia> typeof(jha)
Char
julia> '❤' # heart emoji
'❤': Unicode U+2764 (category So: Symbol, other)
從這些例子可以看出型別是 Char,而且 Julia 還進一步提供了字元類別的資訊。
仔細看的話,這些字元似乎可以用 4 個十六進位數字表示。 完整的字元集則需要多達 6 個十六進位數字。
這些數字稱為「碼位」,目前的範圍從 U+0000 到 U+10FFFF。
它們會顯示在 REPL 中,但在程式碼裡要用 codepoints() 取得。
在Char和Int之間轉換很簡單:
julia> Int('a')
97
julia> Char(97)
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
編譯器允許對Char做_某些_形式的整數運算:
julia> 'b' - 'a' # interval, in alphabetic order
1
julia> 'b' + 'a'
ERROR: MethodError: no method matching +(::Char, ::Char)
julia> 'a' + 5
'f': ASCII/Unicode U+0066 (category Ll: Letter, lowercase)
julia> 'f' + ('A' - 'a') # same as `uppercase('f')`
'F': ASCII/Unicode U+0046 (category Lu: Letter, uppercase)
許多處理字串的函式也能用在Char輸入上。
uppercase()和lowercase()改變大小寫。isuppercase()和islowercase()測試大小寫。isletter()涵蓋許多字母系統isdigit(),只測試 0:9isnumeric(),比isdigit更廣,所以對 ¾ 和各種非歐洲文字都會得到true
isxdigit(),十六進位數字isascii(),Unicode 之前的字元ispunct(),標點符號isspace(),任何空白字元isprint(),可列印字元(相反的是iscntrl())islowercase('A') # false
uppercase('γ') # 'Γ': Unicode U+0393 (category Lu: Letter, uppercase)
ispunct('@') # true
isdigit('A') # false
isxdigit('A') # true
要從字串轉成 Char 向量,可以用collect()。
要從 Char 向量轉成字串,則有String()建構式。
julia> s = "abcde"
"abcde"
julia> cv = collect(s)
5-element Vector{Char}:
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
'd': ASCII/Unicode U+0064 (category Ll: Letter, lowercase)
'e': ASCII/Unicode U+0065 (category Ll: Letter, lowercase)
julia> String(cv)
"abcde"
這對任何字元都適用,不限於 ASCII。
julia> collect("❤,😱")
3-element Vector{Char}:
'❤': Unicode U+2764 (category So: Symbol, other)
',': ASCII/Unicode U+002C (category Po: Punctuation, other)
'😱': Unicode U+1F631 (category So: Symbol, other)
注意,String()建構式是作用在 Vector 上。
要把單一個Char_轉型_成單一字元的字串,該用的函式是string(),開頭是小寫的s。
julia> string('a')
"a"
到目前為止,這份文件裡的內容看起來都相對簡單,所以真的沒什麼好擔心的嗎?
很可惜,這想法太樂觀了!
其中一個麻煩來自每個碼位「最多」需要 6 個十六進位數字。 這代表以 UTF-8 編碼時,不同字元在記憶體中需要的大小也不同。
一個位元組只能儲存到 255 的(無號)數字,也就是兩個十六進位數字,所以 UTF-8 用可變的位元組數(1 到 4 個)來儲存一個Char。
這些單位稱為「碼元」,而ncodeunits()函式會回傳某個字元所需的數量。
julia> codepoint(jha) # jha 'झ' is defined in an earlier example
0x0000091d
julia> ncodeunits(jha)
3
julia> ncodeunits('a') # ASCII character
1
julia> ncodeunits('😱') # emoji
4
此外,並非所有能在螢幕上顯示的東西都有自己專屬的碼位。 有些外觀上彼此不同的字元被視為由其他字元衍生而來,因此 Unicode 把它們當成一個母字元加上一個修飾符。
這個問題會影響字串,為索引帶來挑戰。
在這道練習中,你將實作一組實用常式中的一部分,用來協助開發者清理識別字名稱。
在這 6 個任務中,你將逐步建構transform函式來轉換單一字元,以及clean函式來轉換字串。
合法的識別字包含零個或多個字母、底線、連字號、問號和表情符號。
如果傳遞空字串給clean函式,應該回傳空字串。
實作transform函式,將任何連字號取代為底線。
julia> transform('-')
"_"
移除所有空白字元。 這會包含開頭和結尾的空白。
julia> transform(' ')
""
修改transform函式,將 camelCase 轉換為 kebab-case
julia> transform('D')
"-d"
修改transform函式,略過任何數值字元。
julia> transform('7')
""
修改transform函式,取代介於 'α' 到 'ω' 範圍內的所有希臘字母。
julia> transform('β')
"?"
實作clean函式,將這些操作套用到整個字串。
不符合規則的字元應該原樣通過。
julia> clean(" a2b Cd-ω😀 ")
"ab-cd_?😀"