defmodule DecimalEnv do
@moduledoc """
This module provides macros to encapsulate `Decimal` operations while using
Elixir arithmetic and comparison operators and functions.
"""
@typedoc """
Type name.
"""
@type type_name ::
nil
| :decimal
| :float
| :integer
| :string
| :scientific
| :xsd
| :raw
@typedoc """
Context.
"""
@type context ::
nil
| keyword()
| Decimal.Context.t()
@typedoc """
Output type.
"""
@type output ::
integer()
| float()
| binary()
| Decimal.t()
@typedoc """
Option.
"""
@type option ::
{:as, type_name()}
| {:context, context()}
@typedoc """
Options.
"""
@type options :: [option()]
@doc """
Runs a block under the `Decimal` environment. All numeric values will be
converted to `Decimal`.
Options:
- `:context` - `Decimal.Context` struct. it can also be a `keyword` list.
- `:as` - In which type we should have the result. The possible values are
the following:
+ `:decimal` (default)
+ `:float`
+ `:integer`
+ `:string`
+ `:xsd`
+ `:raw`
+ `:scientific`
## Examples
iex> import DecimalEnv
iex> decimal do: 42.42
#Decimal<42.42>
iex> decimal do: 84.0 - 21 - "21"
#Decimal<42.0>
iex> decimal as: :scientific do
...> "0.0000000002" + 0.000000004
...> end
"4.2E-9"
iex> decimal context: [precision: 2, rounding: :ceiling], as: :integer do
...> 21.1 + 20
...> end
42
"""
@spec decimal(Macro.t()) :: Macro.t()
@spec decimal(options(), Macro.t()) :: Macro.t()
defmacro decimal(options \\ [], do: block) do
quote do
Decimal.Context.with(
DecimalEnv.get_context(unquote(options[:context])),
fn ->
use DecimalEnv.Operators
result = DecimalEnv.Operators.+(unquote(block))
DecimalEnv.convert_type(result, unquote(options[:as]))
end
)
end
end
#########
# Helpers
@doc false
@spec get_context(nil | keyword() | Decimal.Context.t()) ::
Decimal.Context.t()
def get_context(context)
def get_context(nil) do
Decimal.Context.get()
end
def get_context(options) when is_list(options) do
options =
Decimal.Context.get()
|> Map.from_struct()
|> Map.to_list()
|> Keyword.merge(options)
struct(Decimal.Context, options)
end
def get_context(%Decimal.Context{} = context) do
context
end
@doc false
@spec convert_type(Decimal.t(), type_name()) :: output()
def convert_type(decimal, type)
def convert_type(decimal, :decimal), do: decimal
def convert_type(decimal, :float), do: Decimal.to_float(decimal)
def convert_type(decimal, :integer), do: Decimal.to_integer(decimal)
def convert_type(decimal, :string), do: Decimal.to_string(decimal, :normal)
def convert_type(decimal, :scientific), do: Decimal.to_string(decimal)
def convert_type(decimal, :xsd), do: Decimal.to_string(decimal, :xsd)
def convert_type(decimal, :raw), do: Decimal.to_string(decimal, :raw)
def convert_type(decimal, _), do: decimal
end