瀏覽代碼

Merge pull request #6056 from playerofgames/calc_enhance_format

Calculator improvements: new operations, display formats, constants and bases
Gabriel Horner 3 年之前
父節點
當前提交
b226484683
共有 3 個文件被更改,包括 242 次插入25 次删除
  1. 122 8
      src/main/frontend/extensions/calc.cljc
  2. 28 8
      src/main/grammar/calc.bnf
  3. 92 9
      src/test/frontend/extensions/calc_test.cljc

+ 122 - 8
src/main/frontend/extensions/calc.cljc

@@ -1,7 +1,6 @@
 (ns frontend.extensions.calc
   (:refer-clojure :exclude [eval])
-  (:require [clojure.edn :as edn]
-            [clojure.string :as str]
+  (:require [clojure.string :as str]
             [frontend.util :as util]
 
             [bignumber.js :as bn]
@@ -18,6 +17,10 @@
 #?(:clj (def parse (insta/parser (io/resource "grammar/calc.bnf")))
    :cljs (defparser parse (rc/inline "grammar/calc.bnf")))
 
+(def constants {
+  "PI" (bn/BigNumber "3.14159265358979323846")
+  "E"  (bn/BigNumber "2.71828182845904523536")})
+
 (defn exception? [e]
   #?(:clj (instance? Exception e)
      :cljs (instance? js/Error e)))
@@ -29,28 +32,40 @@
 
 ;; TODO: Set DECIMAL_PLACES https://mikemcl.github.io/bignumber.js/#decimal-places
 
+(defn factorial [n]
+  (reduce
+    (fn [a b] (.multipliedBy a b))
+    (bn/BigNumber 1)
+    (range 2 (inc n))))
+
 (defn eval* [env ast]
   (insta/transform
    {:number     (comp bn/BigNumber #(str/replace % "," ""))
     :percent    (fn percent [a] (-> a (.dividedBy 100.00)))
-    :scientific (comp bn/BigNumber edn/read-string)
+    :scientific bn/BigNumber
+    :mixed-number (fn [whole numerator denominator]
+                    (.plus (.dividedBy (bn/BigNumber numerator) denominator) whole))
     :negterm    (fn neg [a] (-> a (.negated)))
     :expr       identity
     :add        (fn add [a b] (-> a (.plus b)))
     :sub        (fn sub [a b] (-> a (.minus b)))
     :mul        (fn mul [a b] (-> a (.multipliedBy b)))
     :div        (fn div [a b] (-> a (.dividedBy b)))
+    :mod        (fn mod [a b] (-> a (.modulo 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)))))
+    :factorial  (fn fact [a] (if (and (.isInteger a) (.isPositive a) (.isLessThan a 254))
+                                 (factorial (.toNumber a))
+                                 (bn/BigNumber 'NaN')))
     :abs        (fn abs [a] (.abs a))
-    :sqrt       (fn abs [a] (.sqrt a))
+    :sqrt       (fn sqrt [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]
+    :exp        (fn exp [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))))
@@ -65,13 +80,38 @@
     :acos       (fn acos [a]
                   #?(:clj (java.lang.Math/acos a) :cljs (bn/BigNumber(js/Math.acos a))))
     :assignment (fn assign! [var val]
-                  (swap! env assoc var val)
+                  (if (contains? constants var)
+                    (throw
+                      (ex-info (util/format "Can't redefine constant %s" var) {:var var}))
+                    (swap! env assoc var val))
                   val)
     :toassign   str/trim
     :comment    (constantly nil)
+    :digits     int
+    :format-fix (fn format [places]
+                  (swap! env assoc :mode "fix" :places places)
+                  (get @env "last"))
+    :format-sci (fn format [places]
+                  (swap! env assoc :mode "sci" :places places)
+                  (get @env "last"))
+    :format-frac (fn format [max-denominator]
+                  (swap! env dissoc :mode :improper)
+                  (swap! env assoc :mode "frac" :max-denominator max-denominator)
+                  (get @env "last"))
+    :format-impf (fn format [max-denominator]
+                  (swap! env assoc :mode "frac" :max-denominator max-denominator :improper true)
+                  (get @env "last"))
+    :format-norm (fn format [precision]
+                  (swap! env dissoc :mode :places)
+                  (swap! env assoc :precision precision)
+                  (get @env "last"))
+    :base       (fn base [b]
+                  (swap! env assoc :base (str/lower-case b))
+                  (get @env "last"))
     :variable   (fn resolve [var]
                   (let [var (str/trim var)]
-                    (or (get @env var)
+                    (or (get constants var)
+                        (get @env var)
                         (throw
                          (ex-info (util/format "Can't find variable %s" var)
                                   {:var var})))))}
@@ -92,12 +132,86 @@
     (swap! env assoc "last" val))
   val)
 
+(defn can-fix?
+  "Check that number can render without loss of all significant digits,
+   and that the absolute value is less than 1e21."
+  [num places]
+  (or (.isZero num )
+    (let [mag (.abs num)
+          lower-bound (-> (bn/BigNumber 0.5) (.shiftedBy (- places)))
+          upper-bound (bn/BigNumber 1e21)]
+      (and (-> mag (.isGreaterThanOrEqualTo lower-bound))
+           (-> mag (.isLessThan upper-bound))))))
+
+(defn can-fit?
+  "Check that number can render normally within the given number of digits.
+   Tolerance allows for leading zeros in a decimal fraction."
+  [num digits tolerance]
+  (and (< (.-e num) digits)
+       (.isInteger (.shiftedBy num (+ tolerance digits)))))
+
+(defn format-base [val base]
+  (let [sign (.-s val)
+       display-val (if (neg-int? sign) (.abs val) val)]
+    (str
+      (when (neg-int? sign) "-")
+      (case base 2 "0b" 8 "0o" 16 "0x")
+      (.toString display-val base))))
+
+(defn format-fraction [numerator denominator improper]
+  (let [whole (.dividedToIntegerBy numerator denominator)]
+    (if (or improper (.isZero whole))
+      (str numerator "/" denominator )
+      (str whole " "
+           (.abs (.modulo numerator denominator)) "/" denominator))))
+
+(defn format-normal [env val]
+  (let [precision (or (get @env :precision) 21)
+        display-val (.precision val precision)]
+    (if (can-fit? display-val precision 1)
+      (.toFixed display-val)
+      (.toExponential display-val))))
+
+(defn format-val [env val]
+  (if (instance? bn/BigNumber val)
+    (let [mode (get @env :mode)
+          base (get @env :base)
+          places (get @env :places)]
+      (cond
+        (= base "hex")
+          (format-base val 16)
+        (= base "oct")
+          (format-base val 8)
+        (= base "bin")
+          (format-base val 2)
+
+        (= mode "fix")
+          (if (can-fix? val places)
+            (.toFixed val places)
+            (.toExponential val places))
+        (= mode "sci")
+          (.toExponential val places)
+        (= mode "frac")
+          (let [max-denominator (or (get @env :max-denominator) 4095)
+                improper  (get @env :improper)
+                [numerator denominator] (.toFraction val max-denominator)
+                delta (.minus (.dividedBy numerator denominator) val)]
+            (if (or (.isZero delta) (< (.-e delta) -16))
+              (if (> denominator 1)
+                (format-fraction numerator denominator improper)
+                (format-normal env numerator))
+              (format-normal env val)))
+
+        :else
+          (format-normal env val)))
+    val))
+
 (defn eval-lines [s]
   {:pre [(string? s)]}
   (let [env (new-env)]
     (mapv (fn [line]
             (when-not (str/blank? line)
-              (assign-last-value env (eval env (parse line)))))
+              (format-val env (assign-last-value env (eval env (parse line))))))
           (str/split-lines s))))
 
 ;; ======================================================================

