Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<groupId>com.github.k13labs</groupId>
<artifactId>clara-rules</artifactId>
<name>clara-rules</name>
<version>1.5.1</version>
<version>1.5.2</version>
<scm>
<tag>1.5.1</tag>
<tag>1.5.2</tag>
<url>https://github.com/k13labs/clara-rules</url>
<connection>scm:git:git://github.com/k13labs/clara-rules.git</connection>
<developerConnection>scm:git:ssh://git@github.com/k13labs/clara-rules.git</developerConnection>
Expand Down
36 changes: 36 additions & 0 deletions src/main/clojure/clara/rules/accumulators.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)])))))
205 changes: 205 additions & 0 deletions src/test/clojure/clara/test_accumulators.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)))))))