Packages

Mathematical expression parser and evaluator in Elixir. Tags: math, parse, parser, eval

Current section

Files

Jump to
abacus lib abacus.ex
Raw

lib/abacus.ex

defmodule Abacus do
@moduledoc """
A math-expression parser, evaluator and formatter for Elixir.
## Features
### Supported operators
- `+`, `-`, `/`, `*`
- Exponentials with `^`
- Factorial (`n!`)
- Bitwise operators
* `<<` `>>` bitshift
* `&` bitwise and
* `|` bitwise or
* `|^` bitwise xor
* `~` bitwise not (unary)
- Boolean operators
* `&&`, `||`, `not`
* `==`, `!=`, `>`, `>=`, `<`, `<=`
* Ternary `condition ? if_true : if_false`
### Supported functions
- `sin(x)`, `cos(x)`, `tan(x)`
- `round(n, precision = 0)`, `ceil(n, precision = 0)`, `floor(n, precision = 0)`
### Reserved words
- `true`
- `false`
- `null`
### Access to variables in scope
- `a` with scope `%{"a" => 10}` would evaluate to `10`
- `a.b` with scope `%{"a" => %{"b" => 42}}` would evaluate to `42`
- `list[2]` with scope `%{"list" => [1, 2, 3]}` would evaluate to `3`
### Data types
- Boolean: `true`, `false`
- None: `null`
- Integer: `0`, `40`, `-184`
- Float: `0.2`, `12.`, `.12`
- String: `"Hello World"`, `"He said: \"Let's write a math parser\""`
If a variable is not in the scope, `eval/2` will result in `{:error, error}`.
"""
@doc """
Evaluates the given expression with no scope.
If `expr` is a string, it will be parsed first.
"""
@spec eval(expr::tuple | charlist | String.t) :: {:ok, result::number} | {:error, error::map}
@spec eval(expr::tuple | charlist | String.t, scope::map) :: {:ok, result::number} | {:error, error::map}
@spec eval!(expr::tuple | charlist | String.t) :: result::number
@spec eval!(expr::tuple | charlist | String.t, scope::map) :: result::number
def eval(source, scope \\ [], vars \\ %{})
def eval(source, scope, _vars) when is_binary(source) or is_bitstring(source) do
with {:ok, ast, vars} <- compile(source) do
eval(ast, scope, vars)
end
end
def eval(expr, scope, vars) do
scope = Abacus.Runtime.Scope.prepare_scope(scope, vars)
try do
case Code.eval_quoted(expr, scope) do
{result, _} -> {:ok, result}
end
rescue
e -> {:error, e}
catch
e -> {:error, e}
end
end
@doc """
Evaluates the given expression.
Raises errors when parsing or evaluating goes wrong.
"""
def eval!(expr) do
eval!(expr, %{})
end
@doc """
Evaluates the given expression with the given scope.
If `expr` is a string, it will be parsed first.
"""
def eval!(expr, scope) do
case Abacus.eval(expr, scope) do
{:ok, result} -> result
{:error, error} -> raise error
end
end
def compile(source) when is_binary(source) do
source
|> String.to_charlist()
|> compile()
end
def compile(source) when is_list(source) do
with _ = Process.put(:variables, %{}),
{:ok, tokens, _} <- :math_term.string(source),
{:ok, ast} <- :new_parser.parse(tokens) do
vars = Process.get(:variables)
ast = Abacus.Compile.post_compile(ast, vars)
Process.delete(:variables)
{:ok, ast, vars}
end
end
@spec format(expr :: tuple | String.t | charlist) :: {:ok, String.t} | {:error, error::map}
@doc """
Pretty-prints the given expression.
If `expr` is a string, it will be parsed first.
"""
def format(expr) when is_binary(expr) or is_bitstring(expr) do
case compile(expr) do
{:ok, expr, _vars} ->
format(expr)
{:error, _} = error -> error
end
end
def format(expr) do
try do
{:ok, Abacus.Format.format(expr)}
rescue
error -> {:error, error}
end
end
end