mirror of https://github.com/logseq/logseq
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.pull/6056/head
parent
903e5c8a3b
commit
70c04e34bf
|
@ -1,7 +1,6 @@
|
||||||
(ns frontend.extensions.calc
|
(ns frontend.extensions.calc
|
||||||
(:refer-clojure :exclude [eval])
|
(:refer-clojure :exclude [eval])
|
||||||
(:require [clojure.edn :as edn]
|
(:require [clojure.string :as str]
|
||||||
[clojure.string :as str]
|
|
||||||
[frontend.util :as util]
|
[frontend.util :as util]
|
||||||
|
|
||||||
[bignumber.js :as bn]
|
[bignumber.js :as bn]
|
||||||
|
@ -18,6 +17,10 @@
|
||||||
#?(:clj (def parse (insta/parser (io/resource "grammar/calc.bnf")))
|
#?(:clj (def parse (insta/parser (io/resource "grammar/calc.bnf")))
|
||||||
:cljs (defparser parse (rc/inline "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]
|
(defn exception? [e]
|
||||||
#?(:clj (instance? Exception e)
|
#?(:clj (instance? Exception e)
|
||||||
:cljs (instance? js/Error e)))
|
:cljs (instance? js/Error e)))
|
||||||
|
@ -29,28 +32,38 @@
|
||||||
|
|
||||||
;; TODO: Set DECIMAL_PLACES https://mikemcl.github.io/bignumber.js/#decimal-places
|
;; 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]
|
(defn eval* [env ast]
|
||||||
(insta/transform
|
(insta/transform
|
||||||
{:number (comp bn/BigNumber #(str/replace % "," ""))
|
{:number (comp bn/BigNumber #(str/replace % "," ""))
|
||||||
:percent (fn percent [a] (-> a (.dividedBy 100.00)))
|
:percent (fn percent [a] (-> a (.dividedBy 100.00)))
|
||||||
:scientific (comp bn/BigNumber edn/read-string)
|
:scientific bn/BigNumber
|
||||||
:negterm (fn neg [a] (-> a (.negated)))
|
:negterm (fn neg [a] (-> a (.negated)))
|
||||||
:expr identity
|
:expr identity
|
||||||
:add (fn add [a b] (-> a (.plus b)))
|
:add (fn add [a b] (-> a (.plus b)))
|
||||||
:sub (fn sub [a b] (-> a (.minus b)))
|
:sub (fn sub [a b] (-> a (.minus b)))
|
||||||
:mul (fn mul [a b] (-> a (.multipliedBy b)))
|
:mul (fn mul [a b] (-> a (.multipliedBy b)))
|
||||||
:div (fn div [a b] (-> a (.dividedBy 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)
|
:pow (fn pow [a b] (if (.isInteger b)
|
||||||
(.exponentiatedBy a b)
|
(.exponentiatedBy a b)
|
||||||
#?(:clj (java.lang.Math/pow a b)
|
#?(:clj (java.lang.Math/pow a b)
|
||||||
:cljs (bn/BigNumber (js/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))
|
:abs (fn abs [a] (.abs a))
|
||||||
:sqrt (fn abs [a] (.sqrt a))
|
:sqrt (fn sqrt [a] (.sqrt a))
|
||||||
:log (fn log [a]
|
:log (fn log [a]
|
||||||
#?(:clj (java.lang.Math/log10 a) :cljs (bn/BigNumber (js/Math.log10 a))))
|
#?(:clj (java.lang.Math/log10 a) :cljs (bn/BigNumber (js/Math.log10 a))))
|
||||||
:ln (fn ln [a]
|
:ln (fn ln [a]
|
||||||
#?(:clj (java.lang.Math/log a) :cljs (bn/BigNumber (js/Math.log 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))))
|
#?(:clj (java.lang.Math/exp a) :cljs (bn/BigNumber (js/Math.exp a))))
|
||||||
:sin (fn sin [a]
|
:sin (fn sin [a]
|
||||||
#?(:clj (java.lang.Math/sin a) :cljs (bn/BigNumber(js/Math.sin a))))
|
#?(:clj (java.lang.Math/sin a) :cljs (bn/BigNumber(js/Math.sin a))))
|
||||||
|
@ -65,13 +78,31 @@
|
||||||
:acos (fn acos [a]
|
:acos (fn acos [a]
|
||||||
#?(:clj (java.lang.Math/acos a) :cljs (bn/BigNumber(js/Math.acos a))))
|
#?(:clj (java.lang.Math/acos a) :cljs (bn/BigNumber(js/Math.acos a))))
|
||||||
:assignment (fn assign! [var val]
|
: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)
|
val)
|
||||||
:toassign str/trim
|
:toassign str/trim
|
||||||
:comment (constantly nil)
|
: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]
|
:variable (fn resolve [var]
|
||||||
(let [var (str/trim var)]
|
(let [var (str/trim var)]
|
||||||
(or (get @env var)
|
(or (get constants var)
|
||||||
|
(get @env var)
|
||||||
(throw
|
(throw
|
||||||
(ex-info (util/format "Can't find variable %s" var)
|
(ex-info (util/format "Can't find variable %s" var)
|
||||||
{:var var})))))}
|
{:var var})))))}
|
||||||
|
@ -92,12 +123,58 @@
|
||||||
(swap! env assoc "last" val))
|
(swap! env assoc "last" val))
|
||||||
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]
|
(defn eval-lines [s]
|
||||||
{:pre [(string? s)]}
|
{:pre [(string? s)]}
|
||||||
(let [env (new-env)]
|
(let [env (new-env)]
|
||||||
(mapv (fn [line]
|
(mapv (fn [line]
|
||||||
(when-not (str/blank? 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))))
|
(str/split-lines s))))
|
||||||
|
|
||||||
;; ======================================================================
|
;; ======================================================================
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<start> = assignment | expr | comment
|
<start> = assignment | expr | comment | mode
|
||||||
expr = add-sub comment
|
expr = add-sub [comment]
|
||||||
comment = <#'\s*(#.*$)?'>
|
comment = <#'\s*(#.*$)?'>
|
||||||
<add-sub> = pow-term | mul-div | add | sub | variable
|
<add-sub> = pow-term | mul-div | add | sub | variable
|
||||||
add = add-sub <'+'> mul-div
|
add = add-sub <'+'> mul-div
|
||||||
sub = 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
|
mul = mul-div <'*'> pow-term
|
||||||
div = 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
|
pow = posterm <'^'> pow-term
|
||||||
|
factorial = posterm <'!'> <#'\s*'>
|
||||||
<function> = log | ln | exp | sqrt | abs | sin | cos | tan | acos | asin | atan
|
<function> = log | ln | exp | sqrt | abs | sin | cos | tan | acos | asin | atan
|
||||||
log = <#'\s*'> <'log('> expr <')'> <#'\s*'>
|
log = <#'\s*'> <'log('> expr <')'> <#'\s*'>
|
||||||
ln = <#'\s*'> <'ln('> expr <')'> <#'\s*'>
|
ln = <#'\s*'> <'ln('> expr <')'> <#'\s*'>
|
||||||
|
@ -22,11 +24,25 @@ atan = <#'\s*'> <'atan('> expr <')'> <#'\s*'>
|
||||||
acos = <#'\s*'> <'acos('> expr <')'> <#'\s*'>
|
acos = <#'\s*'> <'acos('> expr <')'> <#'\s*'>
|
||||||
asin = <#'\s*'> <'asin('> expr <')'> <#'\s*'>
|
asin = <#'\s*'> <'asin('> expr <')'> <#'\s*'>
|
||||||
<posterm> = function | percent | scientific | number | variable | <#'\s*'> <'('> 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
|
<term> = negterm | posterm
|
||||||
scientific = #'\s*[0-9]*\.?[0-9]+(e|E)[\-\+]?[0-9]+()\s*'
|
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*'>
|
percent = number <'%'> <#'\s*'>
|
||||||
variable = #'\s*_*[a-zA-Z]+[_a-zA-Z0-9]*\s*'
|
variable = #'\s*_*[a-zA-Z]+[_a-zA-Z0-9]*\s*'
|
||||||
toassign = #'\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+'
|
|
@ -130,6 +130,23 @@
|
||||||
1.0 "exp(0)"
|
1.0 "exp(0)"
|
||||||
2.0 "ln(exp(2))")))
|
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
|
(deftest variables
|
||||||
(testing "variables can be remembered"
|
(testing "variables can be remembered"
|
||||||
(are [final-env expr] (let [env (calc/new-env)]
|
(are [final-env expr] (let [env (calc/new-env)]
|
||||||
|
@ -182,6 +199,59 @@
|
||||||
[25 5] ["3^2+4^2" "sqrt(last)"]
|
[25 5] ["3^2+4^2" "sqrt(last)"]
|
||||||
[6 12] ["2*3" "# a comment" "" " " "last*2"])))
|
[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
|
(deftest comments
|
||||||
(testing "comments are ignored"
|
(testing "comments are ignored"
|
||||||
(are [value expr] (= value (run expr))
|
(are [value expr] (= value (run expr))
|
||||||
|
@ -201,4 +271,5 @@
|
||||||
" . "
|
" . "
|
||||||
"_ = 2"
|
"_ = 2"
|
||||||
"__ = 4"
|
"__ = 4"
|
||||||
|
"PI = 3.14"
|
||||||
"foo_3 = _")))
|
"foo_3 = _")))
|
||||||
|
|
Loading…
Reference in New Issue