Current section
Files
Jump to
Current section
Files
lib/exun.ex
defmodule Exun do
alias Exun.Cyclic
import Exun.Collect
alias Exun.Eq
alias Exun.UI
@moduledoc """
Symbolic Math for Elixir, with Units support
"""
@doc ~S"""
Parse a math expression, not a 'equality', with context definitions
For example, express 'x' squared meters, and then define x to be 3.
```
iex> Exun.parse( "x[m^2]", %{"x"=>"3"})
{{:unit, {:vari, "x"}, {:elev, {:vari, "m"}, {:numb, 2}}}, %{{:vari, "x"} => {:numb, 3}}}
```
returns a tuple {expression, parsed_context} where
expression is a tuple that holds math AST and
parsed_context is a map whith all equalities (definitions) parsed as
"name" => expression
See parse_txt for an explanation
"""
def parse(txt, context \\ %{}) do
case Cyclic.check(context) do
{:ok, _deps} ->
tree = parse_text(txt)
{tree,
for {func, defi} <- context, into: %{} do
{parse_text(func), parse_text(defi)}
end}
{:err, msg} ->
throw(msg)
{:err, msg, _lst} ->
throw(msg)
end
end
@doc """
Parse and evaluate an expression. If ast is true returns de AST tuple,
if it is false return a human-readable (and parseable) expression.
```
iex> Exun.eval "x[m^2]+4[cm^2]",%{"x"=>"3"}
"3.0004[m^2]"
```
"""
def eval(txt, context \\ %{}) do
eval_ast(txt, context)
|> UI.tostr()
end
@doc """
Same as eval but returns AST
"""
def eval_ast(txt, context \\ %{}) do
{ast, pctx} = parse(txt, context)
# |> IO.inspect(label: "ast and pctx")
ast_eval(ast, pctx)
end
@doc """
Eval an ast (not text expression)
"""
def ast_eval(ast, pctx \\ %{}) do
case ast do
{:error, {line, _app, list}} ->
throw("Error line:#{line} #{list}")
_ ->
# First Collect context
pctx =
for {k, v} <- pctx, into: %{} do
{k, coll(v)}
end
ast
# |> IO.inspect(label: "eval01,AST")
|> replace(pctx)
# |> IO.inspect(label: "eval02,Replaced")
|> coll()
# |> IO.inspect(label: "eval03,mkrec")
end
end
@doc """
Parses an expression in text format. Special symbols used:
- An Unit: <number>[UnitExpression] : 1[m/s]
- A multiplication, division, suma, rest and parenthesis works in the usual way: 2**x-3**(y-2)
- Function call: fname(args): sin(x)
- A derivate: <expression>'<var> : sin(x)'x. Symbol (') has the greatest priority, for example
y^x'x is equals to y^(x'x). Use () to be clear: (y^x)'x
- An integral: $<expression>,<var>: $tan(x),x
- You can sum units: 1[m]+1[cm] if are of the same magnitude. For example 1[m]+1[s] will
throw an exception. You can multiple/divide any two unis.
"""
@spec parse_text(binary) :: any
def parse_text(txt) do
with {:ok, toks, _} <- :exun_lex.string(txt |> String.to_charlist()),
{:ok, tree} <- :exun_yacc.parse(toks) do
tree
end
end
@doc """
Replace definitions in ParsedContext (pc) in tree
"""
def replace(tree, pc) do
newtree = repl(tree, pc)
if not Eq.eq(tree, newtree) do
replace(newtree, pc)
else
newtree
end
end
defp repl(tree, pc) do
case tree do
{:vari, var} ->
Map.get(pc, {:vari, var}, {:vari, var})
{:minus, a} ->
{:minus, repl(a, pc)}
{:fcall, name, args} ->
args = Enum.map(args, &repl(&1, pc))
arity = length(args)
user_function =
Map.keys(pc)
# |> IO.inspect(label: "user_function1")
|> Enum.filter(fn el -> elem(el, 0) == :fcall end)
# |> IO.inspect(label: "user_function2")
|> Enum.filter(fn el -> elem(el, 1) == name and length(elem(el, 2)) == arity end)
# |> IO.inspect(label: "user_function3")
cond do
length(user_function) > 1 ->
{_, dupe_name, _} = user_function |> List.first()
throw("Multiple definition for function #{dupe_name}")
length(user_function) == 1 ->
key = {:fcall, _, args_names} = user_function |> List.first()
ast = pc[key]
nv =
List.zip([args_names, args])
|> Enum.reduce(%{}, fn {n, v}, ac ->
Map.put(ac, n, v)
end)
replace_args(ast, nv)
true ->
{:fcall, name, args}
end
{{:m, op}, lst} ->
{{:m, op}, Enum.map(lst, &replace(&1, pc))}
{op, l, r} ->
{op, replace(l, pc), replace(r, pc)}
_ ->
tree
end
end
defp replace_args(ast, nv) do
case ast do
{:vari, v} ->
Map.get(nv, {:vari, v}, {:vari, v})
{:unit, un, ut} ->
{:unit, replace_args(un, nv), replace_args(ut, nv)}
{:fcall, subname, subargs} ->
{:fcall, subname, subargs |> Enum.map(&replace_args(&1, nv))}
{op, l, r} ->
{op, replace_args(l, nv), replace_args(r, nv)}
other ->
other
end
end
end