Current section
Files
Jump to
Current section
Files
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(expr) do
eval(expr, %{})
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.eval(expr, scope) do
{:ok, result} -> result
{:error, error} -> raise error
end
end
def eval(expr, scope) when is_binary(expr) or is_bitstring(expr) do
with {:ok, parsed} <- parse(expr) do
eval(parsed, scope)
end
end
def eval(expr, scope) do
Abacus.Tree.reduce(expr, &Abacus.Eval.eval(&1, scope))
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 parse(expr) do
{:ok, expr} ->
format(expr)
{:error, _} = error -> error
end
end
def format(expr) do
try do
{:ok, Abacus.Format.format(expr)}
rescue
error -> {:error, error}
end
end
@spec parse(expr :: String.t | charlist) :: {:ok, expr::tuple} | {:error, error::map}
@doc """
Parses the given `expr` to a syntax tree.
"""
def parse(expr) do
with {:ok, tokens} <- lex(expr) do
:math_term_parser.parse(tokens)
else
{:error, error, _} -> {:error, error}
{:error, error} -> {:error, error}
end
end
def variables(expr) do
Abacus.Tree.reduce(expr, fn
{:access, variables} ->
res = Enum.map(variables, fn
{:variable, var} -> var
{:index, index} -> variables(index)
end)
|> List.flatten
|> Enum.uniq
{:ok, res}
{_operator, a, b, c} ->
res = Enum.concat([a, b, c])
|> Enum.uniq
{:ok, res}
{_operator, a, b} ->
res = Enum.concat(a, b)
|> Enum.uniq
{:ok, res}
{_operator, a} -> a
_ -> {:ok, []}
end)
end
defp lex(string) when is_binary(string) do
string
|> String.to_charlist
|> lex
end
defp lex(string) do
with {:ok, tokens, _} <- :math_term.string(string) do
{:ok, tokens}
end
end
end