Browse Source

feat: add calc parsing and evaluation (#2052)

Co-authored-by: Sebastian Bensusan <[email protected]>
Sebastian Bensusan 4 years ago
parent
commit
a53eb09846

+ 2 - 1
deps.edn

@@ -34,7 +34,8 @@
   thheller/shadow-cljs        {:mvn/version "2.12.5"}
   expound/expound             {:mvn/version "0.8.6"}
   com.lambdaisland/glogi      {:mvn/version "1.0.116"}
-  binaryage/devtools          {:mvn/version "1.0.2"}}
+  binaryage/devtools          {:mvn/version "1.0.2"}
+  instaparse/instaparse       {:mvn/version "1.4.10"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.844"}

+ 55 - 0
src/main/frontend/extensions/calc.cljc

@@ -0,0 +1,55 @@
+(ns frontend.extensions.calc
+  (:refer-clojure :exclude [eval])
+  (:require #?(:clj [clojure.java.io :as io])
+            #?(:cljs [shadow.resource :as rc])
+            [clojure.string :as str]
+            [clojure.edn :as edn]
+            [clojure.test :as test :refer [deftest testing is are]]
+            [frontend.util :as util]
+            #?(:clj [instaparse.core :as insta]
+               :cljs [instaparse.core :as insta :refer-macros [defparser]])))
+
+#?(:clj (def parse (insta/parser (io/resource "grammar/calc.bnf")))
+   :cljs (defparser parse (rc/inline "grammar/calc.bnf")))
+
+(defn new-env [] (atom {}))
+
+(defn eval
+  ([ast] (eval (new-env) ast))
+  ([env ast]
+   (doall
+    (insta/transform
+     {:number     edn/read-string
+      :expr       identity
+      :add        +
+      :sub        -
+      :mul        *
+      :div        /
+      :pow        (fn [a b]
+                    #?(:clj (java.lang.Math/pow a b) :cljs (js/Math.pow a b)))
+      :log        (fn [a]
+                    #?(:clj (java.lang.Math/log10 a) :cljs (js/Math.log10 a)))
+      :ln         (fn [a]
+                    #?(:clj (java.lang.Math/log a) :cljs (js/Math.log a)))
+      :sin        (fn [a]
+                    #?(:clj (java.lang.Math/sin a) :cljs (js/Math.sin a)))
+      :cos        (fn [a]
+                    #?(:clj (java.lang.Math/cos a) :cljs (js/Math.cos a)))
+      :tan        (fn [a]
+                    #?(:clj (java.lang.Math/tan a) :cljs (js/Math.tan a)))
+      :atan       (fn [a]
+                    #?(:clj (java.lang.Math/atan a) :cljs (js/Math.atan a)))
+      :asin       (fn [a]
+                    #?(:clj (java.lang.Math/asin a) :cljs (js/Math.asin a)))
+      :acos       (fn [a]
+                    #?(:clj (java.lang.Math/acos a) :cljs (js/Math.acos a)))
+      :assignment (fn [var val]
+                    (swap! env assoc var val)
+                    val)
+      :toassign   str/trim
+      :variable   (fn [var]
+                    (let [var (str/trim var)]
+                      (or (get @env var)
+                          (throw (ex-info (util/format "Can't find variable %s" var)
+                                          {:var var})))))}
+     ast))))

+ 24 - 0
src/main/grammar/calc.bnf

@@ -0,0 +1,24 @@
+<start> = assignment | expr
+expr = add-sub
+<add-sub> = pow-log | mul-div | add | sub |  variable
+add = add-sub <'+'> mul-div
+sub = add-sub <'-'> mul-div
+<mul-div> = pow-log | mul | div
+mul = mul-div <'*'> pow-log
+div = mul-div <'/'> pow-log
+<trig> = sin | cos | tan | acos | asin | atan
+<pow-log> = term | pow | log | ln | trig
+pow = pow-log <'^'> term
+log = <#'\s*'> <'log('> expr <')'> <#'\s*'>
+ln = <#'\s*'> <'ln('> expr <')'> <#'\s*'>
+sin = <#'\s*'> <'sin('> expr <')'> <#'\s*'>
+cos = <#'\s*'> <'cos('> expr <')'> <#'\s*'>
+tan = <#'\s*'> <'tan('> expr <')'> <#'\s*'>
+atan = <#'\s*'> <'atan('> expr <')'> <#'\s*'>
+acos = <#'\s*'> <'acos('> expr <')'> <#'\s*'>
+asin = <#'\s*'> <'asin('> expr <')'> <#'\s*'>
+<term> = number | variable | <#'\s*'> <'('> expr <')'> <#'\s*'>
+number = #'\s*[0-9]+\.?[0-9]*()\s*'
+variable = #'\s*[a-zA-Z]+\s*'
+toassign =  #'\s*[a-zA-Z]+\s*'
+assignment = toassign <#'\s*'> <'='> <#'\s*'> expr

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

@@ -0,0 +1,87 @@
+(ns frontend.extensions.calc-test
+  (:require [clojure.test :as test :refer [deftest testing is are]]
+            [frontend.extensions.calc :as calc]))
+
+(defn run [expr]
+  {:pre [(string? expr)]}
+  (first (calc/eval (calc/parse expr))))
+
+(deftest basic-arithmetic
+  (testing "numbers are parsed as expected"
+    (are [value expr] (= value (run expr))
+      1          "1"
+      1          "   1  "
+      98123      "98123"
+      1.0        " 1.0 "
+      22.1124131 "22.1124131"
+      100.01231  " 100.01231 "))
+  (testing "basic operations work"
+    (are [value expr] (= value (run expr))
+      1             "1 + 0"
+      1             "1 + 1 - 1 "
+      3             "1+2"
+      3             " 1 +2 "
+      1             "(2-1 ) "
+      211           "100  + 111"
+      0             "1 + 2 + 3 + 4 + 5 -1-2-3-4-5"
+      1             "1 * 1"
+      2             "1*2"
+      9             " 3 *3"
+      1             " 2 * 3 / 3 / 2"
+      #?(:clj 1/2
+         :cljs 0.5) " 1 / 2"
+      0.5           " 1/ 2.0"))
+  (testing "power"
+    (are [value expr] (= value (run expr))
+      1.0   "1 ^ 0"
+      4.0   "2^2 "
+      27.0  " 3^ 3"
+      16.0  "2 ^ 2 ^ 2"
+      256.0 "4.000 ^ 4.0"))
+  (testing "operator precedence"
+    (are [value expr] (= value (run expr))
+      1     "1 + 0 * 2"
+      1     "2 * 1 - 1 "
+      4     "8 / 4 + 2 * 1 - 25 * 0 / 1"
+      14.0  "3 *2 ^ 2 + 1 * 2"
+      74.0  "((3*2) ^ 2 + 1) * 2"
+      432.0 "(3*2) ^ (2 + 1) * 2"
+      97.0  "(2 * 3) * 2 ^ (2 * 2) + 1"
+      4.0   "2 * 3 / 2 ^ 2 * 2 + 1"))
+  (testing "scientific functions"
+    (are [value expr] (= value (run expr))
+      1.0  "cos( 0 * 1 )"
+      0.0  "sin( 1 -1 )"
+      0.0  "atan(tan(0))"
+      1.0  "sin(asin(0)) + 1"
+      0.0  "acos(cos(0))"
+      5.0  "2 * log(10) + 3"
+      10.0 "ln(1) + 10")))
+
+(deftest variables
+  (testing "variables can be remembered"
+    (are [final-env expr] (let [env (calc/new-env)]
+                            (calc/eval env (calc/parse expr))
+                            (= final-env @env))
+      {"a" 1}        "a = 1"
+      {"variable" 1} "variable = 1 + 0 * 2"
+      {"x" 1}        "x= 2 * 1 - 1 "
+      {"y" 4}        "y =8 / 4 + 2 * 1 - 25 * 0 / 1"
+      {"zzz" 14.0}   "zzz=3 *2 ^ 2 + 1 * 2"
+      {"foo" 74.0}   "foo = (((3*2) ^ 2 + 1) * 2)"))
+  (testing "variables can be reused"
+    (are [final-env exprs] (let [env (calc/new-env)]
+                            (doseq [expr exprs]
+                              (calc/eval env (calc/parse expr)))
+                            (= final-env @env))
+      {"a" 1 "b" 2}          ["a = 1" "b = a + 1"]
+      {"variable" 1 "x" 0.0} ["variable = 1 + 0 * 2" "x = log(variable)"]
+      {"x" 1 "u" 23 "v" 24}  ["x= 2 * 1 - 1 " "23 + 54" "u= 23" "v = x + u"]))
+  (testing "variables can be rewritten"
+    (are [final-env exprs] (let [env (calc/new-env)]
+                             (doseq [expr exprs]
+                               (calc/eval env (calc/parse expr)))
+                             (= final-env @env))
+      {"a" 2}              ["a = 1" "a = 2"]
+      {"a" 2 "b" 2}        ["a = 1" "b = a + 1" "a = b"]
+      {"variable" 1 "x" 0} ["variable = 1 + 0 * 2" "x = log(variable)" "x = variable - 1"])))