Przeglądaj źródła

Add operations, display formats, constants and bases

New operations:
- factorial operator `!`;
- modulo operator `mod`.

Constants added:
- `PI`
- `E`

New display format controls:
- `:fix <places>` display format;
- `:sci <places>` display format;
- `:norm <display-precision>` display format;
- `:hex`, `:oct`, `:bin` and`:dec` display formats.

New number formats:
- Enter hexadecimal, octal and binary numbers using `0x`, `0o` and `0b` prefixes respectively.

Bugs fixed:
- Loss of precision when supplying numbers in scientific notation.
Martin Scott 3 lat temu
rodzic
commit
70c04e34bf

+ 85 - 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,38 @@
 
 ;; 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
     :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 +78,31 @@
     :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
+    :mode-fix   (fn format [places]
+                  (swap! env assoc :mode "fix" :places places)
+                  (get @env "last"))
+    :mode-sci   (fn format [places]
+                  (swap! env assoc :mode "sci" :places places)
+                  (get @env "last"))
+    :mode-norm  (fn format [precision]
+                  (swap! env dissoc :mode :places)
+                  (swap! env assoc :precision precision)
+                  (get @env "last"))
+    :mode-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 +123,58 @@
     (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-val [env val]
+  (if (instance? bn/BigNumber val)
+    (let [mode (get @env :mode)
+          base (get @env :base)
+          places (get @env :places)]
+      (cond
+        (= base "hex")
+          (.toString val 16)
+        (= base "oct")
+          (.toString val 8)
+        (= base "bin")
+          (.toString val 2)
+
+        (= mode "fix")
+          (if (can-fix? val places)
+            (.toFixed val places)
+            (.toExponential val places))
+        (= mode "sci")
+          (.toExponential val places)
+
+        :else
+          (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)))))
+    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))))
 
 ;; ======================================================================

+ 23 - 7
src/main/grammar/calc.bnf

@@ -1,14 +1,16 @@
-<start> = assignment | expr | comment
-expr = add-sub comment
+<start> = assignment | expr | comment | mode
+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*'>
@@ -22,11 +24,25 @@ 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
+negterm = <#'\s*'> <'-'> posterm | <#'\s*'> <'-'> pow | <#'\s*'> <'-'> 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*'
 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
+<mode> = <#'\s*\:'> ( mode-fix | mode-sci | mode-norm | mode-base ) <#'\s*'> [comment]
+mode-fix = <#'(?i)fix(ed)?\s*'> digits
+mode-sci = <#'(?i)sci(entific)?\s*'> [digits]
+mode-norm = <#'(?i)norm(al)?\s*'> [digits]
+mode-base = mode-hex | mode-dec | mode-oct | mode-bin
+<mode-hex> = #'(?i)hex' <#'(?i)(adecimal)?'>
+<mode-dec> = #'(?i)dec' <#'(?i)(imal)?'>
+<mode-oct> = #'(?i)oct' <#'(?i)(al)?'>
+<mode-bin> = #'(?i)bin' <#'(?i)(ary)?'>
+digits = #'\d+'

+ 71 - 0
src/test/frontend/extensions/calc_test.cljc

@@ -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)]
@@ -182,6 +199,59 @@
       [25 5]   ["3^2+4^2" "sqrt(last)"]
       [6 12]   ["2*3" "# a comment" "" "   " "last*2"])))
 
+(deftest formatting
+  (testing "display normal"
+    (are [values exprs] (let [env (calc/new-env)]
+                          (mapv (fn [expr]
+                                  (calc/eval env (calc/parse expr)))
+                                exprs))
+      [1e6 "1000000"]         ["1e6" ":norm"]
+      [1e6 "1000000"]         ["1e6" ":norm 7"]
+      [1e6 "1e+6"]            ["1e6" ":norm 6"]
+      [0 "0" "3.14"]          ["0" ":norm 3" "PI"]
+      [0 "0" "2"]             ["0" ":norm 1" "E"]
+      [0.000123 "0.000123"]   ["0.000123" ":norm 5"]
+      [0.000123 "1.23e-4"]    ["0.000123" ":norm 4"]
+      [123400000 "123400000"] ["1.234e8" ":norm 9"]
+      [123400000 "1.234e+8"]  ["1.234e8" ":norm 8"]))
+  (testing "display fixed"
+    (are [values exprs] (let [env (calc/new-env)]
+                          (mapv (fn [expr]
+                                  (calc/eval env (calc/parse expr)))
+                                exprs))
+      [0.12345 "0.123450"]    ["0.12345" ":fix 6"]
+      [0.12345 "0.1235"]      ["0.12345" ":fix 4"]
+      ["" "2.7183"]           [":fixed 4" "E"]
+      [0.0005 "0.001"]        ["0.0005" ":fix 3"]
+      [0.0005 "4.000e-4"]     ["0.0004" ":fix 3"]
+      [1e21 "1.00e+21"]       ["1e21+0.1" ":fix 2"]))
+  (testing "display scientific"
+    (are [values exprs] (let [env (calc/new-env)]
+                          (mapv (fn [expr]
+                                  (calc/eval env (calc/parse expr)))
+                                exprs))
+      [1e6 "1e+6"]            ["1e6" ":sci"]
+      [0 "0.000e0" "3.142e+3"]["0" ":sci 3" "PI"]
+      ["" "3.14e+2"]          [":sci" "3.14*10^2"])))
+
+(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] (let [env (calc/new-env)]
+                          (mapv (fn [expr]
+                                  (calc/eval env (calc/parse expr)))
+                                exprs))
+      ["12345" "3039"]          ["12345" ":hex"]
+      ["12345" "30071"]         ["12345" ":oct"]
+      ["12345" "11000000111001"]["12345" ":bin"]
+      ["" "100000000"]          [":bin" "0b10000 * 0b10000"])))
+
 (deftest comments
   (testing "comments are ignored"
     (are [value expr] (= value (run expr))
@@ -201,4 +271,5 @@
       " . "
       "_ = 2"
       "__ = 4"
+      "PI = 3.14"
       "foo_3  = _")))