From 8e2aa8415cc2589c1cf4db1f86d85f314648ff38 Mon Sep 17 00:00:00 2001 From: playerofgames Date: Thu, 7 Jul 2022 14:38:37 +0100 Subject: [PATCH] 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 --- src/main/frontend/extensions/calc.cljc | 17 +++- src/main/grammar/calc.bnf | 26 +++--- src/test/frontend/extensions/calc_test.cljc | 94 +++++++++++++++------ 3 files changed, 97 insertions(+), 40 deletions(-) diff --git a/src/main/frontend/extensions/calc.cljc b/src/main/frontend/extensions/calc.cljc index 8c30b5cdb..d20f0d301 100644 --- a/src/main/frontend/extensions/calc.cljc +++ b/src/main/frontend/extensions/calc.cljc @@ -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)))) ;; ====================================================================== diff --git a/src/main/grammar/calc.bnf b/src/main/grammar/calc.bnf index 29884e78b..12466ef6b 100644 --- a/src/main/grammar/calc.bnf +++ b/src/main/grammar/calc.bnf @@ -1,28 +1,32 @@ - = assignment | expr -expr = add-sub - = pow-term | mul-div | add | sub | variable + = assignment | expr | comment +expr = add-sub comment +comment = <#'\s*(#.*$)?'> + = pow-term | mul-div | add | sub | variable add = add-sub <'+'> mul-div sub = add-sub <'-'> mul-div = pow-term | mul | div mul = mul-div <'*'> pow-term div = mul-div <'/'> pow-term = pow | term -pow = pow-term <'^'> term - = sin | cos | tan | acos | asin | atan +pow = posterm <'^'> pow-term + = 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*'> - = log | ln | trig | percent | scientific | number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'> -negterm = <#'\s*'> <'-'> posterm + = function | percent | scientific | number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'> +negterm = <#'\s*'> <'-'> posterm | <#'\s*'> <'-'> pow = 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 \ No newline at end of file diff --git a/src/test/frontend/extensions/calc_test.cljc b/src/test/frontend/extensions/calc_test.cljc index 53b795a2c..79ee24e4d 100644 --- a/src/test/frontend/extensions/calc_test.cljc +++ b/src/test/frontend/extensions/calc_test.cljc @@ -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 = _")))