Це величезна тема! Про неї можна написати цілу книжку, і кілька людей уже це зробили (можна пошукати на Amazon «unicode book», щоб побачити приклади).
У попередні десятиліття працювати із символами в компʼютерах було набагато простіше: тоді програмісти вважали, що єдина важлива мова - англійська. Отже: 26 літер у верхньому й нижньому регістрі, 10 цифр, кілька розділових знаків, а ще код (0x07), від якого дзеленчав дзвіночок, - і все це вміщалося в 7 бітів: набір символів ASCII.
Звісно, люди почали питати: а як же à, ä та Ł? Потім інші почали питати про ऄ, ஹ та ญ, а молодь захотіла емодзі 😱. Що робити?
Якщо коротко, багатьом розумним і терплячим людям довелося роками працювати в комітетах, опрацьовуючи деталі набору символів Unicode і кодувань на кшталт UTF-8, а чимало програм довелося дуже складно переписувати. До того ж зʼявилося чимало нових багів.
Щоб не зламалося все, стандарт Unicode/UTF-8 гарантує, що перші 127 кодів ідентичні ASCII (навіть дзвіночок).
Мови, створені приблизно після 2005 року, мають величезну перевагу: досить стабільний стандарт Unicode уже існував.
Julia (її вперше випустили 2012 року) могла припускати, що все буде в Unicode: символи, рядки тексту (англ. string), назви змінних і функцій, математичні оператори...
Цитуючи посібник: «Julia робить роботу зі звичайним текстом ASCII простою та ефективною, а роботу з Unicode - настільки ж простою й ефективною, наскільки це можливо.» Звернімо увагу на «наскільки це можливо»: це важлива частина цього твердження.
Літерали символів записують в одинарних лапках, і вони відрізняються від рядків тексту, записаних у подвійних лапках.
Для людей зі світу 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() - символи з часів до Unicodeispunct() - розділові знакиisspace() - будь-який пробільний символisprint() - друковані символи (протилежна - iscntrl())islowercase('A') # false
uppercase('γ') # 'Γ': Unicode U+0393 (category Lu: Letter, uppercase)
ispunct('@') # true
isdigit('A') # false
isxdigit('A') # true
Щоб перевірити, чи є символ у рядку тексту, у нас є in.
Зауважмо, що це відрізняється від пошуку підрядків:
# char search
julia> 'a' ∈ "xabcahliazlnkiw"
true
# substring search
julia> "a" ∈ "xabcahliazlnkiw"
use occursin(needle, haystack) for string containment
Крім того, регулярні вирази (про них ідеться в іншій концепції) дають змогу потужно шукати й обробляти текст.
Char і рядком текстуЩоб перетворити рядок тексту на вектор 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() працює з вектором.
Щоб привести один 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 розглядає їх як базовий символ плюс модифікатор.