diff --git a/docs/dev-practices.md b/docs/dev-practices.md index 4879508aa1..2ed8f9f887 100644 --- a/docs/dev-practices.md +++ b/docs/dev-practices.md @@ -110,12 +110,10 @@ For help on more options, run `node static/tests.js -h`. #### Autorun Tests -To run tests automatically on file save, run `yarn -shadow-cljs watch test --config-merge '{:autorun true}'`. The test output may -appear where shadow-cljs was first invoked e.g. where `yarn watch` is running. -Specific namespace(s) can be auto run with the `:ns-regexp` option e.g. `npx -shadow-cljs watch test --config-merge '{:autorun true :ns-regexp -"frontend.util.page-property-test"}'`. +To run tests automatically on file save, run `clojure -M:test watch test +--config-merge '{:autorun true}'`. Specific namespace(s) can be auto run with +the `:ns-regexp` option e.g. `clojure -M:test watch test --config-merge +'{:autorun true :ns-regexp "frontend.util.page-property-test"}'`. ## Logging diff --git a/e2e-tests/flashcards.spec.ts b/e2e-tests/flashcards.spec.ts index e07a301683..fb8cb33fd3 100644 --- a/e2e-tests/flashcards.spec.ts +++ b/e2e-tests/flashcards.spec.ts @@ -3,7 +3,7 @@ import { test } from './fixtures' import { createRandomPage } from './utils' -test('flashcard demo', async ({ page, block }) => { +test.skip('flashcard demo', async ({ page, block }) => { await createRandomPage(page) await block.mustFill('Why do you add cards? #card #logseq') diff --git a/src/main/frontend/extensions/calc.cljc b/src/main/frontend/extensions/calc.cljc index d20f0d301c..8f1efad5f0 100644 --- a/src/main/frontend/extensions/calc.cljc +++ b/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)))) ;; ====================================================================== diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 12acb85757..e89730b20b 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -1252,7 +1252,8 @@ ;; if different direction, keep clear until one left (state/selection?) - (clear-last-selected-block!))) + (clear-last-selected-block!)) + nil) (defn on-select-block [direction] @@ -3053,7 +3054,8 @@ (select-up-down direction) :else - (select-first-last direction))))) + (select-first-last direction))) + nil)) (defn shortcut-select-up-down [direction] (fn [e] diff --git a/src/main/grammar/calc.bnf b/src/main/grammar/calc.bnf index 12466ef6b6..ac8190e0c8 100644 --- a/src/main/grammar/calc.bnf +++ b/src/main/grammar/calc.bnf @@ -1,14 +1,16 @@ - = assignment | expr | comment -expr = add-sub comment + = assignment | expr | comment | directive +expr = add-sub [comment] comment = <#'\s*(#.*$)?'> = pow-term | mul-div | add | sub | variable add = add-sub <'+'> mul-div sub = add-sub <'-'> mul-div - = pow-term | mul | div + = pow-term | mul | div | mod mul = mul-div <'*'> pow-term div = mul-div <'/'> pow-term - = pow | term +mod = mul-div <'mod'> pow-term + = pow | factorial | term pow = posterm <'^'> pow-term +factorial = posterm <'!'> <#'\s*'> = 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*'> - = function | percent | scientific | number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'> -negterm = <#'\s*'> <'-'> posterm | <#'\s*'> <'-'> pow + = function | percent | scientific | number | mixed-number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'> +negterm = <#'\s*'> <'-'> ( posterm | pow | factorial ) = 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 + = #'\s*(\d+(,\d+)*(\.\d*)?|\d*\.\d+)\s*' + = #'\s*0x([0-9a-fA-F]+(,[0-9a-fA-F]+)*(\.[0-9a-fA-F]*)?|[0-9a-fA-F]*\.[0-9a-fA-F]+)\s*' + = #'\s*0o([0-7]+(,[0-7]+)*(\.[0-7]*)?|[0-7]*\.[0-7]+)\s*' + = #'\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 \ No newline at end of file +assignment = toassign <#'\s*'> <'='> <#'\s*'> expr + = <#'\s*\:'> (format | base) <#'\s*'> [comment] + = <#'(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 + = #'(?i)hex' <#'(?i)(adecimal)?'> + = #'(?i)dec' <#'(?i)(imal)?'> + = #'(?i)oct' <#'(?i)(al)?'> + = #'(?i)bin' <#'(?i)(ary)?'> +digits = #'\d+' \ No newline at end of file diff --git a/src/test/frontend/extensions/calc_test.cljc b/src/test/frontend/extensions/calc_test.cljc index 79ee24e4df..7886231a9f 100644 --- a/src/test/frontend/extensions/calc_test.cljc +++ b/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 = _")))