Uploaded avatar of PercyGrunwald

Correspondência de Unicode em Elixir

@PercyGrunwald
há Mais de 7 anos

Exercism's translation worker: here is the European Portuguese translation.

Os exercícios do Exercism são pequenos, sintéticos e muitas vezes aparentemente triviais. É fácil imaginar que quem já tem muita experiência não teria nada a aprender com eles. No entanto, resolver estes problemas artificiais pode levar-te a aprender e a aplicar partes da tua linguagem que talvez nunca tenhas explorado. Estas novas aprendizagens podem ajudar-te a resolver problemas do mundo real de forma mais eficiente ou mais expressiva.

A Frequência de Letras em Paralelo é um exercício de dificuldade média do percurso de Elixir do Exercism que desvenda um número surpreendente de lições interessantes. Um dos principais desafios deste exercício é lidar com letras de várias línguas, pois um dos casos de teste está em alemão e contém carateres que não fazem parte do alfabeto inglês. Se passas a maior parte do tempo a desenvolver aplicações para falantes de inglês, esta pode ser a primeira vez que tens de lidar com um requisito destes. O que aprendes com este exercício é claramente útil para quem escreve aplicações multilingues ou que não sejam em inglês, mas também pode ajudar em muitas outras áreas, como validações mais robustas de nomes de utilizador e palavras-passe.

Para resolver o exercício com sucesso, tens de implementar uma função, Frequency.frequency/2, que determina a frequência de letras numa lista de strings que podem estar em qualquer língua:

iex> Frequency.frequency(["Freude", "schöner", "Götterfunken"], workers)
%{
  "c" => 1, 
  "d" => 1, 
  "e" => 5, 
  ...
  "ö" => 2
}

Vamos começar pelo problema fundamental que esta função tem de resolver e avançar até uma implementação completa.

Determinar se um caráter é uma letra em Elixir

Como usarias o Elixir para determinar se "a" é uma letra?

Acho que a maioria das pessoas aplicaria uma expressão regular como /[a-z]/:

iex> String.match?("a", ~r/^[a-z]$/)
true

E quanto a "A"?

Acrescentar o modificador i (que ignora a distinção entre maiúsculas e minúsculas) seria provavelmente a forma mais simples:

iex> String.match?("A", ~r/^[a-z]$/i)
true

Ok, e agora quanto a "ö"?

Quando abordei este problema pela primeira vez, não tinha a certeza de qual seria a melhor forma. /[a-z]/i de certeza que não funciona:

iex> String.match?("ö", ~r/^[a-z]$/i)
false

Determinar se "ö" é uma letra é uma parte central da resolução deste problema do Exercism, pois um dos textos dos testes está em alemão:

# parallel_letter_frequency_test.exs
...
# Poem by Friedrich Schiller. The corresponding music is the European Anthem.
@ode_an_die_freude """
Freude schöner Götterfunken
...
"""

Talvez possas usar uma expressão regular para verificar se um caráter não é um caráter especial, mas isso seria provavelmente longo, pouco elegante e frágil. Quanta confiança podes ter de que cobriste todos os carateres especiais possíveis que podem ser passados como valores de entrada à tua função? Acredito que há uma abordagem melhor.

Expressões regulares Unicode em Elixir

Uma abordagem melhor para este problema é usar o modificador uno móduloRegex` do Elixir:

unicode (u) - ativa padrões específicos de Unicode como \p e faz com que modificadores como \w, \W, \s e afins também correspondam a Unicode.

Acontece que o modificador u, e em particular o padrão \p, é uma solução bastante elegante. O padrão \p permite-te corresponder a um grafema (outro nome para um único caráter Unicode) em qualquer uma das categorias de carateres Unicode. Isto inclui não só categorias específicas como Ll (letra, minúscula) e Sc (símbolo, moeda), mas também as categorias principais como L (letra) e S (símbolo).

Podes corresponder a qualquer letra, em qualquer combinação de maiúsculas e minúsculas, de qualquer língua humana abrangida pelo Unicode com o padrão \p{L}. Isto permite correspondências bastante poderosas.

Os carateres latinos básicos do inglês funcionam como habitualmente:

iex> String.match?("a", ~r/^\p{L}$/u)
true
iex> String.match?("A", ~r/^\p{L}$/u)
true

As variantes de carateres latinos com trema e acentos agudos também não são problema:

iex> String.match?("ö", ~r/^\p{L}$/u)
true
iex> String.match?("Á", ~r/^\p{L}$/u)
true

Vamos certificar-nos de que não está apenas a devolver uma correspondência para qualquer caráter. Que tal alguns carateres que parecem letras mas não são:

iex> String.match?("$", ~r/^\p{L}$/u)
false
iex> String.match?("@", ~r/^\p{L}$/u)
false

Muito bom, mas lembras-te de eu ter dito qualquer língua? Sem problema:

# Chinese character for "you"
iex> String.match?("你", ~r/^\p{L}$/u)
true

# Cyrillic capital letter "zhe"
iex> String.match?("Ж", ~r/^\p{L}$/u)
true

Aplicar a correspondência Unicode ao problema em questão

Agora que temos uma ferramenta que nos ajuda a determinar se um grafema é uma letra, podemos aplicá-la para resolver o problema. Uma primeira implementação da função Frequency.frequency/2 poderia ser assim:

def frequency(texts, _workers) do
  texts
  |> get_all_graphemes()
  |> count_letters()
end

defp get_all_graphemes(texts) do
  texts
  |> Enum.join()
  |> String.graphemes()
end

Tudo o que count_letters/1 teria de fazer é aplicar o padrão String.match?(grapheme, ~r/^\p{L}$/u) que identificámos acima para incrementar a contagem de cada letra na lista de graphemes. Aqui está uma implementação de exemplo retirada da minha solução para este problema do Exercism:

defp count_letters(graphemes) do
  Enum.reduce(graphemes, %{}, fn grapheme, acc ->
    if String.match?(grapheme, ~r/^\p{L}$/u) do
      downcased_letter = String.downcase(grapheme)
      Map.update(acc, downcased_letter, 1, fn count -> count + 1 end)
    else
      acc
    end
  end)
end

Esta função recebe uma lista de grafemas, por exemplo ["a", "A", "ö", "$"], e devolve um mapa que conta apenas as letras, ignorando a distinção entre maiúsculas e minúsculas: %{"a" => 2, "ö" => 1}. Tendo em conta que esta função consegue lidar com dados de qualquer língua, diria que são 9 linhas de código bastante poderosas.

Conclusão

Acontece que corresponder a letras que não são inglesas se torna bastante simples quando conheces a correspondência Unicode e, felizmente para nós, é uma funcionalidade central do módulo Regex do Elixir. Antes de resolver este problema do Exercism, mal conhecia esta funcionalidade, mas agora considero-a uma parte indispensável da minha caixa de ferramentas de Elixir.

Podes usar esta nova ferramenta de muitas formas; algumas que me vêm à cabeça são uma validação mais robusta de palavras-passe e nomes de utilizador, ou até para determinar se uma determinada string é uma string de moeda válida sem teres de listar manualmente todos os símbolos de moeda possíveis:

iex> currency_string_regex = ~r/\p{Sc}\d+\.\d{2}/u
~r/\p{Sc}\d+\.\d{2}/u

iex> ["$1.00", "£1.00", "¥1.00", "€1.00", "&1.00"] \
...> |> Enum.filter(&String.match?(&1, currency_string_regex))
["$1.00", "£1.00", "¥1.00", "€1.00"]
Translation missing: pt-PT.number.nth.ordinalized Mar 2019 · Foi útil?