lib/akin/algorithms/hamming.ex

defmodule Akin.Hamming do
  @moduledoc """
  Functions to calculate the Hamming Distance between 2 given strings. The Hamming Distance
  is the the smallest number of substitutions needed to change one string into the other string.

  If the strings are not the same length, nil is returned
  If the string are equal, 0.0 is returned.
  """
  @behaviour Akin.Task
  alias Akin.Corpus

  @spec compare(%Corpus{}, %Corpus{}) :: float()
  @spec compare(%Corpus{}, %Corpus{}, Keyword.t()) :: float()
  @doc """
  Calculates the Hamming distance between 2 given strings.

  ## Examples

    iex> Akin.Hamming.compare(%Akin.Corpus{string: "toned"}, %Akin.Corpus{string: "roses"}, [])
    0.6
    iex> Akin.Hamming.compare(%Akin.Corpus{string: "toned"}, %Akin.Corpus{string: "peanut"}, [])
    nil
    iex> Akin.Hamming.compare(%Akin.Corpus{string: "toned"}, %Akin.Corpus{string: "toned"}, [])
    0.0
  """
  def compare(%Corpus{string: left}, %Corpus{string: right}, _opts) do
    compare(left, right)
  end

  def compare(_, _, _), do: nil

  def compare(left, right)
      when byte_size(left) == 0 or
             byte_size(right) == 0 or
             byte_size(left) != byte_size(right) do
    nil
  end

  def compare(left, right) when left == right, do: 0.0

  def compare(left, right) when is_binary(left) and is_binary(right) do
    score =
      left
      |> String.codepoints()
      |> Enum.zip(right |> String.codepoints())
      |> Enum.count(fn {cp1, cp2} -> cp1 != cp2 end)

    score / byte_size(left)
  end
end