lib/validators/number.ex

defmodule Dsv.Number do
  use Dsv.Validator
  @behaviour Dsv.Comparator

  @moduledoc """
  Dsv.Number module provides functions to validate a number based on various options.


  ## Possible options
    - `:gt` - Greater than (value must be greater than the specified number).
    - `:lt` - Lower than (value must be lower than the specified number).
    - `:gte` - Greater than or equal (value must be greater than or equal to the specified number).
    - `:lte` - Lower than or equal (value must be lower than or equal to the specified number).
    - `:eq` - Equal (value must be equal to the specified number).
    - `:between` - Between (value must be within the specified range defined by two numbers).

  Options can be combined.

  """

  message({:gt, "Value <%= inspect data %> must be greater than: <%= options[:gt] %>"})
  message({:lt, "Value <%= inspect data %> must be lower than: <%= options[:lt] %>"})
  message({:eq, "Value <%= inspect data %> must be equal to value <%= options[:eq] %>"})

  message(
    {[:gt, :lt],
     "Value <%= inspect data %> must be lower than: <%= options[:lt] %> and greater than: <%= options[:gt] %>"}
  )

  message(
    {[:gte, :lt],
     "Value <%= inspect data %> must be lower than: <%= options[:lt] %> and greater than or equal: <%= options[:gte] %>"}
  )

  message(
    {[:gt, :lte],
     "Value <%= inspect data %> must be lower than or equal: <%= options[:lte] %> and greater than: <%= options[:gt] %>"}
  )

  message({:gte, "Value <%= inspect data %> must be greater than or equal: <%= options[:gte] %>"})
  message({:lte, "Value <%= inspect data %> must be lower than or equal: <%= options[:lte] %>"})

  message(
    {[:gte, :lte],
     "Value <%= inspect data %> must be lower than or equal: <%= options[:lte] %> and greater than or equal: <%= options[:gte] %>"}
  )

  message(
    {[:between],
     "Value <%= inspect data %> must be lower than or equal: <%= elem(options[:between], 1) %> and greater than or equal: <%= elem(options[:between], 0) %>"}
  )

  @doc """
  The `valid?/2` function checks if a provided number meets specific validation criteria based on the given options (in the form of a keyword list).


  ## Parameters

    * `number` - The number to be validated (integer of float).
    * `options` - A list of validation options. Each option consists of a keyword followed by the corresponding value.
      Supported options include:
      - `:gt` - Greater than (value must be greater than the specified number).
      - `:lt` - Lower than (value must be lower than the specified number).
      - `:gte` - Greater than or equal (value must be greater than or equal to the specified number).
      - `:lte` - Lower than or equal (value must be lower than or equal to the specified number).
      - `:eq` - Equal (value must be equal to the specified number).
      - `:between` - Between (value must be within the specified range defined by two numbers).

  ## Returns

  A boolean value:

  - `true` if the `number` meets all the specified validation criteria.
  - `false` if the `number` fails to meet any of the specified criteria.


  ## Examples

  `:gt` example:
      iex> Dsv.Number.valid?(4, gt: 3)
      :true

      iex> Dsv.Number.valid?(4, gt: 4)
      :false

      iex> Dsv.Number.valid?(4, gt: 5)
      :false

  `:gte` example:
      iex> Dsv.Number.valid?(4, gte: 3)
      :true

      iex> Dsv.Number.valid?(4, gte: 4)
      :true

      iex> Dsv.Number.valid?(4, gte: 5)
      :false

  `:lt` example:
      iex> Dsv.Number.valid?(4, lt: 3)
      :false

      iex> Dsv.Number.valid?(4, lt: 4)
      :false

      iex> Dsv.Number.valid?(4, lt: 5)
      :true

  `:lte` example:
      iex> Dsv.Number.valid?(4, lte: 3)
      :false

      iex> Dsv.Number.valid?(4, lte: 4)
      :true

      iex> Dsv.Number.valid?(4, lte: 5)
      :true

  `:gt & :lt` example:
      iex> Dsv.Number.valid?(4, gt: 3, lt: 5)
      :true

      iex> Dsv.Number.valid?(4, gt: 4, lt: 5)
      :false

      iex> Dsv.Number.valid?(4, gt: 2, lt: 4)
      :false

  `:gte & :lt` example:
      iex> Dsv.Number.valid?(4, gte: 3, lt: 5)
      :true

      iex> Dsv.Number.valid?(4, gte: 4, lt: 5)
      :true

      iex> Dsv.Number.valid?(4, gte: 2, lt: 4)
      :false

  `:gt & :lte` example:
      iex> Dsv.Number.valid?(4, gt: 3, lte: 5)
      :true

      iex> Dsv.Number.valid?(4, gt: 4, lte: 5)
      :false

      iex> Dsv.Number.valid?(4, gt: 2, lte: 4)
      :true

  `:gte & :lte` example:
      iex> Dsv.Number.valid?(4, gte: 3, lte: 5)
      :true

      iex> Dsv.Number.valid?(4, gte: 4, lte: 5)
      :true

      iex> Dsv.Number.valid?(4, gte: 2, lte: 4)
      :true

      iex> Dsv.Number.valid?(1, gte: 2, lte: 4)
      :false

      iex> Dsv.Number.valid?(5, gte: 2, lte: 4)
      :false

  `:between` example:
      iex> Dsv.Number.valid?(4, between: {3, 5})
      :true

      iex> Dsv.Number.valid?(4, between: {4, 5})
      :true

      iex> Dsv.Number.valid?(4, between: {2, 4})
      :true

      iex> Dsv.Number.valid?(1, between: {2, 4})
      :false

      iex> Dsv.Number.valid?(5, between: {2, 4})
      :false

  `:eq` example:
      iex> Dsv.Number.valid?(4, eq: 4)
      :true

      iex> Dsv.Number.valid?(4, eq: 5)
      :false

  """
  def valid?(data, options) when is_list(options),
    do:
      options
      |> Enum.all?(fn {function_name, value} -> compare(function_name, data, value) end)

  @doc """
  The `validate/2` function checks if a provided number meets specific validation criteria based on the given options.

  ## Parameters

    * `number` - The number to be validated.
    * `options` - A list of validation options. Each option consists of a keyword followed by the corresponding value.
      Supported options include:
      - `:gt` - Greater than (value must be greater than the specified number).
      - `:lt` - Lower than (value must be lower than the specified number).
      - `:gte` - Greater than or equal (value must be greater than or equal to the specified number).
      - `:lte` - Lower than or equal (value must be lower than or equal to the specified number).
      - `:eq` - Equal (value must be equal to the specified number).
      - `:between` - Between (value must be within the specified range defined by two numbers).
    * `message` (optional) - Custom message returned in case of error.

  ## Returns

  - `:ok` if the `number` meets all the specified validation criteria.
  - `{:error, message}` if the `number` fails to meet any of the specified criteria.


  ## Examples

  `:gt` example:
      iex> Dsv.Number.validate(4, gt: 3)
      :ok

      iex> Dsv.Number.validate(4, gt: 4)
      {:error, "Value 4 must be greater than: 4"}

      iex> Dsv.Number.validate(4, gt: 5)
      {:error, "Value 4 must be greater than: 5"}

      iex> Dsv.Number.validate(4, gt: 5, message: "This number is too small.")
      {:error, "This number is too small."}

  `:gte` example:
      iex> Dsv.Number.validate(4, gte: 3)
      :ok

      iex> Dsv.Number.validate(4, gte: 4)
      :ok

      iex> Dsv.Number.validate(4, gte: 5)
      {:error, "Value 4 must be greater than or equal: 5"}

      iex> Dsv.Number.validate(2, gte: 5, message: fn
      ...>  data, _options when data > 3 -> "This value is too small."
      ...>  _data, _options -> "This value is way too small."
      ...> end)
      {:error, "This value is way too small."}


  `:lt` example:
      iex> Dsv.Number.validate(4, lt: 3)
      {:error, "Value 4 must be lower than: 3"}

      iex> Dsv.Number.validate(4, lt: 4)
      {:error, "Value 4 must be lower than: 4"}

      iex> Dsv.Number.validate(4, lt: 4, message: "Please provide smaller number.")
      {:error, "Please provide smaller number."}


      iex> Dsv.Number.validate(4, lt: 5)
      :ok

  `:lte` example:
      iex> Dsv.Number.validate(4, lte: 3)
      {:error, "Value 4 must be lower than or equal: 3"}

      iex> Dsv.Number.validate(4, lte: 4)
      :ok

      iex> Dsv.Number.validate(4, lte: 5)
      :ok

  `:gt & :lt` example:
      iex> Dsv.Number.validate(4, gt: 3, lt: 5)
      :ok

      iex> Dsv.Number.validate(4, gt: 4, lt: 5)
      {:error, "Value 4 must be lower than: 5 and greater than: 4"}

      iex> Dsv.Number.validate(4, gt: 2, lt: 4)
      {:error, "Value 4 must be lower than: 4 and greater than: 2"}

  `:gte & :lt` example:
      iex> Dsv.Number.validate(4, gte: 3, lt: 5)
      :ok

      iex> Dsv.Number.validate(4, gte: 4, lt: 5)
      :ok

      iex> Dsv.Number.validate(4, gte: 2, lt: 4)
      {:error, "Value 4 must be lower than: 4 and greater than or equal: 2"}

  `:gt & :lte` example:
      iex> Dsv.Number.validate(4, gt: 3, lte: 5)
      :ok

      iex> Dsv.Number.validate(4, gt: 4, lte: 5)
      {:error, "Value 4 must be lower than or equal: 5 and greater than: 4"}

      iex> Dsv.Number.validate(4, gt: 2, lte: 4)
      :ok

  `:gte & :lte` example:
      iex> Dsv.Number.validate(4, gte: 3, lte: 5)
      :ok

      iex> Dsv.Number.validate(4, gte: 4, lte: 5)
      :ok

      iex> Dsv.Number.validate(4, gte: 2, lte: 4)
      :ok

      iex> Dsv.Number.validate(1, gte: 2, lte: 4)
      {:error, "Value 1 must be lower than or equal: 4 and greater than or equal: 2"}

      iex> Dsv.Number.validate(5, gte: 2, lte: 4)
      {:error, "Value 5 must be lower than or equal: 4 and greater than or equal: 2"}

  `:between` example:
      iex> Dsv.Number.validate(4, between: {3, 5})
      :ok

      iex> Dsv.Number.validate(4, between: {4, 5})
      :ok

      iex> Dsv.Number.validate(4, between: {2, 4})
      :ok

      iex> Dsv.Number.validate(1, between: {2, 4})
      {:error, "Value 1 must be lower than or equal: 4 and greater than or equal: 2"}

      iex> Dsv.Number.validate(5, between: {2, 4})
      {:error, "Value 5 must be lower than or equal: 4 and greater than or equal: 2"}

  `:eq` example:
      iex> Dsv.Number.validate(4, eq: 4)
      :ok

      iex> Dsv.Number.validate(4, eq: 5)
      {:error, "Value 4 must be equal to value 5"}

  `wrong options` example:
      iex> Dsv.Number.validate(1, lt: 2, gt: "not a number")
      {:error, "Value 1 must be lower than: 2 and greater than: not a number"}

      iex> Dsv.Number.validate("wrong data", between: {1, 5})
      {:error, ~s(Value "wrong data" must be lower than or equal: 5 and greater than or equal: 1)}

  """
  def validate(data, options), do: super(data, options)

  defp compare(_, number1, {min, max})
       when not is_number(number1) or not is_number(min) or not is_number(max),
       do: false

  defp compare(_, number1, number2)
       when not is_number(number1) or (not is_number(number2) and not is_tuple(number2)),
       do: false

  defp compare(:gt, number1, number2), do: number1 > number2
  defp compare(:lt, number1, number2), do: number1 < number2
  defp compare(:gte, number1, number2), do: number1 >= number2
  defp compare(:lte, number1, number2), do: number1 <= number2
  defp compare(:between, number1, {min, max}), do: number1 >= min and number1 <= max
  defp compare(:eq, number1, number2), do: number1 == number2

  @impl Dsv.Comparator
  def to_comparator(value, [:lt]), do: [lt: value]

  @impl Dsv.Comparator
  def to_comparator(value, [:gt]), do: [gt: value]

  @impl Dsv.Comparator
  def to_comparator(value, [:gte]), do: [gte: value]

  @impl Dsv.Comparator
  def to_comparator(value, [:lte]), do: [lte: value]

  @impl Dsv.Comparator
  def to_comparator(_value, options) do
    {:error, "Can't create comparator from given options #{options}"}
  end
end