Uploaded avatar of PercyGrunwald

Il matching Unicode in Elixir

@PercyGrunwald
Oltre 7 anni fa

Gli esercizi su Exercism sono piccoli, sintetici e spesso apparentemente banali. È facile immaginare che i professionisti esperti non abbiano nulla da imparare da loro. Tuttavia, risolvere questi problemi artificiali può spingerti a imparare e applicare parti del tuo linguaggio che potresti non aver mai esplorato. Queste nuove conoscenze possono portarti a risolvere problemi del mondo reale in modo più efficiente o più espressivo.

Parallel Letter Frequency è un esercizio di difficoltà media del track Elixir di Exercism che svela un numero sorprendente di lezioni interessanti. Una delle sfide centrali nel risolvere l'esercizio è gestire lettere provenienti da più lingue, dato che uno dei casi di test è in tedesco e contiene caratteri al di fuori dell'alfabeto inglese. Se passi la maggior parte del tuo tempo a sviluppare applicazioni per chi parla inglese, questa potrebbe essere la prima volta che devi affrontare un requisito del genere. Gli insegnamenti di questo esercizio offrono vantaggi evidenti a chi scrive applicazioni multilingue o in lingue diverse dall'inglese, ma possono essere utili anche in molti altri ambiti, come validazioni più robuste di nomi utente e password.

Per risolvere l'esercizio con successo, devi implementare una funzione, Frequency.frequency/2 , che determina la frequenza delle lettere in una lista di stringhe che potrebbero essere in qualsiasi lingua:

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

Partiamo dal problema fondamentale che questa funzione deve risolvere e arriviamo passo dopo passo a un'implementazione completa.

Stabilire se un carattere è una lettera in Elixir

Come useresti Elixir per stabilire se "a" è una lettera oppure no?

Credo che la maggior parte delle persone userebbe un'espressione regolare come /[a-z]/:

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

E che dire di "A"?

Aggiungere il modificatore i (caseless) sarebbe probabilmente il modo più semplice:

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

Ok, e adesso che dire di "ö"?

Quando ho affrontato questo problema per la prima volta, non ero sicuro di quale fosse il modo migliore: /[a-z]/i di sicuro non funzionerà:

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

Stabilire se "ö" è una lettera è una parte centrale per risolvere questo problema di Exercism, dato che uno dei testi nei test è in tedesco:

# 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
...
"""

Forse potresti usare un'espressione regolare per verificare che un carattere non sia un carattere speciale, ma probabilmente verrebbe lunga, inelegante e fragile. Quanto puoi essere sicuro di aver coperto ogni possibile carattere speciale che potrebbe essere passato come input alla tua funzione? Credo che ci sia un approccio migliore.

Le espressioni regolari Unicode in Elixir

Un approccio migliore a questo problema è usare il modificatore u nel modulo Regex di Elixir:

unicode (u): abilita pattern specifici di Unicode come \p e modifica modificatori come \w, \W, \s e simili in modo che corrispondano anch'essi a Unicode.

Si scopre che il modificatore u, e in particolare il pattern \p, è una soluzione davvero elegante. Il pattern \p ti permette di far corrispondere un grafema (un altro nome per un singolo carattere Unicode) a una qualsiasi delle categorie di caratteri Unicode. Questo include non solo categorie specifiche come Ll (Letter, lowercase) e Sc (Symbol, currency), ma anche le categorie padre come L (Letter) e S (Symbol).

Puoi far corrispondere qualsiasi lettera di qualsiasi caso in qualsiasi lingua umana coperta da Unicode con il pattern \p{L}. Questo permette corrispondenze piuttosto potenti.

I caratteri latini di base dell'inglese funzionano come al solito:

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

Anche le varianti dei caratteri latini con umlaut e accenti acuti non sono un problema:

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

Assicuriamoci che non restituisca una corrispondenza per qualsiasi carattere. Che ne dici di alcuni caratteri che sembrano lettere ma non lo sono:

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

Molto bene, ma ricordi quando ho detto qualsiasi lingua? Nessun 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

Applicare la corrispondenza Unicode al problema in questione

Ora che abbiamo uno strumento che ci aiuta a stabilire se un grafema è una lettera oppure no, possiamo applicarlo per risolvere il problema. Una prima implementazione della funzione Frequency.frequency/2 potrebbe essere questa:

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

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

Tutto ciò che count_letters/1 deve fare è applicare il pattern String.match?(grapheme, ~r/^\p{L}$/u) che abbiamo individuato sopra per incrementare il conteggio di ogni lettera nella lista di graphemes. Ecco un esempio di implementazione tratto dalla mia soluzione a questo problema di 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

Questa funzione accetta una lista di grafemi, ad esempio ["a", "A", "ö", "$"], e restituisce una mappa che conta solo le lettere ignorando il caso: %{"a" => 2, "ö" => 1}. Considerando che questa funzione può gestire input da qualsiasi lingua, direi che sono 9 righe di codice piuttosto potenti.

Conclusione

Si scopre che far corrispondere lettere non inglesi diventa piuttosto semplice quando conosci la corrispondenza Unicode e, per fortuna, è una funzionalità fondamentale del modulo Regex di Elixir. Prima di risolvere questo problema di Exercism conoscevo a malapena questa funzionalità, ma adesso la considererei una parte indispensabile del mio bagaglio di strumenti per Elixir.

Potresti usare questo nuovo strumento in molti modi; alcuni che mi vengono in mente sono una validazione più robusta di password e nomi utente, o persino per stabilire se una stringa di input è una stringa di valuta valida senza dover elencare manualmente tutti i possibili simboli di valuta:

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: it.number.nth.ordinalized Mar 2019 · Ti è stato utile?