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() end end @doc """ Parses an expression in text format. Special symbols used: - An Unit: [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: ' : 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: $,: $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 defp 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}) {: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