consolidate overlapping ranges

transduce over the ordered range values starts and ends and do the
stack push/pop matching over the start and end values to consolidate
the overlapping ranges and result in a new set of consolidated and
sorted range values.
This commit is contained in:
2026-01-10 17:49:34 -06:00
parent 725906c2e8
commit 98761a79ea
2 changed files with 76 additions and 2 deletions

View File

@@ -52,3 +52,48 @@
:boundary-type :end
:type (range-type range)}]))
(sort-by (juxt :value (comp {:start 0 :end 1} :boundary-type)))))
(defmulti ->discrete-value-range (fn [range-type _start _end]
range-type))
(defn combine-overlapping-ranges
"transducer to find and combine overlapping ranges by looking at
ordered range value markers.
When it encounters a range's start boundary it pushes an entry on
the stack, and captures the value for that start boundary if the
stack was previously empty.
When it encounters a range's end boundary value, it pops an item
from the stack, and if the stack is now empty, it uses the value
of the range boundary end and the captured start value to create
a new range object, which will contain any other range start and
end pairs encountered between the new start and end values."
[]
(fn [xf]
(let [stack (volatile! [])
start (volatile! nil)]
(fn
([] (xf))
([result] (xf result))
([result input]
(if (reduced? input)
result
(case (:boundary-type input)
:start (do
(when-not (seq @stack)
(vreset! start (:value input)))
(vswap! stack (fnil conj []) input)
result)
:end (do
(vswap! stack pop)
(if (seq @stack)
result
(xf result (->discrete-value-range (:type input) @start (:value input))))))))))))
(def consolidate-ranges-xf
(comp
(combine-overlapping-ranges)))
(defn consolidate [ranges]
(eduction consolidate-ranges-xf (ordered-range-values ranges)))

View File

@@ -31,6 +31,9 @@
(assert (<= start end))
(->IntInclusiveDiscreteValueRange start end))
(defmethod range/->discrete-value-range :int-range-inclusive [_ start end]
(int-range-inclusive start end))
(deftest integer-ranges-sanity-test
(testing "value equality"
(is (= (int-range-inclusive 0 1)
@@ -54,15 +57,19 @@
(testing "end"
(is (= 8 (range/end (int-range-inclusive 1 8))))))
;; test against integer ranges for easy of expression and interpretation
(deftest ordered-range-values
(testing "ordered range values are sorted by value and then the range's start boundary before any end boundary"
(is (= [{:value 4 :boundary-type :start :type :int-range-inclusive}
(is (= [{:value 1 :boundary-type :start :type :int-range-inclusive}
{:value 2 :boundary-type :end :type :int-range-inclusive}
{:value 4 :boundary-type :start :type :int-range-inclusive}
{:value 5 :boundary-type :start :type :int-range-inclusive}
{:value 5 :boundary-type :start :type :int-range-inclusive}
{:value 5 :boundary-type :end :type :int-range-inclusive}
{:value 5 :boundary-type :end :type :int-range-inclusive}
{:value 8 :boundary-type :end :type :int-range-inclusive}]
(#'range/ordered-range-values [(int-range-inclusive 4 5)
(#'range/ordered-range-values [(int-range-inclusive 1 2)
(int-range-inclusive 4 5)
(int-range-inclusive 5 8)
(int-range-inclusive 5 5)])))
(is (= [{:value 5 :boundary-type :start :type :int-range-inclusive}
@@ -71,3 +78,25 @@
{:value 5 :boundary-type :end :type :int-range-inclusive}]
(#'range/ordered-range-values [(int-range-inclusive 5 5)
(int-range-inclusive 5 5)])))))
;; test against integer ranges for easy of expression and interpretation
(deftest consolidate-ranges
(testing "combines overlapping ranges"
(is (= [(int-range-inclusive 5 5)]
(range/consolidate [(int-range-inclusive 5 5)
(int-range-inclusive 5 5)])))
(is (= [(int-range-inclusive 0 1)
(int-range-inclusive 2 7)
(int-range-inclusive 9 11)]
(range/consolidate [(int-range-inclusive 0 1)
(int-range-inclusive 2 4)
(int-range-inclusive 3 7)
(int-range-inclusive 5 5)
(int-range-inclusive 9 11)
(int-range-inclusive 5 5)])))
(is (= [(int-range-inclusive 2 11)]
(range/consolidate [(int-range-inclusive 2 4)
(int-range-inclusive 3 7)
(int-range-inclusive 5 5)
(int-range-inclusive 6 11)
(int-range-inclusive 5 5)])))))