From 1b565ed7791eb5c5a4e3b779795a29afa3fff7f6 Mon Sep 17 00:00:00 2001 From: Jose Gomez Date: Tue, 15 Apr 2025 10:51:57 -0500 Subject: [PATCH 1/5] feat: add sorting accumulators --- src/main/clojure/clara/rules/accumulators.clj | 37 ++++ src/test/clojure/clara/test_accumulators.clj | 192 ++++++++++++++++++ 2 files changed, 229 insertions(+) diff --git a/src/main/clojure/clara/rules/accumulators.clj b/src/main/clojure/clara/rules/accumulators.clj index f71b1663..5971f6c3 100644 --- a/src/main/clojure/clara/rules/accumulators.clj +++ b/src/main/clojure/clara/rules/accumulators.clj @@ -211,3 +211,40 @@ {: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 grouping accumulator. Behaves like clojure.core/sort-by. + * `sort-field` - required - The field of a fact to sort by. + * `comparator` - optional - The comparator for sort by, defaults to `clojure.core/compare`." + ([sort-field] + (sorting-by sort-field compare)) + ([sort-field comparator] + (assoc (all) :convert-return-fn + (fn do-sort + [return-items] + (sort-by sort-field comparator return-items))))) + +(defn sorted-grouping-by + "Return a generic grouping accumulator. Behaves like clojure.core/group-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)]} + (reduce-to-accum + (fn [m v] + (let [k (group-field v)] + (update m k grouping-fn v))) + {} + (comp convert-return-fn (fn [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..7c621f20 100644 --- a/src/test/clojure/clara/test_accumulators.clj +++ b/src/test/clojure/clara/test_accumulators.clj @@ -536,3 +536,195 @@ (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 >) :from [Temperature]]]]] + + :sessions [empty-session [sorted-all sorted-all-desc] {}]} + + (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 (->> [(->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 []}] + (query all-retracted sorted-all) + (query all-retracted sorted-all-desc)) + "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))))))) From 7a1cdf792f5df965eda4c6c85204d762cc534c42 Mon Sep 17 00:00:00 2001 From: Jose Gomez Date: Tue, 15 Apr 2025 10:53:36 -0500 Subject: [PATCH 2/5] feat: update changelog with note about new sorting accumulators --- CHANGELOG.md | 3 +++ pom.xml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 From 28eff2ad2f4936394e6b7034a2cbddea93f99d8f Mon Sep 17 00:00:00 2001 From: Jose Gomez Date: Tue, 15 Apr 2025 11:00:56 -0500 Subject: [PATCH 3/5] chore: fix comments --- src/main/clojure/clara/rules/accumulators.clj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/clojure/clara/rules/accumulators.clj b/src/main/clojure/clara/rules/accumulators.clj index 5971f6c3..d394df2e 100644 --- a/src/main/clojure/clara/rules/accumulators.clj +++ b/src/main/clojure/clara/rules/accumulators.clj @@ -213,7 +213,7 @@ :retract-fn (fn [items retracted] (drop-one-of items (field retracted)))}))) (defn sorting-by - "Return a generic grouping accumulator. Behaves like clojure.core/sort-by. + "Return a generic sorting accumulator. Behaves like clojure.core/sort-by. * `sort-field` - required - The field of a fact to sort by. * `comparator` - optional - The comparator for sort by, defaults to `clojure.core/compare`." ([sort-field] @@ -225,7 +225,8 @@ (sort-by sort-field comparator return-items))))) (defn sorted-grouping-by - "Return a generic grouping accumulator. Behaves like clojure.core/group-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`. From b265af6ca50367b6fa3a37f0e941bb7ad1453ccb Mon Sep 17 00:00:00 2001 From: Jose Gomez Date: Tue, 15 Apr 2025 11:28:40 -0500 Subject: [PATCH 4/5] feat: update sorting-by accumulator to allow a convert-return-fn --- src/main/clojure/clara/rules/accumulators.clj | 25 +++++++++++-------- src/test/clojure/clara/test_accumulators.clj | 19 +++++++++++--- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/main/clojure/clara/rules/accumulators.clj b/src/main/clojure/clara/rules/accumulators.clj index d394df2e..bc884db2 100644 --- a/src/main/clojure/clara/rules/accumulators.clj +++ b/src/main/clojure/clara/rules/accumulators.clj @@ -214,15 +214,19 @@ (defn sorting-by "Return a generic sorting accumulator. Behaves like clojure.core/sort-by. - * `sort-field` - required - The field of a fact to sort by. - * `comparator` - optional - The comparator for sort by, defaults to `clojure.core/compare`." - ([sort-field] - (sorting-by sort-field compare)) - ([sort-field comparator] - (assoc (all) :convert-return-fn - (fn do-sort - [return-items] - (sort-by sort-field comparator return-items))))) + * `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) + (ifn? comparator)]} + (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 @@ -231,8 +235,7 @@ * `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." + * `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] diff --git a/src/test/clojure/clara/test_accumulators.clj b/src/test/clojure/clara/test_accumulators.clj index 7c621f20..6ac06051 100644 --- a/src/test/clojure/clara/test_accumulators.clj +++ b/src/test/clojure/clara/test_accumulators.clj @@ -539,9 +539,10 @@ (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 >) :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] {}]} + :sessions [empty-session [sorted-all sorted-all-desc sorted-all-conv] {}]} (let [shuffled-facts (shuffle (for [temp (range 80 85)] @@ -565,6 +566,10 @@ (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)) @@ -577,9 +582,17 @@ (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-desc) + (query all-retracted sorted-all-conv)) "Retracting all values should cause a return to the initial value of an empty sequence."))) From a97a98b0b18c6bd0278ca92b69548444a3b17c3c Mon Sep 17 00:00:00 2001 From: Jose Gomez Date: Tue, 15 Apr 2025 11:39:15 -0500 Subject: [PATCH 5/5] chore: re-use grouping by accumulator in sorted-grouping-by accumulator --- src/main/clojure/clara/rules/accumulators.clj | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/clojure/clara/rules/accumulators.clj b/src/main/clojure/clara/rules/accumulators.clj index bc884db2..d1e3fc3e 100644 --- a/src/main/clojure/clara/rules/accumulators.clj +++ b/src/main/clojure/clara/rules/accumulators.clj @@ -221,8 +221,7 @@ convert-return-fn] :or {comparator compare convert-return-fn identity}}] - {:pre [(ifn? convert-return-fn) - (ifn? comparator)]} + {:pre [(ifn? convert-return-fn)]} (assoc (all) :convert-return-fn (comp convert-return-fn (fn do-sort [return-items] @@ -243,12 +242,8 @@ sort-comparator compare convert-return-fn identity}}] {:pre [(ifn? convert-return-fn)]} - (reduce-to-accum - (fn [m v] - (let [k (group-field v)] - (update m k grouping-fn v))) - {} - (comp convert-return-fn (fn [m] - (into (sorted-map-by group-comparator) - (for [[k vs] m] - [k (sort-by sort-field sort-comparator vs)])))))) + (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)])))))