Current section
Files
Jump to
Current section
Files
lib/soundex.ex
defmodule Soundex do
@moduledoc """
Compute Soundex codes for strings.
Soundex is an algorithm for representing (mainly English) names as short phonetic codes.
A Soundex code begins with the first letter of the name, followed by three digits.
They are typically used to match like-sounding names.
For more information, see [the Wikipedia entry](http://en.wikipedia.org/wiki/soundex).
## Examples:
iex> Soundex.soundex("Morris")
"M620"
iex> Soundex.soundex("Harris")
"H620"
iex> Soundex.soundex("Morrison")
"M625"
iex> Soundex.soundex("Smith")
"S530"
iex> Soundex.soundex("Smithie")
"S530"
"""
require Logger
@doc """
Compute the Soundex code of a string.
For details, see (http://en.wikipedia.org/wiki/soundex)[the Wikipedia entry].
The Soundex algorithm is only defined for strings with ASCII characters.
Returns a string.
## Examples:
iex> Soundex.soundex("Jackson")
"J250"
"""
def soundex(name) do
name
|> String.graphemes()
|> remove_invalid_graphemes_from_beginning()
|> Enum.with_index()
|> Enum.map(&with_codes(&1))
|> remove_duplicates_grapheme([], false)
|> Enum.reverse()
|> Enum.with_index()
|> Enum.map_join("", &join_grapheme/1)
|> String.pad_trailing(4, "0")
|> String.slice(0..3)
|> remove_invalid_result()
end
defp remove_invalid_graphemes_from_beginning(graphemes) do
graphemes
|> Enum.filter(&Regex.match?(~r/[a-zA-Z-. ]/, &1))
|> Enum.join("")
|> String.replace(~r/[0-9]{1,}/, "")
|> String.trim()
|> String.graphemes()
end
defp remove_invalid_result("0000"), do: ""
defp remove_invalid_result(result), do: result
defp remove_duplicates_grapheme([], processed, _latest), do: processed
defp remove_duplicates_grapheme([{"H", _code, 0} = grapheme | graphemes], processed, _latest) do
Logger.debug("second1: #{inspect([grapheme, :unknown, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :unknown)
end
defp remove_duplicates_grapheme([{"W", _code, 0} = grapheme | graphemes], processed, _latest) do
remove_duplicates_grapheme(graphemes, [grapheme | processed], :unknown)
end
defp remove_duplicates_grapheme([{_letter, :unknown, 0} | graphemes], processed, _latest) do
remove_duplicates_grapheme(graphemes, processed, :unknown)
end
defp remove_duplicates_grapheme([{_, :vowel, 0} = grapheme | graphemes], processed, _latest) do
remove_duplicates_grapheme(graphemes, [grapheme | processed], :vowel)
end
defp remove_duplicates_grapheme([{_, _code, 0} = grapheme | graphemes], processed, _latest) do
remove_duplicates_grapheme(graphemes, [grapheme | processed], false)
end
defp remove_duplicates_grapheme([{_letter, :unknown, _index} | graphemes], processed, _latest) do
remove_duplicates_grapheme(graphemes, processed, :unknown)
end
defp remove_duplicates_grapheme([{_letter, :vowel, _index} | graphemes], processed, _latest) do
remove_duplicates_grapheme(graphemes, processed, :vowel)
end
defp remove_duplicates_grapheme(
[{_letter, code, _index} = grapheme | graphemes],
processed,
latest
) do
case processed do
##########################################################################
# Second Letter
##########################################################################
[{_, first_code, _}] when first_code == code and latest == :vowel ->
Logger.debug("second1: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
[{_, first_code, _}] when first_code == code and latest == :unknown ->
Logger.debug("second2: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
[{_, first_code, _}] when first_code == code ->
Logger.debug("second3: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, processed, :letter)
[{_, _, _}] ->
Logger.debug("second4: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
##########################################################################
# Third Letter
##########################################################################
[{_second_letter, second_code, _}, _] when second_code == code and latest == :vowel ->
Logger.debug("third1: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
[{_second_letter, second_code, _}, _] when second_code == code and latest == :unknown ->
Logger.debug("third2: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
[{_second_letter, second_code, _}, _] when second_code == code ->
Logger.debug("third3: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, processed, :letter)
[_second, _] ->
Logger.debug("third4: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
##########################################################################
# Fourth Letter
##########################################################################
[{_, third_code, _}, _, _] when third_code == code and latest == :vowel ->
Logger.debug("fourth1: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
[{_, third_code, _}, _, _] when third_code == code and latest == :unknown ->
Logger.debug("fourth2: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
[{_, third_code, _}, _, _] when third_code == code ->
Logger.debug("fourth3: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, processed, :letter)
[_third, _, _] ->
Logger.debug("fourth4: #{inspect([grapheme, latest, processed])}")
remove_duplicates_grapheme(graphemes, [grapheme | processed], :letter)
##########################################################################
##########################################################################
_ ->
remove_duplicates_grapheme(graphemes, processed, :letter)
end
end
defp join_grapheme({{grapheme, _code, _index}, 0}), do: grapheme
defp join_grapheme({{_grapheme, code, _index}, _}), do: code
defp with_codes({grapheme, index}) do
code =
grapheme
|> String.upcase()
|> code_with_weight()
{String.upcase(grapheme), code, index}
end
defp code_with_weight("B"), do: 1
defp code_with_weight("F"), do: 1
defp code_with_weight("P"), do: 1
defp code_with_weight("V"), do: 1
defp code_with_weight("C"), do: 2
defp code_with_weight("S"), do: 2
defp code_with_weight("K"), do: 2
defp code_with_weight("G"), do: 2
defp code_with_weight("J"), do: 2
defp code_with_weight("Q"), do: 2
defp code_with_weight("X"), do: 2
defp code_with_weight("Z"), do: 2
defp code_with_weight("D"), do: 3
defp code_with_weight("T"), do: 3
defp code_with_weight("L"), do: 4
defp code_with_weight("M"), do: 5
defp code_with_weight("N"), do: 5
defp code_with_weight("R"), do: 6
defp code_with_weight("A"), do: :vowel
defp code_with_weight("E"), do: :vowel
defp code_with_weight("I"), do: :vowel
defp code_with_weight("O"), do: :vowel
defp code_with_weight("U"), do: :vowel
defp code_with_weight("Y"), do: :vowel
defp code_with_weight(_), do: :unknown
end