+ 28 - 8
src/main/grammar/calc.bnf

@@ -1,14 +1,16 @@
-<start> = assignment | expr | comment
-expr = add-sub comment
+<start> = assignment | expr | comment | directive
+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-div> = pow-term | mul | div | mod
 mul = mul-div <'*'> pow-term
 div = mul-div <'/'> pow-term
-<pow-term> = pow | term
+mod = mul-div <'mod'> pow-term
+<pow-term> = pow | factorial | term
 pow = posterm <'^'> pow-term
+factorial = posterm <'!'> <#'\s*'>
 <function> = log | ln | exp | sqrt | abs | sin | cos | tan | acos | asin | atan
 log = <#'\s*'> <'log('> expr <')'> <#'\s*'>
 ln = <#'\s*'> <'ln('> expr <')'> <#'\s*'>
@@ -21,12 +23,30 @@ tan = <#'\s*'> <'tan('> expr <')'> <#'\s*'>
 atan = <#'\s*'> <'atan('> expr <')'> <#'\s*'>
 acos = <#'\s*'> <'acos('> expr <')'> <#'\s*'>
 asin = <#'\s*'> <'asin('> expr <')'> <#'\s*'>
-<posterm> = function | percent | scientific | number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'>
-negterm = <#'\s*'> <'-'> posterm | <#'\s*'> <'-'> pow
+<posterm> = function | percent | scientific | number | mixed-number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'>
+negterm = <#'\s*'> <'-'> ( posterm | pow | factorial )
 <term> = negterm | posterm
 scientific = #'\s*[0-9]*\.?[0-9]+(e|E)[\-\+]?[0-9]+()\s*'
