;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns app.util.simple-math (:require [app.common.data :as d] [app.common.exceptions :as ex] [cljs.spec.alpha :as s] [clojure.string :refer [index-of]] [cuerdas.core :as str] [instaparse.core :as insta])) (def parser (insta/parser "opt-expr = '' | expr expr = term ( ('+'|'-') expr)* | ('+'|'-'|'*'|'/') factor term = factor ( ('*'|'/') term)* factor = number | ('(' expr ')') number = #'[0-9]*[.,]?[0-9]+%?' spaces = ' '*")) (defn interpret [tree init-value] (let [token (first tree) args (rest tree)] (case token :opt-expr (if (empty? args) nil (interpret (first args) init-value)) :expr (if (index-of "+-*/" (first args)) (let [operator (first args) second-value (interpret (second args) init-value)] (case operator "+" (+ init-value second-value) "-" (- 0 second-value) ;; Note that there is ambiguity, so we don't allow "*" (* init-value second-value) ;; relative substraction, it's only a negative number "/" (/ init-value second-value))) (let [value (interpret (first args) init-value)] (loop [value value rest-expr (rest args)] (if (empty? rest-expr) value (let [operator (first rest-expr) second-value (interpret (second rest-expr) init-value) rest-expr (-> rest-expr rest rest)] (case operator "+" (recur (+ value second-value) rest-expr) "-" (recur (- value second-value) rest-expr))))))) :term (let [value (interpret (first args) init-value)] (loop [value value rest-expr (rest args)] (if (empty? rest-expr) value (let [operator (first rest-expr) second-value (interpret (second rest-expr) init-value) rest-expr (-> rest-expr rest rest)] (case operator "*" (recur (* value second-value) rest-expr) "/" (recur (/ value second-value) rest-expr)))))) :factor (if (= (first args) "(") (interpret (second args) init-value) (interpret (first args) init-value)) :number (let [value-str (str/replace (first args) "," ".")] (if-not (str/ends-with? value-str "%") (d/parse-double value-str) (-> value-str (str/replace "%" "") (d/parse-double) (/ 100) (* init-value)))) (ex/raise :type :validation :hint (str "Unknown token" token args))))) (defn expr-eval [expr init-value] (s/assert string? expr) (let [result (parser expr) init-value (or init-value 0)] (s/assert number? init-value) (if-not (insta/failure? result) (interpret result init-value) (let [text (:text result) index (:index result) expecting (->> result :reason (map :expecting) (filter some?))] (js/console.debug (str "Invalid value '" text "' at index " index ". Expected one of " expecting ".")) nil))))