lib/random.ex

defmodule Moar.Random do
  # @related [test](/test/random_test.exs)

  @moduledoc "Generates random data."

  @type encoding() :: :base32 | :base64

  @doc """
  Returns a string that starts with `prefix` (defaults to "id") followed by a dash, followed by 10 random lowercase
  letters and numbers, like `foo-ag49cl29zd` or `id-ag49cl29zd`.
  """
  @spec dom_id(String.t()) :: String.t()
  def dom_id(prefix \\ "id"),
    do: "#{prefix}-#{string(10, :base32)}" |> String.downcase()

  @doc "Return a random float greater than or equal to `min` and less than `max`"
  @spec float(number(), number()) :: float()
  def float(min, max) when max > min,
    do: min + :rand.uniform() * (max - min)

  @doc """
  Randomly increases or decreases `number` by a random amount up to `percent` of `number`.
  For example, `Etc.Random.fuzz(100, 0.2)` could return a number as low as 80.0 or as high as 120.0.

  ```elixir
  iex> n = Etc.Random.fuzz(100, 0.2)
  iex> n <= 120 && n >= 80
  true
  iex> n > 120 || n <= 80
  false
  ```
  """
  @spec fuzz(number(), number()) :: number()
  def fuzz(number, percent) when is_number(number) and is_number(percent) and percent >= 0 and percent <= 1,
    do: number * (1.0 - Moar.Random.float(-percent, percent))

  @doc "Returns a random integer between `0` and `max`."
  @spec integer(max :: pos_integer()) :: pos_integer()
  def integer(max \\ 1_000_000_000),
    do: 0..max |> Enum.random()

  @doc """
  Returns a base64- or base32-encoded random string of 32 characters.
  See `Moar.Random.string/2`.
  """
  @spec string(encoding :: encoding()) :: binary()
  def string(:base32), do: string(32, :base32)
  def string(:base64), do: string(32, :base64)

  @doc """
  Returns a base64- or base32-encoded random string of given length.

  ```elixir
  iex> Moar.Random.string()
  "Sr/y4m/YiVSJcIgI5lG+76vMfaZ7KZ7c"
  iex> Moar.Random.string(5)
  "9pJrK"
  iex> Moar.Random.string(5, :base32)
  "AC53Z"
  ```
  """
  @spec string(character_count :: pos_integer(), encoding :: encoding()) :: binary()
  def string(character_count \\ 32, encoding \\ :base64) when is_number(32) and encoding in [:base32, :base64] do
    character_count
    |> :crypto.strong_rand_bytes()
    |> encode(encoding)
    |> binary_part(0, character_count)
  end

  # # #

  defp encode(bytes, :base32), do: Base.encode32(bytes, padding: false)
  defp encode(bytes, :base64), do: Base.encode64(bytes, padding: false)
end