From 48c1afc2a737ebc5c70be98d2c716b76b2ee0426 Mon Sep 17 00:00:00 2001 From: Marlena Meyer Date: Mon, 30 May 2022 12:37:19 +0200 Subject: [PATCH] Implement CQL ConvertsTo: Decimal, Integer, Long, Quantity and String --- modules/cql/src/blaze/elm/boolean.clj | 14 ++ modules/cql/src/blaze/elm/compiler/core.clj | 3 +- .../src/blaze/elm/compiler/type_operators.clj | 30 ++- modules/cql/src/blaze/elm/date_time.clj | 39 ++- modules/cql/src/blaze/elm/quantity.clj | 5 +- modules/cql/src/blaze/elm/string.clj | 6 + modules/cql/src/blaze/elm/tuple.clj | 8 +- .../elm/compiler/type_operators_test.clj | 237 +++++++++++++++++- modules/cql/test/blaze/elm/literal.clj | 25 ++ modules/cql/test/data_readers.clj | 5 + 10 files changed, 343 insertions(+), 29 deletions(-) diff --git a/modules/cql/src/blaze/elm/boolean.clj b/modules/cql/src/blaze/elm/boolean.clj index 4cbab8c6b..75f130d61 100644 --- a/modules/cql/src/blaze/elm/boolean.clj +++ b/modules/cql/src/blaze/elm/boolean.clj @@ -16,6 +16,13 @@ (to-boolean [x] x)) +;; 22.24. ToDecimal +(extend-protocol p/ToDecimal + Boolean + (to-decimal [x] + (if (true? x) 1.0 0.0))) + + ;; 22.25. ToInteger (extend-protocol p/ToInteger Boolean @@ -28,3 +35,10 @@ Boolean (to-long [x] (if (true? x) 1 0))) + + +;; 22.30. ToString +(extend-protocol p/ToString + Boolean + (to-string [x] + (str x))) diff --git a/modules/cql/src/blaze/elm/compiler/core.clj b/modules/cql/src/blaze/elm/compiler/core.clj index ef4694cd7..0bdc4e32c 100644 --- a/modules/cql/src/blaze/elm/compiler/core.clj +++ b/modules/cql/src/blaze/elm/compiler/core.clj @@ -109,5 +109,4 @@ (extend-protocol p/ToString Object - (to-string [x] - (str x))) + (to-string [_])) diff --git a/modules/cql/src/blaze/elm/compiler/type_operators.clj b/modules/cql/src/blaze/elm/compiler/type_operators.clj index be445ad52..a68c76237 100644 --- a/modules/cql/src/blaze/elm/compiler/type_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/type_operators.clj @@ -134,17 +134,37 @@ ;; TODO 22.9. ConvertsToDateTime -;; TODO 22.10. ConvertsToDecimal +;; 22.10. ConvertsToDecimal +(defunop converts-to-decimal [operand] + (when (some? operand) + (some? (p/to-decimal operand)))) + + +;; 22.11. ConvertsToLong +(defunop converts-to-long [operand] + (when (some? operand) + (some? (p/to-long operand)))) + + +;; 22.12. ConvertsToInteger +(defunop converts-to-integer [operand] + (when (some? operand) + (some? (p/to-integer operand)))) -;; TODO 22.11. ConvertsToLong -;; TODO 22.12. ConvertsToInteger +;; 22.13. ConvertsToQuantity +(defunop converts-to-quantity [operand] + (when (some? operand) + (some? (p/to-quantity operand)))) -;; TODO 22.13. ConvertsToQuantity ;; TODO 22.14. ConvertsToRatio -;; TODO 22.15. ConvertsToString +;; 22.15. ConvertsToString +(defunop converts-to-string [operand] + (when (some? operand) + (some? (p/to-string operand)))) + ;; TODO 22.16. ConvertsToTime diff --git a/modules/cql/src/blaze/elm/date_time.clj b/modules/cql/src/blaze/elm/date_time.clj index bd8670094..fb808c279 100644 --- a/modules/cql/src/blaze/elm/date_time.clj +++ b/modules/cql/src/blaze/elm/date_time.clj @@ -13,8 +13,7 @@ [blaze.fhir.spec.type OffsetInstant] [blaze.fhir.spec.type.system DateTimeYear DateTimeYearMonth DateTimeYearMonthDay] [java.time LocalDate LocalDateTime LocalTime OffsetDateTime Year YearMonth Instant] - [java.time.temporal ChronoField ChronoUnit Temporal TemporalAccessor] - [java.util Map])) + [java.time.temporal ChronoField ChronoUnit Temporal TemporalAccessor])) (set! *warn-on-reflection* true) @@ -1325,15 +1324,39 @@ String (to-date-time [s now] - (p/to-date-time (system/parse-date-time s) now)) - - ;; for the anomaly - Map - (to-date-time [_ _])) + (p/to-date-time (system/parse-date-time s) now))) ;; 22.30. ToString (extend-protocol p/ToString PrecisionLocalTime (to-string [{:keys [local-time]}] - (str local-time))) + (str local-time)) + + Year + (to-string [x] + (str x)) + + DateTimeYear + (to-string [x] + (str x)) + + YearMonth + (to-string [x] + (str x)) + + DateTimeYearMonth + (to-string [x] + (str x)) + + LocalDate + (to-string [x] + (str x)) + + DateTimeYearMonthDay + (to-string [x] + (str x)) + + LocalDateTime + (to-string [x] + (str x))) diff --git a/modules/cql/src/blaze/elm/quantity.clj b/modules/cql/src/blaze/elm/quantity.clj index 7792a56a9..d5793054d 100644 --- a/modules/cql/src/blaze/elm/quantity.clj +++ b/modules/cql/src/blaze/elm/quantity.clj @@ -223,9 +223,10 @@ String (to-quantity [s] ;; (+|-)?#0(.0#)?('')? - (let [[_ value unit] (re-matches #"(\d+(?:\.\d+)?)\s*('[^']+')?" s)] + (let [[_ value unit] (re-matches #"([+-]?\d+(?:\.\d+)?)\s*('[^']+')?" s)] (when value - (quantity (p/to-decimal value) (or (str/trim unit "'") "1")))))) + (when-let [value (p/to-decimal value)] + (quantity value (or (str/trim unit "'") "1"))))))) ;; 22.30. ToString diff --git a/modules/cql/src/blaze/elm/string.clj b/modules/cql/src/blaze/elm/string.clj index a630ff8b9..84f3ea707 100644 --- a/modules/cql/src/blaze/elm/string.clj +++ b/modules/cql/src/blaze/elm/string.clj @@ -52,3 +52,9 @@ (try (p/to-decimal (BigDecimal. s)) (catch Exception _)))) + +;; 22.30. ToString +(extend-protocol p/ToString + String + (to-string [s] + (str s))) diff --git a/modules/cql/src/blaze/elm/tuple.clj b/modules/cql/src/blaze/elm/tuple.clj index 65f5acf44..d8ee15747 100644 --- a/modules/cql/src/blaze/elm/tuple.clj +++ b/modules/cql/src/blaze/elm/tuple.clj @@ -5,7 +5,7 @@ (:import [java.util Map])) - +;; 12.1. Equal (extend-protocol p/Equal Map (equal [x y] @@ -17,3 +17,9 @@ false) false) true)))) + +;; 22.23. ToDateTime +(extend-protocol p/ToDateTime + ;; for the anomaly + Map + (to-date-time [_ _])) diff --git a/modules/cql/test/blaze/elm/compiler/type_operators_test.clj b/modules/cql/test/blaze/elm/compiler/type_operators_test.clj index edcc85f30..eec3eebd4 100644 --- a/modules/cql/test/blaze/elm/compiler/type_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/type_operators_test.clj @@ -14,6 +14,7 @@ [blaze.elm.literal-spec] [blaze.elm.protocols :as p] [blaze.elm.quantity :as quantity] + [blaze.elm.quantity-spec] [blaze.fhir.spec.type.system :as system] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [are deftest is testing]])) @@ -277,7 +278,7 @@ (is (= '(convert-quantity (param-ref "q") "g") (core/-form expr)))))) -;; TODO 22.7. ConvertsToBoolean +;; 22.7. ConvertsToBoolean ;; ;; The ConvertsToBoolean operator returns true if the value of its argument is ;; or can be converted to a Boolean value. @@ -429,7 +430,7 @@ ;; If the argument is null, the result is null. -;; TODO 22.10. ConvertsToDecimal +;; 22.10. ConvertsToDecimal ;; ;; The ConvertsToDecimal operator returns true if the value of its argument is ;; or can be converted to a Decimal value. The operator accepts strings using @@ -455,9 +456,44 @@ ;; If the input is a Boolean, the result is true. ;; ;; If the argument is null, the result is null. +(deftest compile-converts-to-decimal-test + (testing "String" + (are [x] (true? (tu/compile-unop elm/converts-to-decimal elm/string x)) + (str decimal/min) + "-1" + "0" + "1" + (str decimal/max)) + (are [x] (false? (tu/compile-unop elm/converts-to-decimal elm/string x)) + (str (- decimal/min 1e-8M)) + (str (+ decimal/max 1e-8M)) + "a")) -;; TODO 22.11. ConvertsToLong + (testing "Boolean" + (are [x] (true? (tu/compile-unop elm/converts-to-decimal elm/boolean x)) + "true")) + + (testing "Decimal" + (are [x] (true? (tu/compile-unop elm/converts-to-decimal elm/decimal x)) + "1.1")) + + (testing "dynamic" + (are [x] (false? (tu/dynamic-compile-eval (elm/converts-to-decimal x))) + #elm/parameter-ref "A") + (are [x] (true? (tu/dynamic-compile-eval (elm/converts-to-decimal x))) + #elm/parameter-ref "1")) + + (tu/testing-unary-null elm/converts-to-decimal) + + (testing "form" + (let [compile-ctx {:library {:parameters {:def [{:name "x"}]}}} + elm #elm/converts-to-decimal #elm/parameter-ref "x" + expr (c/compile compile-ctx elm)] + (is (= '(converts-to-decimal (param-ref "x")) (core/-form expr)))))) + + +;; 22.11. ConvertsToLong ;; ;; The ConvertsToLong operator returns true if the value of its argument is or ;; can be converted to a Long value. The operator accepts strings using the @@ -480,8 +516,44 @@ ;; If the input is a Boolean, the result is true. ;; ;; If the argument is null, the result is null. +(deftest compile-converts-to-long-test + (testing "String" + (are [x] (true? (tu/compile-unop elm/converts-to-long elm/string x)) + (str Long/MIN_VALUE) + "-1" + "0" + "1" + (str Long/MAX_VALUE)) + + (are [x] (false? (tu/compile-unop elm/converts-to-long elm/string x)) + (str (dec (bigint Long/MIN_VALUE))) + (str (inc (bigint Long/MAX_VALUE))) + "a")) + + (testing "Boolean" + (are [x] (true? (tu/compile-unop elm/converts-to-long elm/boolean x)) + "true")) + + (testing "Long" + (are [x] (true? (tu/compile-unop elm/converts-to-long elm/long x)) + "1")) + + (testing "dynamic" + (are [x] (false? (tu/dynamic-compile-eval (elm/converts-to-long x))) + #elm/parameter-ref "A") + (are [x] (true? (tu/dynamic-compile-eval (elm/converts-to-long x))) + #elm/parameter-ref "1")) + + (tu/testing-unary-null elm/converts-to-long) + + (testing "form" + (let [compile-ctx {:library {:parameters {:def [{:name "x"}]}}} + elm #elm/converts-to-long #elm/parameter-ref "x" + expr (c/compile compile-ctx elm)] + (is (= '(converts-to-long (param-ref "x")) (core/-form expr)))))) + -;; TODO 22.12. ConvertsToInteger +;; 22.12. ConvertsToInteger ;; ;; The ConvertsToInteger operator returns true if the value of its argument is ;; or can be converted to an Integer value. The operator accepts strings using @@ -504,8 +576,43 @@ ;; If the input is a Boolean, the result is true ;; ;; If the argument is null, the result is null. +(deftest compile-converts-to-integer-test + (testing "String" + (are [x] (true? (tu/compile-unop elm/converts-to-integer elm/string x)) + (str Integer/MIN_VALUE) + "-1" + "0" + "1" + (str Integer/MAX_VALUE)) + (are [x] (false? (tu/compile-unop elm/converts-to-integer elm/string x)) + (str (dec Integer/MIN_VALUE)) + (str (inc Integer/MAX_VALUE)) + "a")) + + (testing "Boolean" + (are [x] (true? (tu/compile-unop elm/converts-to-integer elm/boolean x)) + "true")) -;; TODO 22.13. ConvertsToQuantity + (testing "Integer" + (are [x] (true? (tu/compile-unop elm/converts-to-integer elm/integer x)) + "1")) + + (testing "dynamic" + (are [x] (false? (tu/dynamic-compile-eval (elm/converts-to-integer x))) + #elm/parameter-ref "A") + (are [x] (true? (tu/dynamic-compile-eval (elm/converts-to-integer x))) + #elm/parameter-ref "1")) + + (tu/testing-unary-null elm/converts-to-integer) + + (testing "form" + (let [compile-ctx {:library {:parameters {:def [{:name "x"}]}}} + elm #elm/converts-to-integer #elm/parameter-ref "x" + expr (c/compile compile-ctx elm)] + (is (= '(converts-to-integer (param-ref "x")) (core/-form expr)))))) + + +;; 22.13. ConvertsToQuantity ;; ;; The ConvertsToQuantity operator returns true if the value of its argument is ;; or can be converted to a Quantity value. The operator may be used with @@ -534,6 +641,45 @@ ;; For Integer, Decimal, and Ratio values, the operator simply returns true. ;; ;; If the argument is null, the result is null. +(deftest compile-converts-to-quantity-test + (testing "String" + (are [x] (true? (tu/compile-unop elm/converts-to-quantity elm/string x)) + (str decimal/min "'m'") + "-1'm'" + "0'm'" + "1'm'" + (str decimal/max "'m'")) + + (are [x] (false? (tu/compile-unop elm/converts-to-quantity elm/string x)) + (str (- decimal/min 1e-8M)) + (str (+ decimal/max 1e-8M)) + (str (- decimal/min 1e-8M) "'m'") + (str (+ decimal/max 1e-8M) "'m'") + "" + "a")) + + (testing "Integer" + (is (true? (tu/compile-unop elm/converts-to-quantity elm/integer "1")))) + + (testing "Decimal" + (is (true? (tu/compile-unop elm/converts-to-quantity elm/decimal "1.1")))) + + ;; TODO: Ratio + + (testing "dynamic" + (are [x] (false? (tu/dynamic-compile-eval (elm/converts-to-quantity x))) + #elm/parameter-ref "A") + (are [x] (true? (tu/dynamic-compile-eval (elm/converts-to-quantity x))) + #elm/parameter-ref "1")) + + (tu/testing-unary-null elm/converts-to-quantity) + + (testing "form" + (let [compile-ctx {:library {:parameters {:def [{:name "x"}]}}} + elm #elm/converts-to-quantity #elm/parameter-ref "x" + expr (c/compile compile-ctx elm)] + (is (= '(converts-to-quantity (param-ref "x")) (core/-form expr)))))) + ;; TODO 22.14. ConvertsToRatio ;; @@ -552,7 +698,7 @@ ;; ;; If the argument is null, the result is null. -;; TODO 22.15. ConvertsToString +;; 22.15. ConvertsToString ;; ;; The ConvertsToString operator returns true if the value of its argument is ;; or can be converted to a String value. @@ -571,6 +717,60 @@ ;; String ;; ;; If the argument is null, the result is null. +(deftest compile-converts-to-string-test + (testing "String" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/string x)) + "foo")) + + (testing "Long" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/long x)) + "1")) + + (testing "Boolean" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/boolean x)) + "true")) + + (testing "Integer" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/integer x)) + "1")) + + (testing "Decimal" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/decimal x)) + "1.1")) + + (testing "Quantity" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/quantity x)) + [1M "m"])) + + (testing "Date" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/date x)) + "2019-01-01")) + + (testing "DateTime" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/date-time x)) + "2019-01-01T01:00")) + + (testing "Time" + (are [x] (true? (tu/compile-unop elm/converts-to-string elm/time x)) + "01:00")) + + ;; TODO: Ratio + + (testing "Tuple" + (are [x] (false? (c/compile {} (elm/converts-to-string (elm/tuple x)))) + {"foo" #elm/integer "1"})) + + (testing "dynamic" + (are [x] (true? (tu/dynamic-compile-eval (elm/converts-to-string x))) + #elm/parameter-ref "A")) + + (tu/testing-unary-null elm/converts-to-string) + + (testing "form" + (let [compile-ctx {:library {:parameters {:def [{:name "x"}]}}} + elm #elm/converts-to-string #elm/parameter-ref "x" + expr (c/compile compile-ctx elm)] + (is (= '(converts-to-string (param-ref "x")) (core/-form expr)))))) ;; TODO 22.16. ConvertsToTime ;; @@ -881,8 +1081,8 @@ ;; 22.24. ToDecimal ;; -;; The ToDecimal operator converts the value of its argument to a Decimal value. -;; The operator accepts strings using the following format: +;; The ToDecimal operator converts the value of its argument to a Decimal +;; value. The operator accepts strings using the following format: ;; ;; (+|-)?#0(.0#)? ;; @@ -891,13 +1091,18 @@ ;; decimal point, at least one digit, and any number of additional digits ;; (including none). ;; -;; Note that the decimal value returned by this operator must be limited in -;; precision and scale to the maximum precision and scale representable for -;; Decimal values within CQL. +;; See Formatting Strings for a description of the formatting strings used in +;; this specification. +;; +;; Note that the Decimal value returned by this operator will be limited in +;; precision and scale to the maximum precision and scale representable by the +;; implementation (at least 28 digits of precision, and 8 digits of scale). ;; ;; If the input string is not formatted correctly, or cannot be interpreted as ;; a valid Decimal value, the result is null. ;; +;; If the input is Boolean, true will result in 1.0, false will result in 0.0. +;; ;; If the argument is null, the result is null. (deftest compile-to-decimal-test (testing "String" @@ -913,6 +1118,11 @@ (str (+ decimal/max 1e-8M)) nil "a" nil)) + (testing "Boolean" + (are [x res] (= res (tu/compile-unop elm/to-decimal elm/boolean x)) + "true" 1.0 + "false" 0.0)) + (tu/testing-unary-null elm/to-decimal) (testing "form" @@ -1086,6 +1296,7 @@ (are [x res] (p/equal res (core/-eval (tu/compile-unop elm/to-quantity elm/string x) {} nil nil)) + "-1" (quantity/quantity -1 "1") "1" (quantity/quantity 1 "1") "1'm'" (quantity/quantity 1 "m") @@ -1098,6 +1309,10 @@ (are [x] (nil? (core/-eval (tu/compile-unop elm/to-quantity elm/string x) {} nil nil)) + (str (- decimal/min 1e-8M)) + (str (+ decimal/max 1e-8M)) + (str (- decimal/min 1e-8M) "'m'") + (str (+ decimal/max 1e-8M) "'m'") "" "a")) diff --git a/modules/cql/test/blaze/elm/literal.clj b/modules/cql/test/blaze/elm/literal.clj index 405ff5aa1..fd4a48676 100644 --- a/modules/cql/test/blaze/elm/literal.clj +++ b/modules/cql/test/blaze/elm/literal.clj @@ -803,6 +803,31 @@ {:type "ConvertsToBoolean" :operand operand}) +;; 22.10. ConvertsToDecimal +(defn converts-to-decimal [operand] + {:type "ConvertsToDecimal" :operand operand}) + + +;; 22.11. ConvertsToLong +(defn converts-to-long [operand] + {:type "ConvertsToLong" :operand operand}) + + +;; 22.12. ConvertsToInteger +(defn converts-to-integer [operand] + {:type "ConvertsToInteger" :operand operand}) + + +;; 22.13. ConvertsToQuantity +(defn converts-to-quantity [operand] + {:type "ConvertsToQuantity" :operand operand}) + + +;; 22.15. ConvertsToString +(defn converts-to-string [operand] + {:type "ConvertsToString" :operand operand}) + + ;; 22.17. Descendents (defn descendents [source] {:type "Descendents" :source source}) diff --git a/modules/cql/test/data_readers.clj b/modules/cql/test/data_readers.clj index a004caa95..d501bc3b4 100644 --- a/modules/cql/test/data_readers.clj +++ b/modules/cql/test/data_readers.clj @@ -71,6 +71,11 @@ elm/can-convert-quantity blaze.elm.literal/can-convert-quantity elm/convert-quantity blaze.elm.literal/convert-quantity elm/converts-to-boolean blaze.elm.literal/converts-to-boolean + elm/converts-to-decimal blaze.elm.literal/converts-to-decimal + elm/converts-to-long blaze.elm.literal/converts-to-long + elm/converts-to-integer blaze.elm.literal/converts-to-integer + elm/converts-to-quantity blaze.elm.literal/converts-to-quantity + elm/converts-to-string blaze.elm.literal/converts-to-string elm/children blaze.elm.literal/children elm/descendents blaze.elm.literal/descendents elm/to-boolean blaze.elm.literal/to-boolean