diff --git a/README.md b/README.md index ed713ba..fa97802 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ There is a version hosted at [Clojars](https://clojars.org/rm-hull/infix). For leiningen include a dependency: ```clojure -[rm-hull/infix "0.1.2"] +[rm-hull/infix "0.2.0"] ``` For maven-based projects, add the following to your `pom.xml`: @@ -38,21 +38,21 @@ For maven-based projects, add the following to your `pom.xml`: rm-hull infix - 0.1.2 + 0.2.0 ``` ## Basic Usage ```clojure -(refer 'infix.macros :only '[infix]) -=> nil +(refer 'infix.macros :only '[infix from-string]) +; => nil (infix 3 + 5 * 8) -=> 43 +; => 43 (infix (3 + 5) * 8) -=> 64 +; => 64 ``` Some `Math` functions have been aliased (see [below](#aliased-operators--functions) for full list), so single argument functions can be @@ -60,38 +60,89 @@ used as follows: ```clojure (infix √(5 * 5)) -=> 5.0 +; => 5.0 (infix √ 121) -=> 11.0 +; => 11.0 (infix 2 ** 6) -=> 64.0 +; => 64.0 (def t 0.324) -=> #'user/t +; => #'user/t (infix sin(2 * t) + 3 * cos(4 * t)) -=> 1.4176457261295824 +; => 1.4176457261295824 (macroexpand-1 '(infix sin(2 * t) + 3 * cos(4 * t)) -=> (+ (Math/sin (* 2 t)) (* 3 (Math/cos (* 4 t)))) +; => (+ (Math/sin (* 2 t)) (* 3 (Math/cos (* 4 t)))) ``` +### Evaluating infix expressions from a string + +A function can created at runtime from an expression held in a string as +follows. When building from a string, a number of binding arguments should be +supplied, corresponding to any variable that may be used in the string +expression, for example: + +```clojure +(def hypot (from-string "sqrt(x**2 + y**2)" x y)) +; => #'user/hypot + +(hypot 3 4) +; => 5 +``` + +In effect, this is equivalent to creating the following function: + +```clojure +(defn hypot [x y] + (infix sqrt(x ** 2 + y ** 2))) +``` + +However, it does so with recourse to `eval` and `read-string` - instead it is +built using a custom infix parser-combinator with a restricted base environment +of math functions, as outlined in the next section. + +Functions may be passed where they are not in the base environment, e.g.: + +```clojure +(defn rad + "Calculate the radians for the supplied degrees" + [deg] + (infix deg * π / 180)) +; => user/rad + +(def rhs-triangle-height + (from-string "tan(rad(angle)) * base" base angle rad) +; => user/rhs-triangle-height + +(rhs-triangle-height 10 45 rad) +; => 9.9999999999998 +``` + +> **TODO:** allow base env to be extended and passed in, possibly +> along the lines of `with-redefs`. + ### Aliased Operators & Functions -| Alias | Operator | | Alias | Operator | | Alias | Operator | -|--------|-----------------|---|--------|-----------------|---|--------|-----------------| -| && | and | | abs | Math/abs | | sin | Math/sin | -| \|\| | or | | signum | Math/signum | | cos | Math/cos | -| == | = | | ** | Math/pow | | tan | Math/tan | -| != | not= | | exp | Math/exp | | asin | Math/asin | -| % | mod | | log | Math/log | | acos | Math/acos | -| << | bit-shift-left | | e | Math/E | | atan | Math/atan | -| >> | bit-shift-right | | π | Math/PI | | sinh | Math/sinh | -| ! | not | | sqrt | Math/sqrt | | cosh | Math/cosh | -| & | bit-and | | √ | Math/sqrt | | tanh | Math/tanh | -| \| | bit-or | | . | * | | | | +| Alias | Operator | | Alias | Operator | | Alias | Operator | +|--------|------------------------|---|--------|-----------------|---|--------|-----------------| +| && | and | | abs | Math/abs | | sin | Math/sin | +| \|\| | or | | signum | Math/signum | | cos | Math/cos | +| == | = | | ** | Math/pow | | tan | Math/tan | +| != | not= | | exp | Math/exp | | asin | Math/asin | +| % | mod | | log | Math/log | | acos | Math/acos | +| << | bit-shift-left | | e | Math/E | | atan | Math/atan | +| >> | bit-shift-right | | π | Math/PI | | sinh | Math/sinh | +| ! | not | | sqrt | Math/sqrt | | cosh | Math/cosh | +| & | bit-and | | √ | Math/sqrt | | tanh | Math/tanh | +| \| | bit-or | | root | b √ a | | sec | Secant | +| | | | φ | Golden ratio | | csc | Cosecant | +| gcd | Greatest common divsor | | fact | Factorial | | cot | Cotangent | +| lcm | Least common multiple | | ∑ | Sum | | asec | Arcsecant | +| | | | ∏ | Product | | acsc | Arccosecant | +| | | | | | | acot | Arccotangent | ## References diff --git a/project.clj b/project.clj index 4c3c8ef..3387a8a 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject rm-hull/infix "0.1.2"` +(defproject rm-hull/infix "0.2.0"` :description "A small Clojure/ClojureScript library for expressing LISP expressions as infix rather than prefix notation" :url "https://github.com/rm-hull/infix" :license { diff --git a/src/infix/grammar.clj b/src/infix/grammar.clj index 7c8a7ea..84b21b0 100644 --- a/src/infix/grammar.clj +++ b/src/infix/grammar.clj @@ -44,6 +44,7 @@ (def digit (from-re #"[0-9]")) +; TODO: allow unicode/utf8 characters (def letter (from-re #"[a-zA-Z]")) (def alpha-num (any-of letter digit (match "_") (match "-"))) @@ -175,20 +176,3 @@ (return (fn [env] ((op env) (t1 env) (t2 env))))))) - -(def base-env - (merge - ; wrapped java.lang.Math constants & functions - (->> - (ns-publics 'infix.math) - (map (fn [[k v]] (vector (keyword k) v))) - (into {})) - - ; Basic ops - { - :+ + - :- - - :* * - :/ / - :% mod - })) diff --git a/src/infix/macros.clj b/src/infix/macros.clj index 6c0620b..96689a2 100644 --- a/src/infix/macros.clj +++ b/src/infix/macros.clj @@ -21,7 +21,11 @@ ;; SOFTWARE. -(ns infix.macros) +(ns infix.macros + (:require + [infix.parser :refer [parse-all]] + [infix.grammar :refer [expression]] + [infix.math :as m])) (def operator-alias {'&& 'and @@ -47,12 +51,27 @@ 'sinh 'Math/sinh 'cosh 'Math/cosh 'tanh 'Math/tanh + 'sec 'm/sec + 'csc 'm/csc + 'cot 'm/cot + 'asec 'm/asec + 'acsc 'm/acsc + 'acot 'm/acot 'exp 'Math/exp 'log 'Math/log 'e 'Math/E 'π 'Math/PI + 'φ 'm/φ 'sqrt 'Math/sqrt - '√ 'Math/sqrt }) + '√ 'Math/sqrt + 'root 'm/root + 'gcd 'm/gcd + 'lcm 'm/lcm + 'fact 'm/fact + 'sum 'm/sum + '∑ 'm/sum + 'product 'm/product + '∏ 'm/product }) (def operator-precedence ; From https://en.wikipedia.org/wiki/Order_of_operations#Programming_languages @@ -107,3 +126,28 @@ infix expressions into standard LISP prefix expressions." [& expr] (-> expr resolve-aliases rewrite)) + +(def base-env + (merge + ; wrapped java.lang.Math constants & functions + (->> + (ns-publics 'infix.math) + (map (fn [[k v]] (vector (keyword k) v))) + (into {})) + + ; Basic ops + { + :+ + + :- - + :* * + :/ / + :% mod + })) + +(defmacro from-string [expr & bindings] + `(if-let [f# (parse-all expression ~expr)] + (with-meta + (fn [~@bindings] + (f# ~(into base-env (map #(vector (keyword %) %) bindings)))) + {:doc ~expr}) + (throw (java.text.ParseException. (str "Failed to parse expression: '" ~expr "'") 0)))) diff --git a/src/infix/math.clj b/src/infix/math.clj index e8d0441..a405997 100644 --- a/src/infix/math.clj +++ b/src/infix/math.clj @@ -20,9 +20,7 @@ ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ;; SOFTWARE. -(ns infix.math - (:require - [infix.macros :refer [infix]])) +(ns infix.math) (defmacro ^:private defunary [func-name & [alias]] (let [arg (gensym "x__")] @@ -58,7 +56,7 @@ (defbinary pow) (defbinary pow **) -(def φ (infix (1 + √ 5) / 2)) +(def φ (/ (inc (√ 5)) 2)) (def e Math/E) (def π Math/PI) (def pi Math/PI) @@ -77,7 +75,7 @@ (defn lcm [a b] (/ (* a b) (gcd a b))) -(defn factorial [n] +(defn fact [n] (if (zero? n) 1 (apply * (range 1 (inc n))))) diff --git a/test/infix/grammar_test.clj b/test/infix/grammar_test.clj index c658876..f3869c6 100644 --- a/test/infix/grammar_test.clj +++ b/test/infix/grammar_test.clj @@ -25,7 +25,8 @@ (:require [clojure.test :refer :all] [infix.grammar :refer :all] - [infix.parser :refer [parse-all]])) + [infix.parser :refer [parse-all]] + [infix.macros :refer [base-env]])) (defn float= ([x y] (float= x y 0.00001)) @@ -120,10 +121,8 @@ (is (float= (Math/atan (/ 1 0.21)) ((parse-all expression "acot(0.21)") base-env))) (is (float= (+ 1 2 5.7 4) ((parse-all expression "sum(1, 2, 5.7, 4)") base-env))) (is (float= (* 1 2 5.7 4) ((parse-all expression "product(1, 2, 5.7, 4)") base-env))) - (is (= 1 ((parse-all expression "factorial 0") base-env))) - (is (= 120 ((parse-all expression "factorial 5") base-env))) + (is (= 1 ((parse-all expression "fact 0") base-env))) + (is (= 120 ((parse-all expression "fact 5") base-env))) (is (= 4 ((parse-all expression "gcd(8, 12)") base-env))) - (is (= 24 ((parse-all expression "lcm(8, 12)") base-env))) - - ) + (is (= 24 ((parse-all expression "lcm(8, 12)") base-env)))) diff --git a/test/infix/macros_tests.clj b/test/infix/macros_tests.clj index 6fe6115..e2ffa49 100644 --- a/test/infix/macros_tests.clj +++ b/test/infix/macros_tests.clj @@ -22,8 +22,9 @@ (ns infix.macros-tests - (:use [clojure.test] - [infix.macros])) + (:require + [clojure.test :refer :all] + [infix.macros :refer [infix from-string]])) (def ε 0.0000001) @@ -52,3 +53,12 @@ y 3 ] (is (= 12 (infix x . y))) (is (= 64.0 (infix x ** y))))) + +(deftest check-from-string + (is (= 7 ((from-string "x + 3" x) 4))) + (is (thrown-with-msg? java.text.ParseException #"Failed to parse expression: 'x \+ '" + ((from-string "x + ") 3))) + (is (thrown-with-msg? clojure.lang.ArityException #"Wrong number of args \(2\) passed to: .*" + ((from-string "x + 3") 2 3))) + (is (thrown-with-msg? IllegalStateException #"x is not bound in environment" + ((from-string "x + 3")))))