-number = #'\s*(\d+(,\d+)*(\.\d*)?|\d*\.\d+)\s*'
+number = decimal-number | hexadecimal-number | octal-number | binary-number
+<decimal-number> = #'\s*(\d+(,\d+)*(\.\d*)?|\d*\.\d+)\s*'
+<hexadecimal-number> = #'\s*0x([0-9a-fA-F]+(,[0-9a-fA-F]+)*(\.[0-9a-fA-F]*)?|[0-9a-fA-F]*\.[0-9a-fA-F]+)\s*'
+<octal-number> = #'\s*0o([0-7]+(,[0-7]+)*(\.[0-7]*)?|[0-7]*\.[0-7]+)\s*'
+<binary-number> = #'\s*0b([01]+(,[01]+)*(\.[01]*)?|[01]*\.[01]+)\s*'
+mixed-number = <#'\s*'> digits <#'\s+'> digits <'/'> digits <#'\s*'>
 percent = number <'%'> <#'\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
+assignment = toassign <#'\s*'> <'='> <#'\s*'> expr
+<directive> = <#'\s*\:'> (format | base) <#'\s*'> [comment]
+<format> = <#'(format|fmt)\s+'> ( format-fix | format-sci | format-norm | format-frac | format-impf )
+format-fix = <#'(?i)fix(ed)?\s*'> digits
+format-sci = <#'(?i)sci(entific)?\s*'> [digits]
+format-norm = <#'(?i)norm(al)?\s*'> [digits]
+format-frac = <#'(?i)frac(tions?)?\s*'> [digits]
+format-impf = <#'(?i)imp(roper)?\s*'> [digits]
+base = base-hex | base-dec | base-oct | base-bin
+<base-hex> = #'(?i)hex' <#'(?i)(adecimal)?'>
+<base-dec> = #'(?i)dec' <#'(?i)(imal)?'>
+<base-oct> = #'(?i)oct' <#'(?i)(al)?'>
+<base-bin> = #'(?i)bin' <#'(?i)(ary)?'>
+digits = #'\d+'

+ 92 - 9
src/test/frontend/extensions/calc_test.cljc

@@ -1,11 +1,11 @@
 (ns frontend.extensions.calc-test
   (:require [clojure.test :as test :refer [are deftest testing]]
+            [clojure.string :as str]
             [clojure.edn :as edn]
             [frontend.extensions.calc :as calc]))
 
 (defn convert-bigNum [b]
-  (edn/read-string (str b))
-  )
+  (edn/read-string (str b)))
 
 (defn run [expr]
   {:pre [(string? expr)]}
@@ -130,6 +130,23 @@
       1.0  "exp(0)"
       2.0  "ln(exp(2))")))
 
