Current section

Files

Jump to
exun lib exun_unit.ex
Raw

lib/exun_unit.ex

defmodule Exun.Unit do
alias Exun.Simpl, as: S
alias Exun.Collect, as: C
alias Exun.UI, as: U
@zero {:numb, 0, 1}
@uno {:numb, 1, 1}
@muno {:numb, -1, 1}
@moduledoc """
Handle Units, converts, sum, multiply, transform to SI and factorize
"""
@prefixes %{
# Exa
"E" => 1.0e18,
# Peta
"P" => 1.0e15,
# Tera
"T" => 1.0e12,
# Giga
"G" => 1.0e9,
# Mega
"M" => 1.0e6,
# Kilo
"K" => 1000,
# hecto
"h" => 100,
# deci
"d" => 0.1,
# centi
"c" => 0.01,
# mili
"m" => 0.001,
# micro
"u" => 1.0e-6,
# nano
"n" => 1.0e-9,
# pico
"p" => 1.0e-12,
# femto
"f" => 1.0e-15,
# atto
"a" => 1.0e-18
}
@fundamental_units %{
# gram
"g" => true,
# second
"s" => true,
# candela
"cd" => true,
# Ampere
"A" => true,
# Kelvin
"K" => true,
# mol
"mol" => true,
# meter
"m" => true
}
@conversions %{
"in" => "2.54[cm]",
"pie" => "30.48[cm]",
"mi" => "1609.344[m]",
"yd" => "91.44[cm]",
"ft" => "0.3048[m]",
"pc" => "3.08567818585e16[m]",
"lyr" => "9.46052840488e15[m]",
"au" => "149597900000[m]",
"chain" => "20.1168402337[m]",
"vara" => "83.6[cm]",
"rd" => "5.02921005842[m]",
"fath" => "1.82880365761[m]",
"angstrom" => "1.0e-10[m]",
"a" => "100[m^2]",
"acre" => "4046.87260987[m^2]",
"l" => "100[cm^3]",
"galUK" => "4.546092[l]",
"galC" => "4.54609[l]",
"gal" => "3.785411784[l]",
"qt" => "0.946352946[l]",
"pt" => "0.473176473[l]",
"cu" => "0.2365882365[l]",
"ozfl" => "2.95735295625e-2[l]",
"ozUK" => "28.413075[ml]",
"tbsp" => "14.78676447813[ml]",
"tsp" => "4.92892159375[ml]",
"bbl" => "158987.294928[ml]",
"bu" => "35239.07[ml]",
"pk" => "8809.7675[ml]",
"fbm" => "2359.737216[ml]",
"yr" => "31556925.9747[s]",
"d" => "86400[s]",
"h" => "3600[s]",
"min" => "60[s]",
"Hz" => "1[1/s]",
"lb" => "453.59237[g]",
"arroba" => "25[lb]",
"oz" => "28.349523125[g]",
"slug" => "14.5939029372[Kg]",
"lbt" => "373.2417216[g]",
"ton" => "907.18474[Kg]",
"tonUK" => "1016.0469088[Kg]",
"t" => "1000[Kg]",
"ozt" => "31.1034768[g]",
"ct" => "0.2[g]",
"grain" => "0.06479891[g]",
"u" => "1.6605402e-24[g]",
"N" => "101.9716[g*m/s^2]",
"dyn" => "0.00001[Kg*m/s^2]",
"gf" => "0.00980665[N]",
"kip" => "4448.22161526[N]",
"lbf" => "4.44822161526[N]",
"pdl" => "0.1382549544376[N]",
"J" => "1[Kg*m^2/s^2]",
"erg" => "0.0000001[J]",
"cal" => "4.1868[J]",
"Btu" => "1055.05585262[J]",
"therm" => "105506000[J]",
"eV" => "1.60217733e-19[J]",
"W" => "1[Kg*m^2/s^3]",
"hp" => "745.699871582[W]",
"CV" => "1[hp]",
"Pa" => "1[Kg/m/s^2]",
"atm" => "101325[Pa]",
"bar" => "100000[Pa]",
"psi" => "6894.75729317[Pa]",
"torr" => "133.322368421[Pa]",
"mmHg" => "1[torr]",
"inHg" => "3386.38815789[Pa]",
"inH2O" => "248.84[Pa]"
}
@doc """
Shows help about unit
Known Units prefixes:
E = 1.0e18
G = 1.0e9
K = 1000
M = 1.0e6
P = 1.0e15
T = 1.0e12
a = 1.0e-18
c = 0.01
d = 0.1
f = 1.0e-15
h = 100
m = 0.001
n = 1.0e-9
p = 1.0e-12
u = 1.0e-6
Know fundamental units:
A = true
K = true
cd = true
g = true
m = true
mol = true
s = true
Know conversions, you can add more via 'context'
lb = 453.59237[g]
torr = 133.322368421[Pa]
vara = 83.6[cm]
W = 1[Kg*m^2/s^3]
dyn = 0.00001[Kg*m/s^2]
ozt = 31.1034768[g]
pie = 30.48[cm]
min = 60[s]
ozUK = 28.413075[ml]
hp = 745.699871582[W]
yr = 31556925.9747[s]
acre = 4046.87260987[m^2]
cal = 4.1868[J]
in = 2.54[cm]
rd = 5.02921005842[m]
galUK = 4.546092[l]
J = 1[Kg*m^2/s^2]
pdl = 0.1382549544376[N]
Btu = 1055.05585262[J]
therm = 105506000[J]
u = 1.6605402e-24[g]
h = 3600[s]
qt = 0.946352946[l]
gf = 0.00980665[N]
ft = 0.3048[m]
ozfl = 2.95735295625e-2[l]
lbf = 4.44822161526[N]
Pa = 1[Kg/m/s^2]
cu = 0.2365882365[l]
bu = 35239.07[ml]
bar = 100000[Pa]
l = 100[cm^3]
ton = 907.18474[Kg]
tonUK = 1016.0469088[Kg]
bbl = 158987.294928[ml]
chain = 20.1168402337[m]
pt = 0.473176473[l]
t = 1000[Kg]
inH2O = 248.84[Pa]
pk = 8809.7675[ml]
inHg = 3386.38815789[Pa]
ct = 0.2[g]
mmHg = 1[torr]
N = 101.9716[g*m/s^2]
oz = 28.349523125[g]
galC = 4.54609[l]
pc = 3.08567818585e16[m]
mi = 1609.344[m]
eV = 1.60217733e-19[J]
kip = 4448.22161526[N]
Hz = 1[1/s]
yd = 91.44[cm]
CV = 1[hp]
slug = 14.5939029372[Kg]
fath = 1.82880365761[m]
arroba = 25[lb]
d = 86400[s]
gal = 3.785411784[l]
angstrom = 1.0e-10[m]
au = 149597900000[m]
atm = 101325[Pa]
lyr = 9.46052840488e15[m]
tbsp = 14.78676447813[ml]
tsp = 4.92892159375[ml]
erg = 0.0000001[J]
a = 100[m^2]
grain = 0.06479891[g]
lbt = 373.2417216[g]
fbm = 2359.737216[ml]
psi = 6894.75729317[Pa]
"""
def help do
IO.puts("Known Units prefixes:")
@prefixes
|> Enum.each(fn {k, v} ->
IO.puts("\t#{k} = #{v}")
end)
IO.puts("Know fundamental units:")
@fundamental_units
|> Enum.each(fn {k, v} ->
IO.puts("\t#{k} = #{v}")
end)
IO.puts("Know conversions, you can add more via 'context'")
@conversions
|> Enum.each(fn {k, v} ->
IO.puts("\t#{k} = #{v}")
end)
end
@doc """
Sum or Rest of two units
"""
def sum({:unit, a = {:numb, _, _}, t1}, {:unit, b = {:numb, _, _}, t2}, pcontext \\ %{}) do
{res1, exps1} = to_si2({@uno, t1}, pcontext, @uno, %{})
{res2, exps2} = to_si2({@uno, t2}, pcontext, @uno, %{})
if exps1 != exps2 do
{:err, "Inconsisten units " <> U.tostr(t1) <> " and " <> U.tostr(t2)}
else
val = S.divi(S.suma(S.mult(a, res1), S.mult(b, res2)), res1)
{:ok, {:unit, val, t1}}
end
end
@doc """
Convert first unit to the second unit in convert:
```
iex(3)> Exun.Unit.convert("3[m]","1[cm]",%{}) |> Exum.UI.tostr()
"300[cm]"
```
"""
def convert_ast({:unit, n = {:numb, _, _}, t1}, {:unit, {:numb, _n2, _d2}, t2}, pcontext \\ %{}) do
{res1, exps1} = to_si2({@uno, t1}, pcontext, @uno, %{})
# |> IO.inspect(label: "convert from unit1")
{res2, exps2} = to_si2({@uno, t2}, pcontext, @uno, %{})
# |> IO.inspect(label: "convert to unit2")
if exps1 != exps2 do
{:err, "Inconsisten units " <> U.tostr(t1) <> " and " <> U.tostr(t2)}
else
{:ok, {:unit, S.divi(S.mult(n, res1), res2), t2}}
end
end
@doc """
Convert units, for example
```
iex> import Exun.Unit
iex> "120[Km/h]" |> convert("m/s")
"33.333333333333336[m/s]"
```
"""
def convert(tu1, tu2, pcontext \\ %{}) when is_binary(tu1) and is_binary(tu2) do
u1 = Exun.new(tu1).ast
u2 = Exun.new("1[" <> tu2 <> "]").ast
case convert_ast(u1, u2, pcontext) do
{:err, msg} -> throw(msg)
{:ok, res} -> U.tostr(res)
end
end
@doc """
Factorize unit u2 from u1, for example
```
iex(1)> {u1,d} = Exun.parse "1[km*Kg*A/h^2]", %{}
{u2,d} = Exun.parse "1[N]", %{}
factorized = Exun.Unit.factorize(u1,u2,%{})
Exum.Tree.tostr factorized
"7.566861148315854e-4[N*A]"
```
"""
def factorize_ast({:unit, n = {:numb, _, _}, t1}, {:unit, {:numb, _n2, _d2}, t2}, pcontext) do
# Divide t1 by t2, reduce to si and multiply by t2
{nres, exps} = to_si2({n, S.divi(t1, t2)}, pcontext, @uno, %{})
"#{U.tostr(nres)}[#{U.tostr(t2)}#{exps_tostr(exps)}]"
end
@doc """
Factorize units, as text:
```
factorize("2[N]","[Kg]")
```
"""
def factorize(e1, e2, pcontext \\ %{}) do
u1 = Exun.new(e1, pcontext).ast
u2 = Exun.new("1" <> e2, pcontext).ast
factorize_ast(u1, u2, pcontext)
end
defp to_si2({:unit, n = {:numb, _, _}, tree}, pcontext \\ %{}) do
to_si2({n, tree}, pcontext, @uno, %{})
end
defp to_si2({nn, {{:m, :mult}, lst}}, pcontext, curre, exps) do
Enum.reduce(lst, {nn, exps}, fn opand, {acu_nn, acu_e} ->
{new_n, ne} = to_si2({@uno, opand}, pcontext, curre, acu_e)
{S.mult(acu_nn, new_n), ne}
end)
end
defp to_si2({nn, {:elev, left, nnexp = {:numb, _, _}}}, pcontext, curre, exps) do
{left_n, exps} = to_si2({@uno, left}, pcontext, S.mult(curre, nnexp), exps)
{S.mult(nn, S.elev(left_n, nnexp)), exps}
end
defp to_si2({n, {:vari, var}}, pcontext, curre, exps) do
case @fundamental_units[var] do
nil ->
{:unit, vdef = {:numb, _, _}, tdef} = get_def(var, pcontext)
to_si2({S.mult(n, vdef), tdef}, pcontext, curre, exps)
true ->
x1 = Map.get(exps, var, @zero)
x2 = S.suma(curre, x1)
{n, Map.put(exps, var, x2)}
end
end
defp to_si2({a, b}, _pcontext, _curr_exp, _exps) do
# IO.inspect({:unit, {:numb, a}, b}, label: "For unit:")
throw("Invalid unit definition: " <> U.tostr({:unit, a, b}))
end
defp get_def(name, pcontext) do
[pref | rest] = name |> String.codepoints()
rest = to_string(rest)
cond do
@fundamental_units[name] != nil ->
{:unit, @uno, {:vari, name}}
@conversions[name] != nil ->
Exun.new(@conversions[name]).ast
pcontext[name] != nil ->
pcontext[name]
@prefixes[pref] != nil and byte_size(rest) > 0 ->
pref_val = {:numb, @prefixes[pref], 1}
{:unit, val, tree} = get_def(rest, pcontext)
# IO.inspect(pref_val,label: "pref_val")
# IO.inspect(val,label: "val")
{:unit, S.mult(pref_val, val), tree}
true ->
throw("Undefined unit #{name}")
end
end
defp exps_tostr(exps) do
exps
|> Enum.reduce("", fn {var, exp}, ac ->
cond do
exp == @zero -> ac
exp == @uno -> "#{ac}*#{var}"
exp == @muno -> "#{ac}/#{var}"
S.signof(exp) == false -> "#{ac}/#{var}^#{U.tostr(S.chsign(exp))}"
true -> "#{ac}*#{var}^#{U.tostr(exp)}"
end
end)
end
@doc """
Converts unit to International System
"""
def toSI({:unit, un, {:numb, _, _}}) do
S.mkrec(un)
end
def toSI({:unit, uv, ut}) do
{r, e} = to_si2({:unit, C.coll(uv), C.coll(ut)})
{:unit, r,
C.coll(
e
|> Enum.reject(fn {_a, b} -> b == @zero end)
|> Enum.reduce(@uno, fn {var, expon}, tree ->
S.mult(tree, {:elev, {:vari, var}, expon})
end)
)}
end
end