Current section
Files
Jump to
Current section
Files
lib/guardsafe.ex
defmodule Guardsafe do
@vsn "0.5.1"
@doc false
def version, do: @vsn
@moduledoc """
Provides readability-enhancing macros that can safely be used in guard clauses.
## Examples
defmodule MacrofyAllTheThings do
import Guardsafe
require Integer
def magic(number) when number |> nil? do
"Staring into the void..."
end
def magic(number) when number |> even? do
"That's not odd."
end
def magic(number) when number |> divisible_by?(5) do
"High five!"
end
end
iex> MacrofyAllTheThings.magic(8)
"That's not odd."
"""
@doc """
Returns true if the integer number is evenly divisible by the divisor.
## Examples
iex> 25 |> divisible_by?(5)
true
"""
defmacro divisible_by?(number, divisor) do
quote do
rem(unquote(number), unquote(divisor)) == 0
end
end
@doc """
Returns true for even integers.
## Examples
iex> even? 7
false
"""
defmacro even?(number) do
quote do
divisible_by?(unquote(number), 2)
end
end
@doc """
Returns true for odd integers.
## Examples
iex> odd? 5
true
"""
defmacro odd?(number) do
quote do
not even?(unquote(number))
end
end
require_warning = fn(module) ->
unless module == Kernel, do: "\n\nRemember to `require #{module}` before use."
end
generate_predicates = fn({module, types}) ->
@module String.to_atom "Elixir.#{module}"
types
|> Enum.each(fn(type) ->
@predicate String.to_atom("#{type}?")
@function String.to_atom("is_#{type}")
@doc """
Expands `#{@predicate}(term)` into `#{module}.#{@function}(term)`.
#{require_warning.(@module)}
## Examples
iex> some_#{type}_variable |> #{@predicate}
true
"""
defmacro unquote(@predicate)(term) do
quote do
unquote(@module).unquote(@function)(unquote(term))
end
end
end)
end
@doc """
Returns true if the term is between the low and high values, inclusively.
## Examples
iex> 5 |> within?(2, 10)
true
"""
defmacro within?(term, low, high) do
quote do
unquote(low) <= unquote(term) and unquote(term) <= unquote(high)
end
end
@doc """
Returns true if the term is considered to be a date tuple.
The date is not checked for validity other than the months being in
the 1..12 range and the days being in the 1..31 range.
## Examples
iex> 5 |> date?({2015, 3, 20})
true
"""
defmacro date?(term) do
quote do
is_tuple(unquote(term))
and (tuple_size(unquote(term)) == 3)
and is_integer(elem(unquote(term), 0))
and is_integer(elem(unquote(term), 1))
and (elem(unquote(term), 1) |> within?(1, 12))
and is_integer(elem(unquote(term), 2))
and (elem(unquote(term), 2) |> within?(1, 31))
end
end
@doc """
Returns true if the term is considered to be a time tuple.
The time is not checked for validity other than the hours being in
the 0..23 range, the minutes being in the 0..59 range, and seconds
being in the 0..60 range. The latter due to the existance of leap seconds.
## Examples
iex> 5 |> time?({15, 33, 42})
true
"""
defmacro time?(term) do
quote do
is_tuple(unquote(term))
and (tuple_size(unquote(term)) == 3)
and is_integer(elem(unquote(term), 0))
and (elem(unquote(term), 0) |> within?(0, 23))
and is_integer(elem(unquote(term), 1))
and (elem(unquote(term), 1) |> within?(0, 59))
and is_integer(elem(unquote(term), 2))
and (elem(unquote(term), 2) |> within?(0, 60))
end
end
@doc """
Returns true if the term is considered to be a time tuple.
`date?` and `time?` are used for checking and validating the
date and time portions of the datetime.
## Examples
iex> datetime? {{2015, 3, 20}, {15, 33, 42}}
true
"""
defmacro datetime?(term) do
quote do
is_tuple(unquote(term))
and (tuple_size(unquote(term)) == 2)
and date?(elem(unquote(term), 0))
and time?(elem(unquote(term), 1))
end
end
[
Kernel: ~w(atom binary bitstring boolean float function integer list map nil number pid port reference tuple),
]
|> Enum.each(generate_predicates)
@doc """
Expands `function?(term, arity)` into `is_function(term, arity)`
## Examples
iex> &String.to_integer/1 |> function?(2)
false
"""
defmacro function?(term, arity) do
quote do
is_function(unquote(term), unquote(arity))
end
end
end