diff --git a/README.md b/README.md index 9100fe5..d0ae153 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ Any expression can be used as an operand for any operator. axel-f has the same o - [x] [AND](https://support.office.com/en-us/article/and-function-5f19b2e8-e1df-4408-897a-ce285a19e9d9) - [x] [OR](https://support.office.com/en-us/article/or-function-7d17ad14-8700-4281-b308-00b131e22af0) - [x] [NOT](https://support.office.com/en-us/article/not-function-9cfc6011-a054-40c7-a140-cd4ba2d87d77) +- [x] [ARABIC](https://support.office.com/en-us/article/arabic-function-9a8da418-c17b-4ef9-a657-9370a30a674f) - [x] [CLEAN](https://support.office.com/en-us/article/clean-function-26f3d7c5-475f-4a9c-90e5-4b8ba987ba41) - [x] [CHAR](https://support.office.com/en-us/article/char-function-bbd249c8-b36e-4a91-8017-1c133f9b837a) - [x] [CODE](https://support.office.com/en-us/article/code-function-c32b692b-2ed0-4a04-bdd9-75640144b928) @@ -120,6 +121,9 @@ Any expression can be used as an operand for any operator. axel-f has the same o - [x] [LOWER](https://support.office.com/en-us/article/lower-function-3f21df02-a80c-44b2-afaf-81358f9fdeb4) - [x] [MID](https://support.office.com/en-us/article/mid-midb-functions-d5f9e25c-d7d6-472e-b568-4ecb12433028) - [x] [PROPER](https://support.office.com/en-us/article/proper-function-52a5a283-e8b2-49be-8506-b2887b889f94) +- [x] [REGEXEXTRACT](https://support.google.com/docs/answer/3098244?hl=en&ref_topic=3105625) +- [x] [REGEXMATCH](https://support.google.com/docs/answer/3098292?hl=en&ref_topic=3105625) +- [x] [REGEXREPLACE](https://support.google.com/docs/answer/3098245?hl=en&ref_topic=3105625) - [x] [REPLACE](https://support.office.com/en-us/article/replace-replaceb-functions-8d799074-2425-4a8a-84bc-82472868878a) - [x] [REPT](https://support.office.com/en-us/article/rept-function-04c4d778-e712-43b4-9c15-d656582bb061) - [x] [RIGHT](https://support.office.com/en-us/article/right-rightb-functions-240267ee-9afa-4639-a02b-f19e1786cf2f) @@ -127,8 +131,11 @@ Any expression can be used as an operand for any operator. axel-f has the same o - [x] [SEARCH](https://support.office.com/en-us/article/search-searchb-functions-9ab04538-0e55-4719-a72e-b6f54513b495) - [x] [SPLIT](https://support.google.com/docs/answer/3094136) - [x] [SUBSTITUTE](https://support.office.com/en-us/article/substitute-function-6434944e-a904-4336-a9b0-1e58df3bc332) +- [x] [T](https://support.office.com/en-us/article/t-function-fb83aeec-45e7-4924-af95-53e073541228) - [x] [TRIM](https://support.office.com/en-us/article/trim-function-410388fa-c5df-49c6-b16c-9e5630b479f9) - [x] [UPPER](https://support.office.com/en-us/article/upper-function-c11f29b3-d1a3-4537-8df6-04d0049963d6) +- [x] [VALUE](https://support.office.com/en-us/article/value-function-257d0108-07dc-437d-ae1c-bc2d3953d8c2) +- [x] [TEXTJOIN](https://support.office.com/en-us/article/textjoin-function-357b449a-ec91-49d0-80c3-0e8fc845691c) - [x] [COUNT](https://support.office.com/en-us/article/count-function-a59cd7fc-b623-4d93-87a4-d23bf411294c) - [x] [IF](https://support.office.com/en-us/article/if-function-69aed7c9-4e8a-4755-a9bc-aa8bbff73be2) @@ -145,7 +152,6 @@ In addition we have special functions for accessing the data in context: `OBJREF - [ ] [ADDRESS](https://support.office.com/en-us/article/address-function-d0c26c0d-3991-446b-8de4-ab46431d4f89) - [ ] [AMORDEGRC](https://support.office.com/en-us/article/amordegrc-function-a14d0ca1-64a4-42eb-9b3d-b0dededf9e51) - [ ] [AMORLINC](https://support.office.com/en-us/article/amorlinc-function-7d417b45-f7f5-4dba-a0a5-3451a81079a8) -- [ ] [ARABIC](https://support.office.com/en-us/article/arabic-function-9a8da418-c17b-4ef9-a657-9370a30a674f) - [ ] [AREAS](https://support.office.com/en-us/article/areas-function-8392ba32-7a41-43b3-96b0-3695d2ec6152) - [ ] [ASC](https://support.office.com/en-us/article/asc-function-0b6abf1c-c663-4004-a964-ebc00b723266) - [ ] [ASIN](https://support.office.com/en-us/article/asin-function-81fb95e5-6d6f-48c4-bc45-58f955c6d347) @@ -517,7 +523,6 @@ In addition we have special functions for accessing the data in context: `OBJREF - [ ] [SUMXMY2](https://support.office.com/en-us/article/sumxmy2-function-9d144ac1-4d79-43de-b524-e2ecee23b299) - [ ] [SWITCH](https://support.office.com/en-us/article/switch-function-47ab33c0-28ce-4530-8a45-d532ec4aa25e) - [ ] [SYD](https://support.office.com/en-us/article/syd-function-069f8106-b60b-4ca2-98e0-2a0f206bdb27) -- [ ] [T](https://support.office.com/en-us/article/t-function-fb83aeec-45e7-4924-af95-53e073541228) - [ ] [TAN](https://support.office.com/en-us/article/tan-function-08851a40-179f-4052-b789-d7f699447401) - [ ] [TANH](https://support.office.com/en-us/article/tanh-function-017222f0-a0c3-4f69-9787-b3202295dc6c) - [ ] [TBILLEQ](https://support.office.com/en-us/article/tbilleq-function-2ab72d90-9b4d-4efe-9fc2-0f81f2c19c8c) @@ -528,7 +533,6 @@ In addition we have special functions for accessing the data in context: `OBJREF - [ ] [T.DIST.RT](https://support.office.com/en-us/article/tdistrt-function-20a30020-86f9-4b35-af1f-7ef6ae683eda) - [ ] [TDIST](https://support.office.com/en-us/article/tdist-function-630a7695-4021-4853-9468-4a1f9dcdd192) - [ ] [TEXT](https://support.office.com/en-us/article/text-function-20d5ac4d-7b94-49fd-bb38-93d29371225c) -- [ ] [TEXTJOIN](https://support.office.com/en-us/article/textjoin-function-357b449a-ec91-49d0-80c3-0e8fc845691c) - [ ] [TIME](https://support.office.com/en-us/article/time-function-9a5aff99-8f7d-4611-845e-747d0b8d5457) - [ ] [TIMEVALUE](https://support.office.com/en-us/article/timevalue-function-0b615c12-33d8-4431-bf3d-f3eb6d186645) - [ ] [T.INV](https://support.office.com/en-us/article/tinv-function-2908272b-4e61-4942-9df9-a25fec9b0e2e) @@ -545,7 +549,6 @@ In addition we have special functions for accessing the data in context: `OBJREF - [ ] [TYPE](https://support.office.com/en-us/article/type-function-45b4e688-4bc3-48b3-a105-ffa892995899) - [ ] [UNICHAR](https://support.office.com/en-us/article/unichar-function-ffeb64f5-f131-44c6-b332-5cd72f0659b8) - [ ] [UNICODE](https://support.office.com/en-us/article/unicode-function-adb74aaa-a2a5-4dde-aff6-966e4e81f16f) -- [ ] [VALUE](https://support.office.com/en-us/article/value-function-257d0108-07dc-437d-ae1c-bc2d3953d8c2) - [ ] [VAR](https://support.office.com/en-us/article/var-function-1f2b7ab2-954d-4e17-ba2c-9e58b15a7da2) - [ ] [VAR.P](https://support.office.com/en-us/article/varp-function-73d1285c-108c-4843-ba5d-a51f90656f3a) - [ ] [VAR.S](https://support.office.com/en-us/article/vars-function-913633de-136b-449d-813e-65a00b2b990b) diff --git a/deps.edn b/deps.edn index b509d01..7a37c5b 100644 --- a/deps.edn +++ b/deps.edn @@ -19,7 +19,7 @@ :test {:extra-paths ["test"] :extra-deps {org.clojure/test.check {:mvn/version "RELEASE"}}} - :coverage {:extra-deps {cloverage {:mvn/version "1.0.11"}} + :coverage {:extra-deps {cloverage {:mvn/version "RELEASE"}} :main-opts ["-m" "cloverage.coverage" "--src-ns-path" "src" "--test-ns-path" "test" diff --git a/pom.xml b/pom.xml index 09558bc..89f6804 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.xapix axel-f - 0.2.1 + 0.2.2-SNAPSHOT axel-f diff --git a/release-js/package.json b/release-js/package.json index 11e730d..9d508a0 100644 --- a/release-js/package.json +++ b/release-js/package.json @@ -1,6 +1,6 @@ { "name": "axel-f", - "version": "0.2.1", + "version": "0.2.2-SNAPSHOT", "description": "axel-f is an engine that can evaluate excel-like expressions.", "homepage": "https://github.com/xapix-io/axel-f", "author": "Kirill Chernyshov (https://github.com/DeLaGuardo)", diff --git a/src/axel_f/error.cljc b/src/axel_f/error.cljc index 6f606d8..89cc015 100644 --- a/src/axel_f/error.cljc +++ b/src/axel_f/error.cljc @@ -13,3 +13,9 @@ arg-position (when arg-position " ") "expects number values. But '" value "' is a text and cannot be coerced to a number.")) + +(defn format-not-a-string-error [fnname arg-position value] + (str "Function " fnname " parameter " + arg-position + (when arg-position " ") + "expects text values. But '" value "' is a number and cannot be coerced to a string.")) diff --git a/src/axel_f/functions.cljc b/src/axel_f/functions.cljc index 8e383c3..104202e 100644 --- a/src/axel_f/functions.cljc +++ b/src/axel_f/functions.cljc @@ -66,6 +66,10 @@ :desc "Returns the text with the non-printable ASCII characters removed." :args [{:desc "The text whose non-printable characters are to be removed."}]} + "ARABIC" {:impl #'text/arabic-fn + :desc "Computes the value of a Roman numeral." + :args [{:desc "The Roman numeral to format, whose value must be between 1 and 3999, inclusive."}]} + "CHAR" {:impl #'text/char-fn :desc "Convert a number into a character according to the current Unicode table." :args [{:desc "The number of the character to look up from the current Unicode table in decimal format."}]} @@ -74,7 +78,7 @@ :desc "Returns the numeric Unicode map value of the first character in the string provided." :args [{:desc "The string whose first character's Unicode map value will be returned."}]} - "CONCATENATE" {:impl (partial text/join-fn "") + "CONCATENATE" {:impl (partial text/textjoin-fn "" false) :desc "Appends strings to one another." :args [{:desc "The initial string."} {:desc "More strings to append in sequence." @@ -99,7 +103,8 @@ {:desc "The character within arg2 at which to start the search." :opt true}]} - "JOIN" {:impl #'text/join-fn + "JOIN" {:impl (fn [delimeter & args] + (apply text/textjoin-fn delimeter false args)) :desc "Concatenates the elements of one or more one-dimensional arrays using a specified delimiter." :args [{:desc "The character or string to place between each concatenated value."} {:desc "The value or values to be appended using arg1."} @@ -131,6 +136,22 @@ :desc "Capitalizes each word in a specified string." :args [{:desc "The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase."}]} + "REGEXEXTRACT" {:impl #'text/regexextract-fn + :desc "Extracts matching substrings according to a regular expression." + :args [{:desc "The input text."} + {:desc "The first part of arg1 that matches this expression will be returned."}]} + + "REGEXMATCH" {:impl #'text/regexmatch-fn + :desc "Whether a piece of text matches a regular expression." + :args [{:desc "The text to be tested against the regular expression."} + {:desc "The regular expression to test the text against."}]} + + "REGEXREPLACE" {:impl #'text/regexreplace-fn + :desc "Replaces part of a text string with a different text string using regular expressions." + :args [{:desc "The text, a part of which will be replaced."} + {:desc "The regular expression. All matching instances in text will be replaced."} + {:desc "The text which will be inserted into the original text."}]} + "REPLACE" {:impl #'text/replace-fn :desc "Replaces part of a text string with a different text string." :args [{:desc "The text, a part of which will be replaced."} @@ -177,6 +198,10 @@ {:desc "The instance of arg2 within arg1 to replace with arg3. By default, all occurrences of arg2 are replaced; however, if arg4 is specified, only the indicated instance of arg2 is replaced." :opt true}]} + "T" {:impl #'text/t-fn + :desc "Returns string arguments as text." + :args [{:desc "The argument to be converted to text."}]} + "TRIM" {:impl #'text/trim-fn :desc "Removes leading, trailing, and repeated spaces in text." :args [{:desc "The text or reference to a cell containing text to be trimmed."}]} @@ -185,6 +210,19 @@ :desc "Converts a specified string to uppercase." :args [{:desc "The string to convert to uppercase."}]} + "VALUE" {:impl #'text/value-fn + :desc "Converts a string in any of the date, time or number formats that axel-f understands into a number." + :args [{:desc "The string containing the value to be converted."}]} + + "TEXTJOIN" {:impl #'text/textjoin-fn + :desc "Combines the text from multiple strings and/or arrays, with a specifiable delimiter separating the different texts." + :args [{:desc "A string, possibly empty, or a reference to a valid string. If empty, text will be simply concatenated."} + {:desc "A boolean; if TRUE, empty strings selected in the text arguments won't be included in the result."} + {:desc "Any text item. This could be a string, or an array of strings in a range."} + {:desc "Additional text item(s)." + :opt true + :repeatable true}]} + "COUNT" {:impl #'stat/count-fn :desc "Returns a count of the number of numeric values in a dataset." :args [{:desc "The first value or range to consider when counting."} diff --git a/src/axel_f/functions/text.cljc b/src/axel_f/functions/text.cljc index 3a60627..039b5be 100644 --- a/src/axel_f/functions/text.cljc +++ b/src/axel_f/functions/text.cljc @@ -21,8 +21,29 @@ (or (get cmap pattern) pattern) (string/escape pattern cmap)))) -;; TODO -;; (defn arabic-fn [roman-numeral]) +(def roman-numerals + {\I 1 \V 5 \X 10 \L 50 + \C 100 \D 500 \M 1000 "IV" 4 + "IX" 9 "XL" 40 "XC" 90 "CD" 400 + "CM" 900}) + +(defn arabic-fn [s] + (let [sign (if (string/starts-with? s "-") + -1 1) + s (partition-all 2 (vec (string/replace-first s #"-" "")))] + (loop [acc 0 current (first s) other (rest s)] + (if current + (let [is-pair? (get roman-numerals (apply str current)) + value (or is-pair? + (get roman-numerals (first current)))] + (if is-pair? + (recur (+ acc value) (first other) (rest other)) + (let [new-other (->> (concat current other) + flatten + rest + (partition-all 2))] + (recur (+ acc value) (first new-other) (rest new-other))))) + (* sign acc))))) ;; TODO ;; (defn asc-fn []) @@ -69,12 +90,6 @@ ;; TODO ;; (defn fixed-fn []) -(defn join-fn [delimeter & items] - (->> items - flatten - (map coercion/excel-str) - (string/join delimeter))) - (defn left-fn ([text] (left-fn text 1)) ([text number] @@ -113,14 +128,53 @@ (defn proper-fn [text] (string/replace (coercion/excel-str text) #"\w*" string/capitalize)) -;; TODO -;; (defn regexextract-fn []) +(defn regexextract-fn [text regular-expression] + (cond + (not (string? text)) + (throw (error/error "#VALUE!" + (error/format-not-a-string-error "REGEXEXTRACT" 1 text))) -;; TODO -;; (defn regexmatch-fn []) + (not (string? regular-expression)) + (throw (error/error "#VALUE!" + (error/format-not-a-string-error "REGEXEXTRACT" 2 regular-expression))) + + :otherwise + (let [res (re-find (re-pattern regular-expression) + text)] + (cond + (string? res) res + (vector? res) (second res) + :otherwise res)))) + +(defn regexmatch-fn [text regular-expression] + (cond + (not (string? text)) + (throw (error/error "#VALUE!" + (error/format-not-a-string-error "REGEXMATCH" 1 text))) -;; TODO -;; (defn regexreplace-fn []) + (not (string? regular-expression)) + (throw (error/error "#VALUE!" + (error/format-not-a-string-error "REGEXMATCH" 2 regular-expression))) + + :otherwise + (boolean (regexextract-fn text regular-expression)))) + +(defn regexreplace-fn [text regular-expression replacement] + (cond + (not (string? text)) + (throw (error/error "#VALUE!" + (error/format-not-a-string-error "REGEXREPLACE" 1 text))) + + (not (string? regular-expression)) + (throw (error/error "#VALUE!" + (error/format-not-a-string-error "REGEXREPLACE" 2 regular-expression))) + + (not (string? replacement)) + (throw (error/error "#VALUE!" + (error/format-not-a-string-error "REGEXREPLACE" 3 replacement))) + + :otherwise + (string/replace text (re-pattern regular-expression) replacement))) (defn replace-fn [text position length new-text] @@ -155,11 +209,7 @@ (if-let [n (coercion/excel-number n)] (if (<= 0 n 3999) (let [n (int n) - alphabet (sort-by val > - {\I 1 \V 5 \X 10 \L 50 - \C 100 \D 500 \M 1000 "IV" 4 - "IX" 9 "XL" 40 "XC" 90 "CD" 400 - "CM" 900})] + alphabet (sort-by val > roman-numerals)] (loop [res "" n n] (if (zero? n) res (let [[rom arab] (some #(when (<= (val %) n) %) alphabet)] @@ -215,8 +265,9 @@ (throw (error/error "#VALUE!" (error/format-not-a-number-error "SUBSTITUTE" 4 occurrence))))) -;; TODO -;; (defn t-fn []) +(defn t-fn [value] + (when (string? value) + value)) ;; TODO ;; (defn text-fn []) @@ -231,8 +282,20 @@ coercion/excel-str string/upper-case)) -;; TODO -;; (defn value-fn []) +(defn value-fn [s] + (or + (when (and (seqable? s) + (empty? s)) + 0) + (when-not (boolean? s) + (coercion/excel-number s)) + (throw (error/error "#VALUE!" (str "VALUE parameter '" (coercion/excel-str s) "' cannot be parsed to number."))))) -;; TODO -;; (defn textjoin-fn []) +(defn textjoin-fn [delimeter ignore-empty & items] + (->> items + flatten + (map coercion/excel-str) + (filter (if ignore-empty + not-empty + identity)) + (string/join delimeter))) diff --git a/test/axel_f/functions_test.cljc b/test/axel_f/functions_test.cljc index ddde55e..4be4841 100644 --- a/test/axel_f/functions_test.cljc +++ b/test/axel_f/functions_test.cljc @@ -283,6 +283,67 @@ (t/is (= "Foo-Bar.Baz" (sut/run "PROPER(\"foo-bar.baz\")"))))) +(t/deftest regexextract-function-test + (t/testing "REGEXEXTRACT function" + (t/is (= "826.25" + (sut/run "REGEXEXTRACT('The price today is $826.25', '[0-9]*\\.[0-9]+[0-9]+')"))) + + (t/is (= "Content" + (sut/run "REGEXEXTRACT('(Content) between brackets', '\\(([A-Za-z]+)\\)')"))) + + (t/is (= nil + (sut/run "REGEXEXTRACT('FOO', '[a-z]+')"))) + + (t/is (= {:type "#VALUE!" + :reason "Function REGEXEXTRACT parameter 1 expects text values. But '123' is a number and cannot be coerced to a string."} + (sut/run "REGEXEXTRACT(123, '123')"))) + + (t/is (= {:type "#VALUE!" + :reason "Function REGEXEXTRACT parameter 2 expects text values. But '123' is a number and cannot be coerced to a string."} + (sut/run "REGEXEXTRACT('123', 123)"))))) + +(t/deftest regexmatch-function-test + (t/testing "REGEXMATCH function" + (t/is (= true + (sut/run "REGEXMATCH('The price today is $826.25', '[0-9]*\\.[0-9]+[0-9]+')"))) + + (t/is (= true + (sut/run "REGEXMATCH('(Content) between brackets', '\\(([A-Za-z]+)\\)')"))) + + (t/is (= false + (sut/run "REGEXMATCH('FOO', '[a-z]+')"))) + + (t/is (= {:type "#VALUE!" + :reason "Function REGEXMATCH parameter 1 expects text values. But '123' is a number and cannot be coerced to a string."} + (sut/run "REGEXMATCH(123, '123')"))) + + (t/is (= {:type "#VALUE!" + :reason "Function REGEXMATCH parameter 2 expects text values. But '123' is a number and cannot be coerced to a string."} + (sut/run "REGEXMATCH('123', 123)"))))) + +(t/deftest regexreplace-function-test + (t/testing "REGEXREPLACE function" + (t/is (= "The price today is $0.00" + (sut/run "REGEXREPLACE('The price today is $826.25', '[0-9]*\\.[0-9]+[0-9]+', '0.00')"))) + + (t/is (= "Word between brackets" + (sut/run "REGEXREPLACE('(Content) between brackets', '\\(([A-Za-z]+)\\)', 'Word')"))) + + (t/is (= "FOO" + (sut/run "REGEXREPLACE('FOO', '[a-z]+', 'OOF')"))) + + (t/is (= {:type "#VALUE!" + :reason "Function REGEXREPLACE parameter 1 expects text values. But '123' is a number and cannot be coerced to a string."} + (sut/run "REGEXREPLACE(123, '123', '321')"))) + + (t/is (= {:type "#VALUE!" + :reason "Function REGEXREPLACE parameter 2 expects text values. But '123' is a number and cannot be coerced to a string."} + (sut/run "REGEXREPLACE('123', 123, '321')"))) + + (t/is (= {:type "#VALUE!" + :reason "Function REGEXREPLACE parameter 3 expects text values. But '321' is a number and cannot be coerced to a string."} + (sut/run "REGEXREPLACE('123', '123', 321)"))))) + (t/deftest replace-function-test (t/testing "REPLACE function" (t/is (= "abcde*k" @@ -325,6 +386,56 @@ (t/is (= "Price" (sut/run "RIGHT(\"Price\", 10)"))))) +(t/deftest arabic-function-test + (t/testing "ARABIC function" + + (t/are [x y] (= x (sut/run (str "ARABIC('" y "')"))) + 0 "" + 5 "V" + 9 "IX" + 12 "XII" + 16 "XVI" + 29 "XXIX" + 44 "XLIV" + 45 "XLV" + 68 "LXVIII" + 83 "LXXXIII" + 97 "XCVII" + 99 "XCIX" + 500 "D" + 501 "DI" + 649 "DCXLIX" + 798 "DCCXCVIII" + 891 "DCCCXCI" + 1000 "M" + 1004 "MIV" + 1006 "MVI" + 1023 "MXXIII" + 2014 "MMXIV" + 3999 "MMMCMXCIX" + -5 "-V" + -9 "-IX" + -12 "-XII" + -16 "-XVI" + -29 "-XXIX" + -44 "-XLIV" + -45 "-XLV" + -68 "-LXVIII" + -83 "-LXXXIII" + -97 "-XCVII" + -99 "-XCIX" + -500 "-D" + -501 "-DI" + -649 "-DCXLIX" + -798 "-DCCXCVIII" + -891 "-DCCCXCI" + -1000 "-M" + -1004 "-MIV" + -1006 "-MVI" + -1023 "-MXXIII" + -2014 "-MMXIV" + -3999 "-MMMCMXCIX"))) + (t/deftest roman-function-test (t/testing "ROMAN function" @@ -411,6 +522,14 @@ :reason "Function SUBSTITUTE parameter 4 expects number values. But 'baz' is a text and cannot be coerced to a number."} (sut/run "SUBSTITUTE(1, \"foo\", \"bar\", \"baz\")"))))) +(t/deftest t-function-text + (t/testing "T function" + + (t/is (= "foo" + (sut/run "T('foo')"))) + + (t/is (nil? (sut/run "T(123)"))))) + (t/deftest trim-function-test (t/testing "TRIM function" (t/is (= "more spaces" @@ -425,6 +544,22 @@ (sut/run "UPPER(1)"))) )) +(t/deftest value-function-test + (t/testing "VALUE function" + + (t/is (= 123.1 + (sut/run "VALUE('123.1')"))) + + (t/is (= 0 + (sut/run "VALUE('')"))) + + (t/is (= 0 + (sut/run "VALUE(0)"))) + + (t/is (= {:type "#VALUE!" + :reason "VALUE parameter 'TRUE' cannot be parsed to number."} + (sut/run "VALUE(TRUE)"))))) + (t/deftest count-function-test (let [context {:data [1 2 3]}] (t/testing "COUNT function"