lib/faker/util.ex

defmodule Faker.Util do
  import Faker, only: [localize: 1]

  @moduledoc """
  Collection of useful functions for your fake data. Functions aware of locale.
  """

  @doc """
  Pick a random element from an enumerable. Can also be provided a blacklist enumerable as a second argument.

  ## Examples

      iex> Faker.Util.pick(10..100)
      79
      iex> Faker.Util.pick([1, 3, 5, 7])
      3
      iex> Faker.Util.pick([true, false, nil])
      true
      iex> Faker.Util.pick(["a", "b", "c"], ["b"])
      "a"
      iex> Faker.Util.pick([1, "2", 3.0], 1..10)
      "2"
  """

  @spec pick(Enum.t(), Enum.t()) :: any
  def pick(enum, blacklist)

  def pick(enum, blacklist) do
    Enum.reject(enum, fn el -> Enum.member?(blacklist, el) end)
    |> pick()
  end

  def pick(enum) do
    Enum.at(enum, Faker.random_between(0, Enum.count(enum) - 1))
  end

  @doc """
  Generate N unique elements

  ## Examples

      iex> Faker.Util.sample_uniq(2, &Faker.Internet.email/0)
      ["conor2058@schiller.com", "elizabeth2056@rolfson.net"]
      iex> Faker.Util.sample_uniq(10, fn -> Faker.String.base64(4) end)
      [
        "0CzJ",
        "3nuk",
        "D1Ip",
        "IqFG",
        "My3J",
        "W3yW",
        "e/kH",
        "gd75",
        "hVCK",
        "snJn"
      ]
      iex> Faker.Util.sample_uniq(1, &Faker.Phone.EnUs.area_code/0)
      ["508"]
      iex> Faker.Util.sample_uniq(0, &Faker.Internet.email/0)
      ** (FunctionClauseError) no function clause matching in Faker.Util.sample_uniq/3
  """
  @spec sample_uniq(pos_integer, (-> any), MapSet.t()) :: [any]
  def sample_uniq(count, sampler, acc \\ MapSet.new())
      when is_integer(count) and count > 0 and is_function(sampler, 0) do
    case MapSet.size(acc) do
      ^count ->
        MapSet.to_list(acc)

      _ ->
        acc = MapSet.put(acc, sampler.())

        sample_uniq(count, sampler, acc)
    end
  end

  @doc """
  Execute fun n times with the index as first param and return the results as a list

  ## Examples

      iex> Faker.Util.list(3, &(&1))
      [0, 1, 2]
      iex> Faker.Util.list(3, &(&1 + 1))
      [1, 2, 3]
      iex> Faker.Util.list(5, &(&1 * &1))
      [0, 1, 4, 9, 16]
      iex> Faker.Util.list(3, &(to_string(&1)))
      ["0", "1", "2"]
  """
  @spec list(integer, (integer -> any)) :: [any]
  def list(n, fun) when is_function(fun, 1) do
    Enum.map(0..(n - 1), &fun.(&1))
  end

  @spec list(integer, (-> any)) :: [any]
  def list(n, fun) when is_function(fun, 0) do
    Enum.map(0..(n - 1), fn _ -> fun.() end)
  end

  @doc """
  Execute fun n times with the index as first param and join the results with joiner

  ## Examples

      iex> Faker.Util.join(3, ", ", &Faker.Code.isbn13/0)
      "9781542646109, 9783297052358, 9790203032090"
      iex> Faker.Util.join(4, "-", fn -> Faker.format("####") end)
      "7337-6033-7459-8109"
      iex> Faker.Util.join(2, " vs ", &Faker.Superhero.name/0)
      "Falcon vs Green Blink Claw"
      iex> Faker.Util.join(2, " or ", &Faker.Color.name/0)
      "Purple or White"
  """
  @spec join(integer, binary, (-> binary)) :: binary
  def join(n, joiner \\ "", fun) do
    Enum.join(list(n, fun), joiner)
  end

  @doc """
  Get a random digit as a string; one of 0-9

  ## Examples

      iex> Faker.Util.digit()
      "0"
      iex> Faker.Util.digit()
      "1"
      iex> Faker.Util.digit()
      "5"
      iex> Faker.Util.digit()
      "4"
  """
  @spec digit() :: binary
  localize(:digit)

  @doc """
  Converts a list to a string, with "and" before the last item. Uses an Oxford comma.

  ## Examples

      iex> Faker.Util.to_sentence(["Black", "White"])
      "Black and White"
      iex> Faker.Util.to_sentence(["Jon Snow"])
      "Jon Snow"
      iex> Faker.Util.to_sentence(["Oceane", "Angeline", "Nicholas"])
      "Angeline, Nicholas, and Oceane"
      iex> Faker.Util.to_sentence(["One", "Two", "Three", "Four"])
      "Two, Three, Four, and One"
  """
  @spec to_sentence([binary]) :: binary
  def to_sentence(items) do
    Module.concat(__MODULE__, Faker.mlocale()).to_sentence(items)
  end

  @doc """
  Get a random alphabet character as a string; one of a-z or A-Z

  ## Examples

      iex> Faker.Util.letter()
      "E"
      iex> Faker.Util.letter()
      "L"
      iex> Faker.Util.letter()
      "R"
      iex> Faker.Util.letter()
      "C"
      iex> Faker.Util.letter()
      "e"
  """
  @spec letter() :: binary
  localize(:letter)

  @doc """
  Get a random lowercase character as a string; one of a-z

  ## Examples

      iex> Faker.Util.lower_letter()
      "e"
      iex> Faker.Util.lower_letter()
      "l"
      iex> Faker.Util.lower_letter()
      "r"
      iex> Faker.Util.lower_letter()
      "c"
  """
  @spec lower_letter() :: binary
  localize(:lower_letter)

  @doc """
  Get a random uppercase character as a string; one of A-Z

  ## Examples

      iex> Faker.Util.upper_letter()
      "E"
      iex> Faker.Util.upper_letter()
      "L"
      iex> Faker.Util.upper_letter()
      "R"
      iex> Faker.Util.upper_letter()
      "C"
  """
  @spec upper_letter() :: binary
  localize(:upper_letter)

  @doc """
  Start a cycle. See cycle/1
  """
  @spec cycle_start([any]) :: pid
  def cycle_start(items) do
    {:ok, cycle_pid} = Agent.start_link(fn -> {[], items} end)
    cycle_pid
  end

  @doc """
  Cycle randomly through the given list with guarantee every element of the list is used once before
  elements are being picked again. This is done by keeping a list of remaining elements that have
  not been picked yet. The list of remaining element is returned, as well as the randomly picked
  element.
  """
  @spec cycle(pid) :: any
  def cycle(cycle_pid) do
    Agent.get_and_update(cycle_pid, fn
      {[], items} ->
        [h | t] = Faker.shuffle(items)
        {h, {t, items}}

      {[h | t], items} ->
        {h, {t, items}}
    end)
  end

  @doc """
  Format a string with randomly generated data. Format specifiers are replaced by random values. A
  format specifier follows this prototype:

      %[length]specifier

  The following specifier rules are present by default:

    - **d**: digits 0-9
    - **a**: lowercase letter a-z
    - **A**: uppercase letter A-Z
    - **b**: anycase letter a-z, A-Z

  The specifier rules can be overridden using the second argument.

  ## Examples

      iex> Faker.Util.format("%2d-%3d %a%A %2d%%")
      "01-542 aS 61%"
      iex> Faker.Util.format("%8nBATMAN", n: fn() -> "nana " end)
      "nana nana nana nana nana nana nana nana BATMAN"
  """
  @spec format(binary, Keyword.t()) :: binary
  def format(
        format_str,
        rules \\ [d: &digit/0, A: &upper_letter/0, a: &lower_letter/0, b: &letter/0]
      ) do
    Regex.replace(~r/%(?:%|(\d*)([a-zA-Z]))/, format_str, &format_replace(&1, &2, &3, rules))
  end

  defp format_replace("%%", _, _, _), do: "%"

  defp format_replace(_, "", rule_char, rules) do
    format_replace(nil, 1, rule_char, rules)
  end

  defp format_replace(_, length_str, rule_char, rules) when is_binary(length_str) do
    format_replace(nil, String.to_integer(length_str), rule_char, rules)
  end

  defp format_replace(_, n, rule_char, rules) when is_integer(n) do
    rule_key = String.to_existing_atom(rule_char)

    case rules[rule_key] do
      fun when is_function(fun) -> join(n, fun)
      _ -> raise "Rule #{rule_key} not found or not a function"
    end
  end
end