Sfoglia il codice sorgente

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
playerofgames 3 anni fa
parent
commit
8e2aa8415c

+ 15 - 2
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))))
 
 ;; ======================================================================

+ 15 - 11
src/main/grammar/calc.bnf

@@ -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

+ 67 - 27
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  = _")))