Current section
Files
Jump to
Current section
Files
lib/akin.ex
defmodule Akin do
@moduledoc """
Compare two strings for similarity. Options accepted in a keyword list (i.e. [ngram_size: 3]).
1. `algorithms`: algorithms to use in comparision. Accepts the name or a keyword list. Default is algorithms/0.
1. `metric` - algorithm metric. Default is both
- "string": uses string algorithms
- "phonetic": uses phonetic algorithms
1. `unit` - algorithm unit. Default is both.
- "whole": uses algorithms best suited for whole string comparison (distance)
- "partial": uses algorithms best suited for partial string comparison (substring)
1. `level` - level for double phonetic matching. Default is "normal".
- "strict": both encodings for each string must match
- "strong": the primary encoding for each string must match
- "normal": the primary encoding of one string must match either encoding of other string (default)
- "weak": either primary or secondary encoding of one string must match one encoding of other string
1. `match_at`: an algorith score equal to or above this value is condsidered a match. Default is 0.9
1. `ngram_size`: number of contiguous letters to split strings into. Default is 2.
1. `short_length`: qualifies as "short" to recieve a shortness boost. Used by Name Metric. Default is 8.
1. `stem`: boolean representing whether to compare the stemmed version the strings; uses Stemmer. Default `false`
"""
import Akin.Util,
only: [list_algorithms: 0, list_algorithms: 1, modulize: 1, compose: 1, opts: 2]
alias Akin.Corpus
alias Akin.Names
NimbleCSV.define(CSVParse, separator: "\t")
@opts [ngram_size: 2, level: "normal", short_length: 8, match_at: 0.9]
@spec compare(binary() | %Corpus{}, binary() | %Corpus{}, keyword()) :: float()
@doc """
Compare two strings. Return map of algorithm metrics.
Options accepted as a keyword list. If no options are given, default values will be used.
"""
def compare(left, right, opts \\ @opts)
def compare(left, right, opts) when is_binary(left) and is_binary(right) do
if opts(opts, :stem) do
left = compose(left).stems |> Enum.join(" ")
right = compose(right).stems |> Enum.join(" ")
compare(compose(left), compose(right), opts)
else
compare(compose(left), compose(right), opts)
end
end
def compare(%Corpus{} = left, %Corpus{} = right, opts) do
Enum.reduce(algorithms(opts), %{}, fn algorithm, acc ->
Map.put(acc, algorithm, apply(modulize(algorithm), :compare, [left, right, opts]))
end)
|> Enum.reduce([], fn {k, v}, acc ->
if is_nil(v) do
acc
else
[{String.replace(k, ".", ""), v} | acc]
end
end)
|> Enum.map(fn {k, v} -> {String.to_atom(k), r(v)} end)
|> Enum.into(%{})
end
@spec compare_using(binary(), binary() | %Corpus{}, binary() | %Corpus{}, keyword()) :: float()
@doc """
Compare two strings using a particular algorithm. Return a map of metrics. The algorithm
name must be an atom (i.e. "jaro_winkler").
Options accepted as a keyword list. If no options are given, the default values will be used.
"""
def compare_using(algorithm, left, right, opts \\ @opts)
def compare_using(algorithm, left, right, opts) when is_binary(left) and is_binary(right) do
compare_using(algorithm, compose(left), compose(right), opts)
end
def compare_using(algorithm, %Corpus{} = left, %Corpus{} = right, opts) do
apply(modulize(algorithm), :compare, [left, right, opts]) |> Float.round(2)
end
@spec max(map()) :: float()
@spec max(binary() | %Corpus{}, binary() | %Corpus{}, keyword()) :: list()
@doc """
Compare two strings. Return only the highest algorithm metrics. Options accepted as a keyword list.
"""
def max(%{} = scores) do
{_k, max_val} = Enum.max_by(scores, fn {_k, v} -> v end)
Enum.filter(scores, fn {_k, v} -> v == max_val end)
end
def max(left, right, opts \\ @opts) do
compare(left, right, opts)
|> max()
end
@spec match_names(binary() | %Corpus{}, binary() | %Corpus{}, keyword()) :: float()
@doc """
Compare a string against a list of strings. Matches are determined by algorithem metrics equal to or higher than the
`match_at` option. Return a list of strings that are a likely match.
"""
def match_names(left, rights, opts \\ @opts)
def match_names(left, rights, opts) when is_binary(left) and is_list(rights) do
rights = Enum.map(rights, fn right -> compose(right) end)
match_names(compose(left), rights, opts)
end
def match_names(%Corpus{} = left, rights, opts) do
Enum.reduce(rights, [], fn right, acc ->
case Names.compare(left, right, opts) do
%{scores: scores} ->
if Enum.any?(scores, fn {_algo, score} -> score > opts(opts, :match_at) end) do
[right.original | acc]
else
acc
end
_ ->
acc
end
end)
end
@spec match_names_metrics(binary(), list(), keyword()) :: float()
@doc """
Compare a string against a list of strings. Matches are determined by algorithem metrics equal to or higher than the
`match_at` option. Return a list of strings that are a likely match and their algorithm metrics.
"""
def match_names_metrics(left, rights, opts \\ @opts)
def match_names_metrics(left, rights, opts) when is_binary(left) and is_list(rights) do
Enum.reduce(rights, [], fn right, acc ->
%{left: left, right: right, metrics: scores, match: match} =
match_name_metrics(left, right, opts)
if match == 1 do
[%{left: left, right: right, metrics: scores, match: 1} | acc]
else
[%{left: left, right: right, metrics: scores, match: 0} | acc]
end
end)
end
@spec match_name_metrics(binary(), binary(), keyword()) :: float()
@doc """
Compare a string to a string with logic specific to names. Matches are determined by algorithem
metrics equal to or higher than the `match_at` option. Return a list of strings that are a likely
match and their algorithm metrics.
"""
def match_name_metrics(left, rights, opts \\ @opts)
def match_name_metrics(left, right, opts) when is_binary(left) and is_binary(right) do
left = compose(left)
right = compose(right)
case Names.compare(left, right, opts) do
%{scores: scores} ->
left = Enum.join(left.list, " ")
right = Enum.join(right.list, " ")
if Enum.any?(scores, fn {_algo, score} -> score > opts(opts, :match_at) end) do
%{left: left, right: right, metrics: scores, match: 1}
else
%{left: left, right: right, metrics: scores, match: 0}
end
_ ->
nil
end
end
@spec phonemes(binary() | %Corpus{}) :: list()
@doc """
Returns list of unqieu phonetic representations of a string resulting from the single and
double metaphone algorithms.
"""
def phonemes(string) when is_binary(string) do
phonemes(compose(string), string)
end
defp phonemes(%Corpus{string: string}, _original_string) do
single = Akin.Metaphone.Single.compute(string)
double = Akin.Metaphone.Double.parse(string) |> Tuple.to_list()
[single | double]
|> List.flatten()
|> Enum.uniq()
end
@doc """
Return the default option values
"""
def default_opts, do: @opts
@doc """
List of algorithms available for us in making comparisons.
"""
def algorithms(), do: list_algorithms()
def algorithms(opts), do: list_algorithms(opts)
@doc """
Round data types that can be rounded to 2 decimal points.
"""
def r(v) when is_float(v), do: Float.round(v, 2)
def r(v) when is_binary(v), do: Float.round(String.to_float(v), 2)
def r(v) when is_integer(v), do: Float.round(v / 1, 2)
end