lib/growth_book/hash.ex

defmodule GrowthBook.Hash do
  @type value() :: String.t() | integer()
  @type seed() :: String.t() | integer()
  @type version() :: 1 | 2

  @doc """
  Hashes a string to a float between `0.0` and `1.0`, using the
  [Fowler-Noll-Vo](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function)
  (`fnv32a`) algorithm.
  """
  @spec hash(seed(), value(), version()) :: float() | nil
  def hash(seed, value, version) do
    seed = to_string(seed)
    value = to_string(value)

    case version do
      1 -> hash_v1(seed, value)
      2 -> hash_v2(seed, value)
      _ -> nil
    end
  end

  defp hash_v1(seed, value) do
    (value <> seed)
    |> fnv32a()
    |> mod(1000)
  end

  defp hash_v2(seed, value) do
    (seed <> value)
    |> fnv32a()
    |> Integer.to_string()
    |> fnv32a()
    |> mod(10000)
  end

  defp mod(n, m), do: rem(n, m) / m

  @fnv32_prime 16_777_619
  @fnv32_init 2_166_136_261
  @fnv32_mask 0xFFFFFFFF

  # Fowler-Noll-Vo 32-bit FNV-1a hash
  @doc false
  @spec fnv32a(binary(), integer()) :: integer()
  def fnv32a(data, state \\ @fnv32_init)

  def fnv32a(<<head::8, tail::binary>>, state) do
    import Bitwise, only: [band: 2, bxor: 2]

    hash = band(bxor(state, head) * @fnv32_prime, @fnv32_mask)

    fnv32a(tail, hash)
  end

  def fnv32a(<<>>, state), do: state
end