|
|
@@ -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))))
|
|
|
|
|
|
;; ======================================================================
|