Calculator: bug fixes for issue #5917, and feature additions (#5918)

* Calculator: bug fixes and feature additions

- Fix order of operations for negation and exponentiation;
- Support non-integer powers;
- Improve number parsing;
- Support comments;
- Add maths functions;
- More permissive variable naming;
- Store last result in 'last' variable.

* Fix lint warning

* Preserve last value across comments and empty lines

* Fix lint warning

* Use BigNumber operations to maintain precision

* Add conditional around pow call

* Split up long test

* Remove duplicate tests
pull/5966/head
playerofgames 2022-07-07 14:38:37 +01:00 committed by GitHub
parent f39f26c8d3
commit 8e2aa8415c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 97 additions and 40 deletions

View File

@ -40,11 +40,18 @@
:sub (fn sub [a b] (-> a (.minus b)))
:mul (fn mul [a b] (-> a (.multipliedBy b)))
:div (fn div [a b] (-> a (.dividedBy b)))
:pow (fn pow [a b] (-> a (.exponentiatedBy b)))
:pow (fn pow [a b] (if (.isInteger b)
(.exponentiatedBy a b)
#?(:clj (java.lang.Math/pow a b)
:cljs (bn/BigNumber (js/Math.pow a b)))))
:abs (fn abs [a] (.abs a))
:sqrt (fn abs [a] (.sqrt a))
:log (fn log [a]
#?(:clj (java.lang.Math/log10 a) :cljs (bn/BigNumber (js/Math.log10 a))))
:ln (fn ln [a]
#?(:clj (java.lang.Math/log a) :cljs (bn/BigNumber (js/Math.log a))))
:exp (fn ln [a]
#?(:clj (java.lang.Math/exp a) :cljs (bn/BigNumber (js/Math.exp a))))
:sin (fn sin [a]
#?(:clj (java.lang.Math/sin a) :cljs (bn/BigNumber(js/Math.sin a))))
:cos (fn cos [a]
@ -61,6 +68,7 @@
(swap! env assoc var val)
val)
:toassign str/trim
:comment (constantly nil)
:variable (fn resolve [var]
(let [var (str/trim var)]
(or (get @env var)
@ -79,12 +87,17 @@
(catch #?(:clj Exception :cljs js/Error) e
e))))
(defn assign-last-value [env val]
(when-not (nil? val)
(swap! env assoc "last" val))
val)
(defn eval-lines [s]
{:pre [(string? s)]}
(let [env (new-env)]
(mapv (fn [line]
(when-not (str/blank? line)
(eval env (parse line))))
(assign-last-value env (eval env (parse line)))))
(str/split-lines s))))
;; ======================================================================

View File

@ -1,28 +1,32 @@
<start> = assignment | expr
expr = add-sub
<add-sub> = pow-term | mul-div | add | sub | variable
<start> = assignment | expr | comment
expr = add-sub comment
comment = <#'\s*(#.*$)?'>
<add-sub> = pow-term | mul-div | add | sub | variable
add = add-sub <'+'> mul-div
sub = add-sub <'-'> mul-div
<mul-div> = pow-term | mul | div
mul = mul-div <'*'> pow-term
div = mul-div <'/'> pow-term
<pow-term> = pow | term
pow = pow-term <'^'> term
<trig> = sin | cos | tan | acos | asin | atan
pow = posterm <'^'> pow-term
<function> = log | ln | exp | sqrt | abs | sin | cos | tan | acos | asin | atan
log = <#'\s*'> <'log('> expr <')'> <#'\s*'>
ln = <#'\s*'> <'ln('> expr <')'> <#'\s*'>
exp = <#'\s*'> <'exp('> expr <')'> <#'\s*'>
sqrt = <#'\s*'> <'sqrt('> expr <')'> <#'\s*'>
abs = <#'\s*'> <'abs('> expr <')'> <#'\s*'>
sin = <#'\s*'> <'sin('> expr <')'> <#'\s*'>
cos = <#'\s*'> <'cos('> expr <')'> <#'\s*'>
tan = <#'\s*'> <'tan('> expr <')'> <#'\s*'>
atan = <#'\s*'> <'atan('> expr <')'> <#'\s*'>
acos = <#'\s*'> <'acos('> expr <')'> <#'\s*'>
asin = <#'\s*'> <'asin('> expr <')'> <#'\s*'>
<posterm> = log | ln | trig | percent | scientific | number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'>
negterm = <#'\s*'> <'-'> posterm
<posterm> = function | percent | scientific | number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'>
negterm = <#'\s*'> <'-'> posterm | <#'\s*'> <'-'> pow
<term> = negterm | posterm
scientific = #'\s*[0-9]+\.?[0-9]*(e|E)-?[0-9]+()\s*'
number = #'\s*\d+(,\d+)*(\.\d*)?\s*'
scientific = #'\s*[0-9]*\.?[0-9]+(e|E)[\-\+]?[0-9]+()\s*'
number = #'\s*(\d+(,\d+)*(\.\d*)?|\d*\.\d+)\s*'
percent = number <'%'> <#'\s*'>
variable = #'\s*[a-zA-Z]+(\_+[a-zA-Z]+)*\s*'
toassign = #'\s*[a-zA-Z]+(\_+[a-zA-Z]+)*\s*'
variable = #'\s*_*[a-zA-Z]+[_a-zA-Z0-9]*\s*'
toassign = #'\s*_*[a-zA-Z]+[_a-zA-Z0-9]*\s*'
assignment = toassign <#'\s*'> <'='> <#'\s*'> expr

View File

@ -19,7 +19,11 @@
98123 "98123"
1.0 " 1.0 "
22.1124131 "22.1124131"
100.01231 " 100.01231 ")
100.01231 " 100.01231 "
0.01231 " .01231 "
0.015 ".015 "
-0.2 "-.2"
-0.3 "- .3")
(testing "even when they have the commas in the wrong place"
(are [value expr] (= value (run expr))
98123 "9812,3"
@ -62,15 +66,6 @@
2.0 "2*100%"
0.01 "2%/2"
500e3 "50% * 1e6"))
(testing "power"
(are [value expr] (= value (run expr))
1.0 "1 ^ 0"
4.0 "2^2 "
27.0 " 3^ 3"
0.125 " 2^ -3"
16.0 "2 ^ 2 ^ 2"
256.0 "4.000 ^ 4.0"
4096.0 "200% ^ 12"))
(testing "operator precedence"
(are [value expr] (= value (run expr))
1 "1 + 0 * 2"
@ -90,9 +85,39 @@
12.3 "123.0e-1"
-12.3 "-123.0e-1"
12.3 "123.0E-1"
2.0 "1e0 + 1e0"))
(testing "scientific functions"
12300 "123.0E+2"
2.0 "1e0 + 1e0"
10 ".1e2"
0.001 ".1e-2"
-0.045 "-.45e-1"
-210 "-.21e3"))
(testing "avoiding rounding errors"
(are [value expr] (= value (run expr))
3.3 "1.1 + 2.2"
2.2 "3.3 - 1.1"
0.0001 "1/10000"
1e-7 "1/10000000")))
(deftest scientific-functions
(testing "power"
(are [value expr] (= value (run expr))
1.0 "1 ^ 0"
4.0 "2^2 "
-9.0 "-3^2 "
9.0 "(-3)^2 "
27.0 " 3^ 3"
0.125 " 2^ -3"
512.0 "2 ^ 3 ^ 2"
256.0 "4.000 ^ 4.0"
2.0 "4^0.5"
0.1 "100^(-0.5)"
125.0 "25^(3/2)"
4096.0 "200% ^ 12"))
(testing "functions"
(are [value expr] (= value (run expr))
2.0 "sqrt( 4 )"
3.0 "abs( 3 )"
3.0 "abs( -3 )"
1.0 "cos( 0 * 1 )"
0.0 "sin( 1 -1 )"
0.0 "atan(tan(0))"
@ -101,14 +126,9 @@
0.0 "acos(cos(0))"
5.0 "2 * log(10) + 3"
1.0 "-2 * log(10) + 3"
10.0 "ln(1) + 10"))
(testing "avoiding rounding errors"
(are [value expr] (= value (run expr))
3.3 "1.1 + 2.2"
2.2 "3.3 - 1.1"
0.0001 "1/10000"
1e-7 "1/10000000"
)))
10.0 "ln(1) + 10"
1.0 "exp(0)"
2.0 "ln(exp(2))")))
(deftest variables
(testing "variables can be remembered"
@ -116,7 +136,8 @@
(calc/eval env (calc/parse expr))
(= final-env (into {} (for [[k v] @env] [k (convert-bigNum v)]))))
{"a" 1} "a = 1"
{"a" -1} "a = -1"
{"a" -1} "a = -1"
{"k9" 27} "k9 = 27"
{"variable" 1} "variable = 1 + 0 * 2"
{"x" 1} "x= 2 * 1 - 1 "
{"y" 4} "y =8 / 4 + 2 * 1 - 25 * 0 / 1"
@ -128,6 +149,7 @@
(calc/eval env (calc/parse expr))
(= final-env (into {} (for [[k v] @env] [k (convert-bigNum v)]))))
{"a_a" 1} "a_a = 1"
{"_foo" 1} "_foo = 1"
{"x_yy_zzz" 1} "x_yy_zzz= 1"
{"foo_bar_baz" 1} "foo_bar_baz = 1 + -0 * 2"))
(testing "variables can be reused"
@ -150,15 +172,33 @@
{"a" 2 "b" 2} ["a = 1" "b = a + 1" "a = b"]
{"variable" 1 "x" 0} ["variable = 1 + 0 * 2" "x = log(variable)" "x = variable - 1"])))
(deftest last-value
(testing "last value is set"
(are [values exprs] (let [env (calc/new-env)]
(mapv (fn [expr]
(calc/eval env (calc/parse expr)))
exprs))
[42 126] ["6*7" "last*3"]
[25 5] ["3^2+4^2" "sqrt(last)"]
[6 12] ["2*3" "# a comment" "" " " "last*2"])))
(deftest comments
(testing "comments are ignored"
(are [value expr] (= value (run expr))
nil "# this comment is ignored"
nil " # this comment is ignored "
8.0 "2*4# double 4"
10.0 "2*5 # double 5"
12.0 "2*6 # double 6"
14.0 "2*7 # 99")))
(deftest failure
(testing "expressions that don't match the spec fail"
(are [expr] (calc/failure? (calc/eval (calc/new-env) (calc/parse expr)))
"foo_ ="
"foo__ ="
"oo___ ="
" "
"bar_2 = 2 + 4"
"bar_2a = 3 + 4"
"foo_ = "
"foo__ ="
"foo_3 = a")))
" . "
"_ = 2"
"__ = 4"
"foo_3 = _")))