├── test ├── test_helper.exs └── expr_test.exs ├── .gitignore ├── lib ├── expr │ ├── constants.ex │ ├── stack.ex │ ├── ops.ex │ └── parser.ex └── expr.ex ├── mix.exs ├── config └── config.exs ├── LICENSE └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /lib/expr/constants.ex: -------------------------------------------------------------------------------- 1 | defmodule Expr.Constants do 2 | 3 | def constants do 4 | %{"pi" => :math.pi, 5 | "e" => 2.718281828459045} 6 | end 7 | 8 | end -------------------------------------------------------------------------------- /lib/expr/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Expr.Stack do 2 | 3 | def push(stack, e), do: [e|stack] 4 | def pop([]), do: {nil, nil} 5 | def pop([e|stack]), do: {e, stack} 6 | 7 | end -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExCalc.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :expr, 6 | version: "0.1.0", 7 | elixir: "~> 1.0", 8 | description: description, 9 | package: package] 10 | end 11 | 12 | defp description do 13 | """ 14 | An Elixir library for parsing and evaluating mathematical 15 | expressions 16 | """ 17 | end 18 | 19 | defp package do 20 | [licenses: ["MIT"], 21 | contributors: ["Robbie D."], 22 | links: %{"GitHub" => "https://github.com/Rob-bie/Expr"}] 23 | end 24 | 25 | def application do 26 | [applications: [:logger]] 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/expr.ex: -------------------------------------------------------------------------------- 1 | defmodule Expr do 2 | 3 | import Expr.Stack 4 | import Expr.Ops 5 | 6 | def eval!(expr) do 7 | expr 8 | |> Expr.Parser.expressionify 9 | |> calculate 10 | end 11 | 12 | def eval!(expr, vars) do 13 | expr 14 | |> Expr.Parser.expressionify(vars) 15 | |> calculate 16 | end 17 | 18 | def calculate(rpn), do: calculate(rpn, []) 19 | def calculate([], stk), do: stk 20 | 21 | def calculate(rpn, stk) do 22 | {top, stack} = pop(rpn) 23 | cond do 24 | is_number(top) -> calculate(stack, push(stk, top)) 25 | true -> 26 | func = oprs[top].f 27 | cond do 28 | is_function(func, 2) -> 29 | {{o, _}, {ox, st}} = {pop(stk), elem(pop(stk), 1) |> pop} 30 | calculate(stack, push(st, func.(ox, o))) 31 | is_function(func, 1) -> 32 | {o, st} = pop(stk) 33 | calculate(stack, push(st, func.(o))) 34 | end 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robbie D. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/expr/ops.ex: -------------------------------------------------------------------------------- 1 | defmodule Expr.Ops do 2 | 3 | alias __MODULE__ 4 | 5 | defstruct p: nil, f: nil, a: :r 6 | 7 | def oprs do 8 | %{"#" => %Ops{:p => 4, :f => &(&1 * - 1)}, 9 | "!" => %Ops{:p => 4, :f => &fact/1}, 10 | "abs" => %Ops{:p => 4, :f => &abs/1}, 11 | "sin" => %Ops{:p => 4, :f => &:math.sin/1}, 12 | "cos" => %Ops{:p => 4, :f => &:math.cos/1}, 13 | "tan" => %Ops{:p => 4, :f => &:math.tan/1}, 14 | "log" => %Ops{:p => 4, :f => &:math.log/1}, 15 | "sqrt" => %Ops{:p => 4, :f => &:math.sqrt/1}, 16 | "atan" => %Ops{:p => 4, :f => &:math.atan/1}, 17 | "acos" => %Ops{:p => 4, :f => &:math.acos/1}, 18 | "asin" => %Ops{:p => 4, :f => &:math.asin/1}, 19 | "ceil" => %Ops{:p => 4, :f => &Float.ceil/1}, 20 | "floor" => %Ops{:p => 4, :f => &Float.floor/1}, 21 | "log10" => %Ops{:p => 4, :f => &:math.log10/1}, 22 | "^" => %Ops{:p => 4, :f => &:math.pow/2}, 23 | "*" => %Ops{:p => 3, :f => &(&1 * &2), :a => :l}, 24 | "/" => %Ops{:p => 3, :f => &(&1 / &2), :a => :l}, 25 | "%" => %Ops{:p => 3, :f => &fmod/2, :a => :l}, 26 | "+" => %Ops{:p => 2, :f => &(&1 + &2), :a => :l}, 27 | "-" => %Ops{:p => 2, :f => &(&1 - &2), :a => :l}} 28 | end 29 | 30 | def fact(0.0), do: 1.0 31 | def fact(0), do: 1 32 | 33 | def fact(n) do 34 | {i, dec} = to_string(n) |> Integer.parse 35 | case {i, dec} do 36 | {_, ".0"} -> fact(i, 1) 37 | {_, ""} -> fact(i, 1) 38 | _ -> :bad_arg 39 | end 40 | end 41 | 42 | def fact(0, acc), do: acc 43 | def fact(n, acc), do: fact(n - 1, acc * n) 44 | 45 | def fmod(n, denom), do: n - denom * Float.floor( n / denom ) 46 | 47 | end 48 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # expr 2 | 3 | expr is an Elixir library for parsing and evaluating mathematical expressions. 4 | 5 | - [expr](#expr) 6 | - [Installation](#installation) 7 | - [Examples](#examples) 8 | - [Operators and constants](#operators-and-constants) 9 | - [Basic examples](#basic-examples) 10 | - [Variable usage](#variable-usage) 11 | - [Caveats](#caveats) 12 | - [TODO](#todo) 13 | - [License](#license) 14 | 15 | 16 | 17 | ## Installation 18 | 19 | Add Expr to your ```mix.exs``` dependencies: 20 | 21 | ```elixir 22 | def deps do 23 | [{:expr, "~> 0.1.0"}] 24 | end 25 | ``` 26 | 27 | Afterwards, run the command ```mix deps.get``` and you're good to go. 28 | 29 | ## Examples 30 | 31 | ##### Operators and constants 32 | 33 | | Operator |Precedence | Associativity | 34 | | :-------------: | :-------------: | :------------: | 35 | | log10 | 4 | RIGHT | 36 | | floor | 4 | RIGHT | 37 | | ceil | 4 | RIGHT | 38 | | asin | 4 | RIGHT | 39 | | acos | 4 | RIGHT | 40 | | atan | 4 | RIGHT | 41 | | sqrt | 4 | RIGHT | 42 | | log | 4 | RIGHT | 43 | | tan | 4 | RIGHT | 44 | | cos | 4 | RIGHT | 45 | | sin | 4 | RIGHT | 46 | | abs | 4 | RIGHT | 47 | | ! | 4 | RIGHT | 48 | | # | 4 | RIGHT | 49 | | ^ | 4 | RIGHT | 50 | | / | 3 | LEFT | 51 | | * | 3 | LEFT | 52 | | + | 2 | LEFT | 53 | | - | 2 | LEFT | 54 |
55 | 56 | | Symbol | Value | 57 | | :-------------: | :-------------: | 58 | | pi | 3.14 ... | 59 | | e | 2.71 ...| 60 | 61 | ##### Basic examples 62 | 63 | The result of all evaluations are returned at the front of a list. As of version ```0.1.0``` there is no error checking or expression validation. If something goes wrong, an arithmetic error will be thrown. While the parser will understand your intent for most expressions, the more explicit you are, the better. (Be liberal with parenthesis if you're getting unexpected results!) 64 | 65 | ```elixir 66 | Expr.eval!("1 + 2 * 3") 67 | => [7.0] 68 | 69 | Expr.eval!("5/2") 70 | => [2.5] 71 | 72 | Expr.eval!("5! + abs(-5)") 73 | => [125.0] 74 | 75 | Expr.eval!("--4") #Negation 76 | => [4.0] 77 | 78 | Expr.eval!("5(sqrt(abs(-16)))") #Implicit multiplication 79 | => [20.0] 80 | 81 | Expr.eval!("(5^2)(floor(2.75))") #Implicit multiplication #2 82 | => [50.0] 83 | 84 | Expr.eval!("sin(60)") #RADIANS! 85 | => [-0.3048106211022167] 86 | ``` 87 | 88 | ##### Variable usage 89 | 90 | Valid variable names cannot contain operator names, constant names or start with numbers. Starting with capital letters, containing numbers and unused symbols are fine. However, I recommend using short, lowercase variable names. A map of variable names and their values are passed to ```Expr.eval!``` along with an expression when evaluating expressions that contain variables. 91 | 92 | ```elixir 93 | Expr.eval!("x + y / x", %{"x" => 2, "y" => 4}) 94 | => [3.0] 95 | 96 | Expr.eval!("-x@ + 1", %{"x@" => 2}) 97 | => [-1.0] 98 | 99 | Expr.eval!("-(sqrt(abs(some_var)))", %{"some_var" => -2.5}) 100 | => [-1.5811388300841898] 101 | 102 | Expr.eval!("ABC+2^CBA", %{"ABC" => 2, "CBA" => 3}) 103 | => [10.0] 104 | 105 | vars = %{"a" => 2.5, "b" => 3, "c" => 0.25, "d" => 10, "z" => 6.5} 106 | Expr.eval!("a^(b+c)-d(z)", vars) 107 | => [-45.35260266120413] 108 | ``` 109 | 110 | ##### Caveats 111 | 112 | - Technically you can have a variable name that is the same as a constant name, however the constant's value will override the value of the variable. 113 | 114 | ```Expr.eval!("pi + 1", %{"pi" => 1}) => 4.141592...``` 115 | 116 | 117 | - Sometimes a malformed expression will actually evaluate and return a list of length ```>1```, whenever this happens know something has gone wrong. 118 | 119 | 120 | - Complex numbers are not supported, it's not uncommon that you'll have a properly formed expression and somewhere during the calculation an arithmetic error will be thrown. For example, trying to take the square root of a negative number. 121 | 122 | 123 | - Negation is represented by ```#``` under the hood, ```#``` can be used in expressions. 124 | 125 | 126 | - Implicit multiplication is currently only *partially* supported. While ```pi(r^2)``` is entirely valid, ```pir^2``` is unfortunately not. This may be coming in a future version. 127 | 128 | 129 | ## TODO 130 | 131 | - Function support for functions with arity ```>2``` 132 | - More operator/constant support 133 | - Expression validator 134 | - Improved implicit multiplication 135 | - Clean up messy parser code 136 | - Comments. 137 | 138 | ## License 139 | 140 | ``` 141 | This work is free. You can redistribute it and/or modify it under the 142 | terms of the MIT License. See the LICENSE file for more details. 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /lib/expr/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Expr.Parser do 2 | 3 | import Expr.Constants 4 | import Expr.Stack 5 | import Expr.Ops 6 | 7 | def expressionify(expr) do 8 | expr 9 | |> lex 10 | |> conv 11 | |> format 12 | |> parse 13 | end 14 | 15 | def expressionify(expr, vars) do 16 | expr 17 | |> lex 18 | |> conv(vars) 19 | |> format 20 | |> parse 21 | end 22 | 23 | def lex(input) do 24 | regex = ~r/()[\+|)|\(|\-|\*|\/|^|!|]()/ 25 | 26 | String.split(input) 27 | |> Enum.map(&Regex.split(regex, &1, [on: [1,2], trim: true])) 28 | |> List.flatten 29 | end 30 | 31 | def parse(tokens), do: parse(tokens, {[], []}) 32 | def parse([], {[], rpn}), do: Enum.reverse(rpn) 33 | def parse([], {[o|t], rpn}), do: parse([], {t, [o|rpn]}) 34 | 35 | def parse([e|t], {ops, rpn}) when is_number(e) do 36 | parse(t, {ops, push(rpn, e)}) 37 | end 38 | 39 | def parse([e|t], {ops, rpn}) when e == "(" do 40 | parse(t, {push(ops, e), rpn}) 41 | end 42 | 43 | def parse([e|t], {ops, rpn}) when e == ")" do 44 | parse(t, empty_parens({ops, rpn})) 45 | end 46 | 47 | def parse([e|t], {ops, rpn}) do 48 | {top, _} = pop(ops) 49 | a = oprs[e].a 50 | cond do 51 | top == nil or top == "(" -> parse(t, {push(ops, e), rpn}) 52 | a == :r and oprs[e].p >= oprs[top].p -> 53 | parse(t, {push(ops, e), rpn}) 54 | oprs[e].p > oprs[top].p -> parse(t, {push(ops, e), rpn}) 55 | true -> parse(t, shift(e, {ops, rpn})) 56 | end 57 | end 58 | 59 | def shift(e, {ops, rpn}) do 60 | {top, stack} = pop(ops) 61 | cond do 62 | top == nil or top == "(" -> {push(ops, e), rpn} 63 | oprs[top].p >= oprs[e].p -> shift(e, {stack, push(rpn, top)}) 64 | true -> {push(ops, e), rpn} 65 | end 66 | end 67 | 68 | def empty_parens({ops, rpn}) do 69 | {top, stack} = pop(ops) 70 | case top do 71 | "(" -> {stack, rpn} 72 | nil -> {[], rpn} 73 | _ -> empty_parens({stack, push(rpn, top)}) 74 | end 75 | end 76 | 77 | def format(tokens), do: format(Enum.with_index(tokens), tokens, []) 78 | def format([], _, acc), do: Enum.reverse(acc) 79 | def format([{"-", 0}|t], copy, acc), do: format(t, copy, ["#"|acc]) 80 | 81 | def format([{"-", i}|t], copy, acc) do 82 | {prev, next} = if i == 0 do 83 | {nil, Enum.at(copy, i + 1)} 84 | else 85 | {Enum.at(copy, i - 1), Enum.at(copy, i + 1)} 86 | end 87 | 88 | cond do 89 | is_number(prev) and is_number(next) -> 90 | format(t, copy, ["-"|acc]) 91 | prev == "!" -> 92 | format(t, copy, ["-"|acc]) 93 | prev == ")" and next == "(" -> 94 | format(t, copy, ["-"|acc]) 95 | prev == "-" and next == "-" -> 96 | format(t, copy, ["#"|acc]) 97 | prev == "(" and next == "-" -> 98 | format(t, copy, ["#"|acc]) 99 | prev == ")" and is_number(next) or next == "-" -> 100 | format(t, copy, ["-"|acc]) 101 | is_number(next) -> format(tl(t), copy, [next * -1|acc]) 102 | next == "(" -> format(t, copy, ["#"|acc]) 103 | true -> format(t, copy, ["#"|acc]) 104 | end 105 | end 106 | 107 | def format([{"(", 0}|t], copy, acc), do: format(t, copy, ["("|acc]) 108 | 109 | def format([{"(", i}|t], copy, acc) do 110 | prev = Enum.at(copy, i - 1) 111 | empty? = if i - 2 < 0 do 112 | nil 113 | else 114 | Enum.at(copy, i - 2) != "(" 115 | end 116 | 117 | cond do 118 | is_number(prev) -> format(t, copy, ["("|["*"|acc]]) 119 | prev == ")" and empty? -> format(t, copy, ["("|["*"|acc]]) 120 | true -> format(t, copy, ["("|acc]) 121 | end 122 | end 123 | 124 | def format([{token, _}|t], copy, acc) do 125 | format(t, copy, [token|acc]) 126 | end 127 | 128 | def conv(input), do: conv(input, [], %{}) 129 | def conv(input, vars), do: conv(input, [], vars) 130 | def conv([], acc, _), do: Enum.reverse(acc) 131 | 132 | def conv([token|t], acc, vars) do 133 | is_num? = num_parse(token) 134 | constant? = constants[token] 135 | variable? = vars[token] 136 | cond do 137 | is_num? != :error -> conv(t, [elem(is_num?, 0)|acc], vars) 138 | constant? != nil -> conv(t, [constant?|acc], vars) 139 | variable? != nil -> conv(t, [variable?|acc], vars) 140 | true -> conv(t, [token|acc], vars) 141 | end 142 | end 143 | 144 | defp num_parse(token) do 145 | case Integer.parse(token) do 146 | {int, ""} -> 147 | {int, ""} 148 | _ -> 149 | Float.parse(token) 150 | end 151 | end 152 | 153 | end 154 | -------------------------------------------------------------------------------- /test/expr_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExprTest do 2 | 3 | use ExUnit.Case 4 | 5 | test "Stack.push" do 6 | stack = [1, 2, 3] 7 | assert Expr.Stack.push(stack, 0) == [0, 1, 2, 3] 8 | end 9 | 10 | test "Stack.pop" do 11 | stack = [1, 2, 3] 12 | assert Expr.Stack.pop(stack) == {1, [2, 3]} 13 | end 14 | 15 | test "Stack.pop (Empty)" do 16 | stack = [] 17 | assert Expr.Stack.pop(stack) == {nil, nil} 18 | end 19 | 20 | test "Lex expr: 1+2/3" do 21 | expr = "1+2/3" 22 | result = ["1", "+", "2", "/", "3"] 23 | 24 | assert Expr.Parser.lex(expr) == result 25 | end 26 | 27 | test "Lex expr: ( (2^4 + 3) - 1)" do 28 | expr = "( (2^4 + 3) - 1)" 29 | result = ["(", "(", "2", "^", "4", "+", "3", ")", 30 | "-", "1", ")"] 31 | 32 | assert Expr.Parser.lex(expr) == result 33 | end 34 | 35 | test "Format and expand expression" do 36 | expr = "5(1 + 2)" 37 | result = [5.0, "*", "(", 1.0, "+", 2.0, ")"] 38 | 39 | assert Expr.Parser.lex(expr) 40 | |> Expr.Parser.conv 41 | |> Expr.Parser.format == result 42 | end 43 | 44 | test "Format and expand expression #2" do 45 | expr = "-5(1 + -2)" 46 | result = ["#", 5.0, "*", "(", 1.0, "+", -2.0, ")"] 47 | 48 | assert Expr.Parser.lex(expr) 49 | |> Expr.Parser.conv 50 | |> Expr.Parser.format == result 51 | end 52 | 53 | test "Format and expand expression (complex)" do 54 | expr = " -( 1 + -2)^2 * -5(cos(60)/2.5^ 3)+log(1) " 55 | result = ["#", "(", 1.0, "+", -2.0, ")", "^", 56 | 2.0, "*", -5.0, "*", "(", "cos", "(", 57 | 60.0, ")", "/", 2.5, "^", 3.0, ")", "+", 58 | "log", "(", 1.0, ")"] 59 | 60 | assert Expr.Parser.lex(expr) 61 | |> Expr.Parser.conv 62 | |> Expr.Parser.format == result 63 | end 64 | 65 | test "fmod returns the floating-point remainder of the division x/y" do 66 | {x, y} = { 5.1, 3.0 } 67 | result = 2.1 68 | precision = 15 #avoid hardcoding higly precision float 69 | 70 | assert Expr.Ops.fmod(x, y)|>Float.round(precision) == result 71 | end 72 | 73 | test "1 + 1 -> postfix" do 74 | expr = "1 + 1" 75 | result = [1.0, 1.0, "+"] 76 | 77 | assert Expr.Parser.expressionify(expr) == result 78 | end 79 | 80 | test "Infix expression -> postfix" do 81 | expr = "()(( 1 + 1) + 3 ) * 5 ^ 6" 82 | result = [1.0, 1.0, "+", 3.0, "+", 5.0, 6.0, "^", "*"] 83 | 84 | assert Expr.Parser.expressionify(expr) == result 85 | end 86 | 87 | test "Eval empty expression" do 88 | expr = "" 89 | result = [] 90 | 91 | assert Expr.eval!(expr) == result 92 | end 93 | 94 | test "Eval single value expression" do 95 | expr = "5" 96 | result = [5.0] 97 | 98 | assert Expr.eval!(expr) == result 99 | end 100 | 101 | test "Eval single value expression (neg)" do 102 | expr = "-5" 103 | result = [-5.0] 104 | 105 | assert Expr.eval!(expr) == result 106 | end 107 | 108 | test "Eval basic expression" do 109 | expr = "1 + 0 + 3 - 4" 110 | assert Expr.eval!(expr) == [0.0] 111 | end 112 | 113 | test "Eval factorial expression" do 114 | expr = "5! + 3.5 * 2" 115 | result = [127.0] 116 | 117 | assert Expr.eval!(expr) == result 118 | end 119 | 120 | test "Eval modulo expression" do 121 | expr = "9 % 2" 122 | result = [1.0] 123 | 124 | assert Expr.eval!(expr) == result 125 | end 126 | 127 | test "Eval trig expression" do 128 | expr = "cos(90) / (2^-2)" 129 | result = [:math.cos(90) / :math.pow(2, -2)] 130 | 131 | assert Expr.eval!(expr) == result 132 | end 133 | 134 | test "Eval exp expression (Right assoc)" do 135 | expr = "2^3^4" 136 | result = [2.4178516392292583e24] 137 | 138 | assert Expr.eval!(expr) == result 139 | end 140 | 141 | test "Eval exp expression (Forced left assoc)" do 142 | expr = "(2^3)^4" 143 | result = [4096.0] 144 | 145 | assert Expr.eval!(expr) == result 146 | end 147 | 148 | test "Eval negation expression" do 149 | expr = "-(5 + 8)" 150 | result = [-13.0] 151 | 152 | assert Expr.eval!(expr) == result 153 | end 154 | 155 | test "Eval negation expression #2" do 156 | expr = "--(5 + 8)" 157 | result = [13.0] 158 | 159 | assert Expr.eval!(expr) == result 160 | end 161 | 162 | test "Eval negation expression #3" do 163 | expr = "--(---5)" 164 | result = [-5.0] 165 | 166 | assert Expr.eval!(expr) == result 167 | end 168 | 169 | test "Eval negation expression #4" do 170 | expr = "--5---(---5)" 171 | result = [10.0] 172 | 173 | assert Expr.eval!(expr) == result 174 | end 175 | 176 | test "Eval implicit multiplication expression" do 177 | expr = "5(sqrt(16))" 178 | result = [20.0] 179 | 180 | assert Expr.eval!(expr) == result 181 | end 182 | 183 | test "Eval implicit multiplication expression #2" do 184 | expr = "(abs(-5))(2^3)" 185 | result = [40.0] 186 | 187 | assert Expr.eval!(expr) == result 188 | end 189 | 190 | test "Eval rounding expression" do 191 | expr = "ceil(5.2) + floor(4.9)" 192 | result = [10.0] 193 | 194 | assert Expr.eval!(expr) == result 195 | end 196 | 197 | test "Eval complex expression" do 198 | expr = "((5^2 + 5) / 6^3 + 3 * 4)^2/4+(3- 1 * 2)" 199 | result = [37.83815586419753] 200 | 201 | assert Expr.eval!(expr) == result 202 | end 203 | 204 | test "Eval complex expression #2" do 205 | expr = "5(sqrt(9) + abs(-5)) / 9! - 3" 206 | result = [-2.999889770723104] 207 | 208 | assert Expr.eval!(expr) == result 209 | end 210 | 211 | test "Eval complex expression #3" do 212 | expr = "-2^(sqrt(4) - 2!) / abs(-sin(3.5))" 213 | result = [-2.850763437540464] 214 | 215 | assert Expr.eval!(expr) == result 216 | end 217 | 218 | test "Eval complex expression #4" do 219 | expr = "(4!) / -(sqrt(floor(16.54))) + log10(60)" 220 | result = [-4.221848749616356] 221 | 222 | assert Expr.eval!(expr) == result 223 | end 224 | 225 | test "Eval variable expression #1" do 226 | expr = "(x + y) / z" 227 | vars = %{"x" => 2, "y" => 4, "z" => 3} 228 | result = [2.0] 229 | 230 | assert Expr.eval!(expr, vars) == result 231 | end 232 | 233 | test "Eval variable expression #2" do 234 | expr = "pi(r^2)" 235 | vars = %{"r" => 4} 236 | result = [50.26548245743669] 237 | 238 | assert Expr.eval!(expr, vars) == result 239 | end 240 | 241 | end --------------------------------------------------------------------------------