diff --git a/CHANGELOG.md b/CHANGELOG.md index e36d8a8c..31e417d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ This is a history of changes to k13labs/clara-rules. +# 1.5.2 +* implement sorting-by and sorted-grouping-by accumulators + # 1.5.1 * do not break inspection if no accumulated facts are found diff --git a/pom.xml b/pom.xml index 4fecccf5..64691a5c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,9 +5,9 @@ com.github.k13labs clara-rules clara-rules - 1.5.1 + 1.5.2 - 1.5.1 + 1.5.2 https://github.com/k13labs/clara-rules scm:git:git://github.com/k13labs/clara-rules.git scm:git:ssh://git@github.com/k13labs/clara-rules.git diff --git a/src/main/clojure/clara/rules/accumulators.clj b/src/main/clojure/clara/rules/accumulators.clj index f71b1663..d1e3fc3e 100644 --- a/src/main/clojure/clara/rules/accumulators.clj +++ b/src/main/clojure/clara/rules/accumulators.clj @@ -211,3 +211,39 @@ {:initial-value [] :reduce-fn (fn [items value] (conj items (field value))) :retract-fn (fn [items retracted] (drop-one-of items (field retracted)))}))) + +(defn sorting-by + "Return a generic sorting accumulator. Behaves like clojure.core/sort-by. + * `field` - required - The field of a fact to sort by. + * `comparator` - optional - The comparator for sort by, defaults to `clojure.core/compare`. + * `convert-return-fn` - optional - Converts the resulting sorted data. Defaults to clojure.core/identity." + [field & {:keys [comparator + convert-return-fn] + :or {comparator compare + convert-return-fn identity}}] + {:pre [(ifn? convert-return-fn)]} + (assoc (all) :convert-return-fn + (comp convert-return-fn (fn do-sort + [return-items] + (sort-by field comparator return-items))))) + +(defn sorted-grouping-by + "Return a generic sorted grouping accumulator. Behaves like clojure.core/group-by into a map + as if created by `clojure.core/sorted-map-by`, and each group of values is sorted as if by `clojure.core/sort-by`. + * `group-field` - required - The field of a fact to group facts by. + * `sort-field` - required - The field of a fact to sort facts by. + * `group-comparator` - optional - The comparator to compare sorted groups, defaults to `clojure.core/compare`. + * `sort-comparator` - optional - The comparator to compare sorted values, defaults to `clojure.core/compare`. + * `convert-return-fn` - optional - Converts the resulting grouped data. Defaults to clojure.core/identity." + [group-field sort-field & {:keys [group-comparator + sort-comparator + convert-return-fn] + :or {group-comparator compare + sort-comparator compare + convert-return-fn identity}}] + {:pre [(ifn? convert-return-fn)]} + (update (grouping-by group-field convert-return-fn) + :convert-return-fn comp (fn do-sort [m] + (into (sorted-map-by group-comparator) + (for [[k vs] m] + [k (sort-by sort-field sort-comparator vs)]))))) diff --git a/src/test/clojure/clara/test_accumulators.clj b/src/test/clojure/clara/test_accumulators.clj index e6feac7d..6ac06051 100644 --- a/src/test/clojure/clara/test_accumulators.clj +++ b/src/test/clojure/clara/test_accumulators.clj @@ -536,3 +536,208 @@ (is (and (= (count (query session wind-query)) 1) (= [(->WindSpeed 75 "KCI")] wind-facts) (seq? wind-facts)))))) + +(def-rules-test test-accum-sorting-by + {:queries [sorted-all [[] [[?t <- (acc/sorting-by :temperature) from [Temperature]]]] + sorted-all-desc [[] [[?t <- (acc/sorting-by :temperature :comparator >) :from [Temperature]]]] + sorted-all-conv [[] [[?t <- (acc/sorting-by :temperature :convert-return-fn #(map :temperature %)) :from [Temperature]]]]] + + :sessions [empty-session [sorted-all sorted-all-desc sorted-all-conv] {}]} + + (let [shuffled-facts (shuffle + (for [temp (range 80 85)] + (->Temperature temp "MCI"))) + session (-> empty-session + (insert-all shuffled-facts) + fire-rules) + + retracted (-> session + (retract (->Temperature 81 "MCI") + (->Temperature 82 "MCI")) + fire-rules) + + all-retracted (-> (apply retract session shuffled-facts) + (fire-rules))] + + ;; Ensure expected items are there. Ordering is guaranteed. + (is (= [{:?t (sort-by :temperature shuffled-facts)}] + (query session sorted-all))) + + (is (= [{:?t (sort-by :temperature > shuffled-facts)}] + (query session sorted-all-desc))) + + (is (= [{:?t (->> (sort-by :temperature shuffled-facts) + (map :temperature))}] + (query session sorted-all-conv))) + + (is (= [{:?t (->> [(->Temperature 81 "MCI") + (->Temperature 82 "MCI")] + (apply disj (set shuffled-facts)) + (sort-by :temperature))}] + (query retracted sorted-all))) + + (is (= [{:?t (->> [(->Temperature 81 "MCI") + (->Temperature 82 "MCI")] + (apply disj (set shuffled-facts)) + (sort-by :temperature >))}] + (query retracted sorted-all-desc))) + + (is (= [{:?t (->> [(->Temperature 81 "MCI") + (->Temperature 82 "MCI")] + (apply disj (set shuffled-facts)) + (sort-by :temperature) + (map :temperature))}] + (query retracted sorted-all-conv))) + + (is (= [{:?t []}] + (query all-retracted sorted-all) + (query all-retracted sorted-all-desc) + (query all-retracted sorted-all-conv)) + "Retracting all values should cause a return to the initial value of + an empty sequence."))) + +(def-rules-test test-accum-sorted-grouping-by + {:queries [sorted-grouping [[] [[?t <- (acc/sorted-grouping-by :location :temperature) + :from [Temperature]]]] + + sorted-grouping-desc [[] [[?t <- (acc/sorted-grouping-by :location :temperature + :group-comparator #(compare %2 %1) + :sort-comparator >) + :from [Temperature]]]] + sorted-grouping-conv [[] [[?t <- (acc/sorted-grouping-by :location :temperature + :convert-return-fn vec) + :from [Temperature]]]]] + + :sessions [empty-session [sorted-grouping sorted-grouping-desc sorted-grouping-conv] {}]} + + (let [shuffled-facts (shuffle + (concat + (for [temp (range 80 85)] + (->Temperature temp "MCI")) + (for [temp (range 85 90)] + (->Temperature temp "ORD")) + (for [temp (range 90 95)] + (->Temperature temp "TPA")))) + session (-> empty-session + (insert-all shuffled-facts) + fire-rules) + + retracted-session (-> session + (retract (->Temperature 80 "MCI") + (->Temperature 85 "ORD") + (->Temperature 90 "TPA")) + fire-rules) + retracted-all-session (-> (apply retract session shuffled-facts) + fire-rules)] + + (testing "sorted grouping-accum" + (testing "all facts" + (is (= [{:?t [["MCI" (->> (for [fact shuffled-facts + :when (= (:location fact) "MCI")] + fact) + (sort-by :temperature))] + ["ORD" (->> (for [fact shuffled-facts + :when (= (:location fact) "ORD")] + fact) + (sort-by :temperature))] + ["TPA" (->> (for [fact shuffled-facts + :when (= (:location fact) "TPA")] + fact) + (sort-by :temperature))]]}] + (for [result (query session sorted-grouping)] + (update result :?t vec))))) + (testing "retracted some facts" + (is (= [{:?t [["MCI" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "MCI") + (> (:temperature fact) 80))] + fact) + (sort-by :temperature))] + ["ORD" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "ORD") + (> (:temperature fact) 85))] + fact) + (sort-by :temperature))] + ["TPA" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "TPA") + (> (:temperature fact) 90))] + fact) + (sort-by :temperature))]]}] + (for [result (query retracted-session sorted-grouping)] + (update result :?t vec))))) + (testing "retracted all facts" + (is (= [{:?t {}}] + (query retracted-all-session sorted-grouping))))) + + (testing "sorted grouping-accum desc" + (testing "all facts" + (is (= [{:?t [["TPA" (->> (for [fact shuffled-facts + :when (= (:location fact) "TPA")] + fact) + (sort-by :temperature >))] + ["ORD" (->> (for [fact shuffled-facts + :when (= (:location fact) "ORD")] + fact) + (sort-by :temperature >))] + ["MCI" (->> (for [fact shuffled-facts + :when (= (:location fact) "MCI")] + fact) + (sort-by :temperature >))]]}] + (for [result (query session sorted-grouping-desc)] + (update result :?t vec))))) + (testing "retracted some facts" + (is (= [{:?t [["TPA" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "TPA") + (> (:temperature fact) 90))] + fact) + (sort-by :temperature >))] + ["ORD" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "ORD") + (> (:temperature fact) 85))] + fact) + (sort-by :temperature >))] + ["MCI" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "MCI") + (> (:temperature fact) 80))] + fact) + (sort-by :temperature >))]]}] + (for [result (query retracted-session sorted-grouping-desc)] + (update result :?t vec))))) + (testing "retracted all facts" + (is (= [{:?t {}}] + (query retracted-all-session sorted-grouping-desc))))) + + (testing "sorted grouping-accum with custom return-convert-fn (vec)" + (testing "all facts" + (is (= [{:?t [["MCI" (->> (for [fact shuffled-facts + :when (= (:location fact) "MCI")] + fact) + (sort-by :temperature))] + ["ORD" (->> (for [fact shuffled-facts + :when (= (:location fact) "ORD")] + fact) + (sort-by :temperature))] + ["TPA" (->> (for [fact shuffled-facts + :when (= (:location fact) "TPA")] + fact) + (sort-by :temperature))]]}] + (query session sorted-grouping-conv)))) + (testing "retracted some facts" + (is (= [{:?t [["MCI" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "MCI") + (> (:temperature fact) 80))] + fact) + (sort-by :temperature))] + ["ORD" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "ORD") + (> (:temperature fact) 85))] + fact) + (sort-by :temperature))] + ["TPA" (->> (for [fact shuffled-facts + :when (and (= (:location fact) "TPA") + (> (:temperature fact) 90))] + fact) + (sort-by :temperature))]]}] + (query retracted-session sorted-grouping-conv)))) + (testing "retracted all facts" + (is (= [{:?t []}] + (query retracted-all-session sorted-grouping-conv)))))))