Este é um assunto grande! É possível escrever um livro longo sobre ele, e várias pessoas já o fizeram (pesquise "unicode book" na Amazon para ver alguns exemplos).
Lidar com caracteres em computadores era muito mais simples em décadas anteriores, quando quem programava assumia que o inglês era a única língua importante. Então: 26 letras, maiúsculas e minúsculas, 10 dígitos, vários sinais de pontuação, mais um código (0x07) para tocar um sino, e tudo cabia em 7 bits: o conjunto de caracteres ASCII.
Naturalmente, as pessoas começaram a perguntar sobre à, ä e Ł, depois outras começaram a perguntar sobre ऄ, ஹ e ญ, e os jovens queriam emojis 😱. O que fazer?
Para encurtar a história, muitas pessoas inteligentes e pacientes tiveram que participar de comitês por anos, definindo os detalhes do conjunto de caracteres Unicode e de codificações como UTF-8, e muito software precisou de uma reescrita muito complicada. Além disso, muitos bugs novos surgiram.
Para evitar que tudo quebrasse, o projeto do Unicode/UTF-8 garante que os primeiros 127 códigos sejam idênticos ao ASCII (até mesmo o sino).
Linguagens criadas depois de 2005 têm a enorme vantagem de que já existia um padrão Unicode razoavelmente estável.
Julia (lançada em 2012) pôde assumir que tudo seria Unicode: caracteres, strings, nomes de variáveis e funções, operadores matemáticos...
Citando o manual, "Julia torna o tratamento de texto ASCII simples e eficiente, e lidar com Unicode é tão simples e eficiente quanto possível." Repare no "quanto possível", é uma parte importante da afirmação.
Literais de caractere são escritos entre aspas simples e são distintos de strings escritas entre aspas duplas.
Isso é óbvio para quem vem do mundo C/C++, mas pode confundir quem programa em Python e 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)
Podemos ver pelos exemplos que o tipo é Char, e Julia tem mais informações sobre a categoria do caractere.
Olhando mais de perto, parece que esses caracteres podem ser representados por 4 dígitos hexadecimais. O conjunto completo de caracteres precisa de até 6 dígitos hexadecimais.
Esses números são chamados de "pontos de código" e atualmente vão de U+0000 a U+10FFFF.
Eles aparecem no REPL, mas dentro do código use codepoints() para obtê-los.
Converter entre Char e Int é simples:
julia> Int('a')
97
julia> Char(97)
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
O compilador permite algumas formas de aritmética de inteiros com Chars:
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)
Um subconjunto das funções de manipulação de strings também funciona com entradas do tipo Char.
uppercase() e lowercase().isuppercase() e islowercase().isletter(), abrange muitos alfabetosisdigit(), testa estritamente 0:9isnumeric(), mais ampla que isdigit, então true para ¾ e vários alfabetos não europeusisxdigit(), dígitos hexadecimaisisascii(), caractere pré-Unicodeispunct(), pontuaçãoisspace(), qualquer caractere de espaço em brancoisprint(), caracteres imprimíveis (o oposto é iscntrl())islowercase('A') # false
uppercase('γ') # 'Γ': Unicode U+0393 (category Lu: Letter, uppercase)
ispunct('@') # true
isdigit('A') # false
isxdigit('A') # true
Para verificar se um caractere está presente em uma string, temos in.
Repare que isso é diferente de substrings:
# char search
julia> 'a' ∈ "xabcahliazlnkiw"
true
# substring search
julia> "a" ∈ "xabcahliazlnkiw"
use occursin(needle, haystack) for string containment
Além disso, as expressões regulares (assunto de outro conceito) permitem buscas e manipulações poderosas.
Para converter String em Char Vector, podemos usar collect().
Para converter Char Vector em String, existe o construtor 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"
Isso funciona com qualquer caractere, não apenas 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)
Repare que o construtor String() opera sobre um Vector.
Para converter um único Char em uma string de 1 caractere, a função é string(), com s minúsculo.
julia> string('a')
"a"
Tudo até aqui no documento parece relativamente simples, então será que realmente não há muito com que se preocupar?
Infelizmente, isso é otimista demais!
Uma complicação vem da necessidade de "até" 6 dígitos hexadecimais por ponto de código. Isso significa que caracteres diferentes precisam de quantidades diferentes de espaço na memória quando codificados em UTF-8.
Um byte só pode armazenar números (sem sinal) até 255, dois dígitos hexadecimais, então o UTF-8 usa um número variável de bytes (1 a 4) para armazenar um Char.
Eles são chamados de "unidades de código", e a função ncodeunits() retorna o número necessário para um determinado caractere.
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
Além disso, nem tudo que pode ser exibido na tela tem seu próprio ponto de código único. Alguns caracteres visualmente distintos são considerados derivados de outros, então o Unicode os trata como um caractere pai mais um modificador.