defmodule DefUnit do
@moduledoc """
DefUnit defines macros used to create type specs and documentation
when working with a "core" set of measurement units, and also defines
operators to convert them to and from other units.
## Example
```
use DefUnit
@doc_to_operator "to SI"
@doc_from_operator "from SI"
# Units calculations are done in
DefUnit.core "m", :m, "SI length"
DefUnit.core "m2", :m2, "SI area"
DefUnit.core "kg", :kg, "SI mass"
DefUnit.core "kgm-3", :kgm3, "SI density"
DefUnit.core "s", :s, "Time"
DefUnit.core "C", :c, "Temperature in Celcius"
DefUnit.core "ms-1", :ms, "SI Velocity"
DefUnit.core "ms-2", :ms2, "SI Acceleration"
DefUnit.core "Nm2", :nm2, "SI Pressure"
# Units we convert to and from above units
DefUnit.other "feet", :feet, 0.3048, :m, "FPS length and altitude"
DefUnit.other "lbs", :lbs, 0.453592, :kg, "FPS mass"
# Units with more complex from/to conversion calculations
DefUnit.other "F", :f,
{
&((&1 - 32.0) * (5.0 / 9.0)),
&((&1 * (9.0 / 5.0)) + 32.0)
},
:c, "Temperature in Farhrenheit"
```
"""
defmacro __using__(_options) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__,
:core_units, accumulate: true
Module.register_attribute __MODULE__,
:other_units, accumulate: true
Module.register_attribute __MODULE__,
:doc_to_operator, accumulate: false
Module.register_attribute __MODULE__,
:doc_from_operator, accumulate: false
end
end
@doc ~S"""
Define a 'core' unit.
- `eq` is the short name for the unit used in the typedoc - use <sup> for ordinals
- `core_type` is the name used in the type spec for this unit
- `description` is the description used in the typedoc
"""
defmacro core(eq, core_type, description) do
quote do
@core_units {unquote(eq), unquote(core_type)}
@typedoc unquote(description <> " " <> eq)
@type unquote({core_type, [], nil}) :: float
@doc @doc_from_operator
@spec number <~ unquote(core_type) :: unquote({core_type, [], nil})
def value <~ unquote(core_type) do
value
end
@doc @doc_to_operator
@spec unquote({core_type, [], nil}) ~> unquote(core_type) :: unquote({core_type, [], nil})
def value ~> unquote(core_type) do
value
end
end
end
@doc """
Define an 'other' unit.
- `eq` is the short name for the unit used in the typedoc - use <sup> for ordinals
- `other_type` is the name used in the type spec for this unit
- `ratio` is either a multiplier to convert this unit to the core unit, or a 2-tuple of from/to conversion functions
- `core_type` is the name of the corresponding core type
- `description` is the description used in the typedoc
"""
defmacro other(eq, other_type, ratio, core_type, description) do
name_string = Atom.to_string(other_type)
core_type_string = Atom.to_string(core_type)
to_ratio_name = String.to_atom(name_string <> "_to_" <> core_type_string)
from_ratio_name = String.to_atom(core_type_string <> "_to_" <> name_string)
{from_ratio, from_op} = cond do
is_number(ratio) ->
{
{:@, [], [{from_ratio_name, [], [1.0 / ratio]}]},
quote do: value * unquote(ratio)
}
is_tuple(ratio) ->
{fn_from, _} = ratio
{
{:@, [], [{from_ratio_name, [], [:na]}]},
quote do: (unquote(fn_from)).(value)
}
end
{to_ratio, to_op} = cond do
is_number(ratio) ->
{
{:@, [], [{to_ratio_name, [], [ratio]}]},
quote do: value / unquote(ratio)
}
is_tuple(ratio) ->
{_, fn_to} = ratio
{
{:@, [], [{to_ratio_name, [], [:na]}]},
quote do: (unquote(fn_to)).(value)
}
end
quote do
if length(for {_, ct} <- @core_units, ct == unquote(core_type), do: ct) == 0 do
raise ArgumentError,
message: """
Unit '#{unquote(other_type)}' refers to unknown core unit '#{unquote(core_type)}'
"""
end
@other_units {unquote(eq), unquote(other_type), unquote(core_type)}
@typedoc unquote(description <> " " <> eq)
@type unquote({other_type, [], nil}) :: float
unquote(to_ratio)
unquote(from_ratio)
@spec number <~ unquote(other_type) :: unquote({core_type, [], nil})
def value <~ unquote(other_type) do
unquote(from_op)
end
@spec unquote({core_type, [], nil}) ~> unquote(other_type) :: unquote({other_type, [], nil})
def value ~> unquote(other_type) do
unquote(to_op)
end
end
end
end