+(deftest additional-operators
+  (testing "mod"
+    (are [value expr] (= value (run expr))
+      0.0    "1 mod 1"
+      1.0    "7 mod 3"
+      3.0    "7 mod 4"
+      0.5    "4.5 mod 2"
+      -3.0   "-7 mod 4"))
+  (testing "factorial"
+    (are [value expr] (= value (run expr))
+      1.0     "0!"
+      1.0     "1!"
+      6.0     "3.0!"
+      -120.0  "-5!"
+      124.0   "(2+3)!+4"
+      240.0   "10 * 4!")))
+
 (deftest variables
   (testing "variables can be remembered"
     (are [final-env expr] (let [env (calc/new-env)]
@@ -174,13 +191,78 @@
 
 (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"])))
+    (are [values exprs] (= values (calc/eval-lines (str/join "\n" exprs)))
+      ["42" "126"]            ["6*7" "last*3"]
+      ["25" "5"]              ["3^2+4^2" "sqrt(last)"]
+      ["6" nil nil nil "12"]  ["2*3" "# a comment" "" "   " "last*2"])))
+
+(deftest formatting
+  (testing "display normal"
+    (are [values exprs] (= values (calc/eval-lines (str/join "\n" exprs)))
+      [nil "1000000"]     [":format norm" "1e6" ]
+      [nil "1000000"]     [":format norm 7" "1e6"]
+      [nil "1e+6"]        [":format norm 6" "1e6"]
+      [nil "3.14"]        [":format norm 3" "PI"]
+      [nil "3"]           [":format norm 1" "E"]
+      [nil "0.000123"]    [":format norm 5" "0.000123"]
+      [nil "1.23e-4"]     [":format norm 4" "0.000123"]
+      [nil "123400000"]   [":format normal 9" "1.234e8"]
+      [nil "1.234e+8"]    [":format normal 8" "1.234e8"]))
+  (testing "display fixed"
+    (are [values exprs] (= values (calc/eval-lines (str/join "\n" exprs)))
+      [nil "0.123450"]    [":format fix 6" "0.12345"]
+      [nil "0.1235"]      [":format fix 4" "0.12345"]
+      [nil "2.7183"]      [":format fixed 4" "E"]
+      [nil "0.001"]       [":format fix 3" "0.0005"]
+      [nil "4.000e-4"]    [":format fix 3" "0.0004"]
+      [nil "1.00e+21"]    [":format fixed 2" "1e21+0.1"]))
+  (testing "display scientific"
+    (are [values exprs] (= values (calc/eval-lines (str/join "\n" exprs)))
+      [nil "1e+6"]        [":format sci" "1e6"]
+      [nil "3.142e+0"]    [":format sci 3" "PI"]
+      [nil "3.14e+2"]     [":format scientific" "3.14*10^2"])))
+
+(deftest fractions
+  (testing "mixed numbers"
+    (are [value expr] (= value (run expr))
+      0          "0 0/1"
+      1          "0 1/1"
+      1          "1 0/1"
+      2.5        "2 1/2"
+      2.5        "2 1/2"
+      -4.28      "-4 7/25"
+      2.00101    "2 101/100000"
+      -99.2      "-99 8/40"))
+  (testing "display fractions"
+    (are [values exprs] (= values (calc/eval-lines (str/join "\n" exprs)))
+      [nil "4 3/8"]           [":format frac" "4.375"]
+      [nil "-7 1/4"]          [":format fraction" "-7.25"]
+      [nil "2"]               [":format fractions" "19/20 + 1 1/20"]
+      [nil "-2"]              [":format frac" "19/17 - 3 2/17"]
+      [nil "3.14157"]         [":format frac" "3.14157"]
+      [nil "3 14157/100000"]  [":format frac 100000" "3.14157"]))
+  (testing "display improper fractions"
+    (are [values exprs] (= values (calc/eval-lines (str/join "\n" exprs)))
+      [nil "35/8"]            [":format improper" "4.375"]
+      [nil "-29/4"]           [":format imp" "-7.25"]
+      [nil "3.14157"]         [":format improper" "3.14157" ]
+      [nil "314157/100000"]   [":format imp 100000" "3.14157"])))
+
+(deftest base-conversion
+  (testing "mixed base input"
+    (are [value expr] (= value (run expr))
+      255.0     "0xff"
+      511.0     "0x0A + 0xF5 + 0x100"
+      83.0      "0o123"
+      324.0     "0x100 + 0o100 + 0b100"
+      32.0      "0b100 * 0b1000"))
+  (testing "mixed base output"
+    (are [values exprs] (= values (calc/eval-lines (str/join "\n" exprs)))
+      ["12345" "0x3039"]          ["12345" ":hex"]
+      ["12345" "0o30071"]         ["12345" ":oct"]
+      ["12345" "0b11000000111001"]["12345" ":bin"]
+      [nil "0b100000000"]         [":bin" "0b10000 * 0b10000"]
+      [nil "-0xff"]               [":hex" "-255"])))
 
 (deftest comments
   (testing "comments are ignored"
@@ -201,4 +283,5 @@
       " . "
       "_ = 2"
       "__ = 4"
+      "PI = 3.14"
       "foo_3  = _")))