Files
range-difference/test/challenge/discrete_value_range_test.clj
Steven Proctor 9ffa5d1b97 performance increases
memoize construction of ranges, and use transient/mutable
Java collections for internal collection state
2026-01-13 21:01:46 -06:00

479 lines
26 KiB
Clojure

(ns challenge.discrete-value-range-test
(:require
[clojure.test :refer [deftest testing is]]
[challenge.discrete-value-range :as range]))
(defrecord IntInclusiveDiscreteValueRange
[^int start ^int end]
range/DiscreteValueRange
(abuts [_this other]
(or (= 1 (abs (- start (.end other))))
(= 1 (abs (- end (.start other))))))
(value-before [__this]
(dec start))
(value-after [_this]
(inc end))
(start [_this]
start)
(end [_this]
end)
(range-type [_this]
:int-range-inclusive)
(union [this other]
(when-not (range/abuts this other)
(throw (ex-info "Cannot union non-abutting ranges" {})))
(range/->discrete-value-range (.range-type this)
(min start (.start other))
(max end (.end other))))
Object
(toString [_this]
(str start ".." end)))
(defn- int-range-inclusive* [start end]
(assert (<= start end) (str "start : " start "; end: " end))
(->IntInclusiveDiscreteValueRange start end))
(def int-range-inclusive (memoize int-range-inclusive*))
(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)
(int-range-inclusive 0 1))))
(testing "abuts"
(is (= true (range/abuts (int-range-inclusive 0 1)
(int-range-inclusive 2 3))))
(is (= true (range/abuts (int-range-inclusive 1 1)
(int-range-inclusive 2 3))))
(is (= true (range/abuts (int-range-inclusive 4 7)
(int-range-inclusive 2 3))))
(is (= false (range/abuts (int-range-inclusive 4 7)
(int-range-inclusive 1 2)))))
(testing "value-before"
(is (= 0 (range/value-before (int-range-inclusive 1 8)))))
(testing "value-after"
(is (= 9 (range/value-after (int-range-inclusive 1 8)))))
(testing "start"
(is (= 1 (range/start (int-range-inclusive 1 8)))))
(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 1 :prev-value 0 :boundary-type :start :type :int-range-inclusive}
{:value 2 :next-value 3 :boundary-type :end :type :int-range-inclusive}
{:value 4 :prev-value 3 :boundary-type :start :type :int-range-inclusive}
{:value 5 :prev-value 4 :boundary-type :start :type :int-range-inclusive}
{:value 5 :prev-value 4 :boundary-type :start :type :int-range-inclusive}
{:value 5 :next-value 6 :boundary-type :end :type :int-range-inclusive}
{:value 5 :next-value 6 :boundary-type :end :type :int-range-inclusive}
{:value 8 :next-value 9 :boundary-type :end :type :int-range-inclusive}]
(#'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 :prev-value 4 :boundary-type :start :type :int-range-inclusive}
{:value 5 :prev-value 4 :boundary-type :start :type :int-range-inclusive}
{:value 5 :next-value 6 :boundary-type :end :type :int-range-inclusive}
{:value 5 :next-value 6 :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 3 7)
(int-range-inclusive 9 11)}
(range/consolidate [(int-range-inclusive 0 1)
(int-range-inclusive 3 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)]))))
(testing "conjoins abutting ranges"
(is (= #{(int-range-inclusive 0 9)}
(range/consolidate [(int-range-inclusive 0 1)
(int-range-inclusive 2 4)
(int-range-inclusive 5 5)
(int-range-inclusive 6 9)
(int-range-inclusive 5 5)])))
(is (= #{(int-range-inclusive 0 9)}
(range/consolidate [(int-range-inclusive 0 1)
(int-range-inclusive 2 3)
(int-range-inclusive 4 5)
(int-range-inclusive 6 9)
(int-range-inclusive 5 5)]))))
(testing "combines overlapping ranges and conjoins abutting ranges"
(is (= #{(int-range-inclusive 0 7)
(int-range-inclusive 13 17)}
(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 13 17)
(int-range-inclusive 5 5)])))))
(deftest walk-range-boundaries-test
(testing "only filter-ranges"
(is (= #{}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :filter-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 4
:next-value 5
:type :int-range-inclusive}]))))
(testing "only source-ranges"
(is (= #{(int-range-inclusive 1 5)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :source-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 5
:next-value 6
:type :int-range-inclusive}])))
(is (= #{(int-range-inclusive 1 5)
(int-range-inclusive 11 15)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :source-range
:value 1
:next-value 2
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 5
:prev-value 4
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :source-range
:value 11
:next-value 12
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 15
:prev-value 14
:type :int-range-inclusive}]))))
(testing "filter-ranges and source-ranges are the same"
(is (= #{}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :filter-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :source-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 5
:next-value 6
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 5
:next-value 6
:type :int-range-inclusive}]))))
(testing "filter-ranges before source-ranges"
(is (= #{(int-range-inclusive 11 15)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :filter-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 5
:next-value 6
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :source-range
:value 11
:prev-value 10
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 15
:next-value 16
:type :int-range-inclusive}]))))
(testing "filter-ranges after source-ranges"
(is (= #{(int-range-inclusive 1 5)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :source-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 5
:next-value 6
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :filter-range
:prev-value 10
:value 11
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 15
:next-value 16
:type :int-range-inclusive}]))))
(testing "single filter-range between source-range"
(is (= #{(int-range-inclusive 1 5)
(int-range-inclusive 11 20)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :source-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :filter-range
:prev-value 5
:value 6
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 10
:next-value 11
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 20
:next-value 21
:type :int-range-inclusive}]))))
(testing "single filter-range between source-range but ends align"
(is (= #{(int-range-inclusive 1 5)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :source-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :filter-range
:prev-value 5
:value 6
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 10
:next-value 11
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 10
:next-value 11
:type :int-range-inclusive}]))))
(testing "multiple filter-ranges between source-range"
(is (= #{(int-range-inclusive 1 5)
(int-range-inclusive 11 12)
(int-range-inclusive 20 20)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :source-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :filter-range
:prev-value 5
:value 6
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 10
:next-value 11
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :filter-range
:prev-value 12
:value 13
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 19
:next-value 20
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 20
:prev-value 19
:type :int-range-inclusive}]))))
(testing "source range between filter-range"
(is (= #{}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :filter-range
:prev-value 1
:value 1
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :source-range
:value 6
:prev-value 5
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 10
:next-value 11
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :source-range
:value 13
:prev-value 12
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 19
:next-value 20
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 20
:next-value 21
:type :int-range-inclusive}]))))
(testing "filter range overlaps source ranges"
(is (= #{(int-range-inclusive 1 4)
(int-range-inclusive 11 13)}
(range/walk-range-boundaries [{:boundary-type :start
:range-source-type :source-range
:value 1
:prev-value 0
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :filter-range
:prev-value 4
:value 5
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 6
:next-value 7
:type :int-range-inclusive}
{:boundary-type :start
:range-source-type :source-range
:value 8
:prev-value 7
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :filter-range
:value 10
:next-value 11
:type :int-range-inclusive}
{:boundary-type :end
:range-source-type :source-range
:value 13
:next-value 14
:type :int-range-inclusive}])))))
(deftest difference-test
(testing "unary"
(is (= #{(int-range-inclusive 1 5)}
(range/difference #{(int-range-inclusive 1 5)})))
(is (= #{(int-range-inclusive 1 5)
(int-range-inclusive 10 15)}
(range/difference #{(int-range-inclusive 1 5)
(int-range-inclusive 10 15)})))
(is (= #{(int-range-inclusive 1 15)}
(range/difference #{(int-range-inclusive 1 5)
(int-range-inclusive 5 11)
(int-range-inclusive 12 15)}))))
(testing "binary"
(testing "empty starting set"
(is (= #{}
(range/difference #{}
#{(int-range-inclusive 6 10)}))))
(testing "single item in starting set"
(is (= #{(int-range-inclusive 1 10)}
(range/difference #{(int-range-inclusive 1 10)}
#{}))))
(testing "ranges to remove are before all ranges in starting set"
(is (= #{(int-range-inclusive 100 150)}
(range/difference #{(int-range-inclusive 100 150)}
#{(int-range-inclusive 1 10)
(int-range-inclusive 60 70)}))))
(testing "ranges to remove are after all ranges in starting set"
(is (= #{(int-range-inclusive 1 5)}
(range/difference #{(int-range-inclusive 1 5)}
#{(int-range-inclusive 10 15)
(int-range-inclusive 60 70)}))))
(testing "ranges to remove partially overlap at beginning of starting set"
(is (= #{(int-range-inclusive 6 10)}
(range/difference #{(int-range-inclusive 1 10)}
#{(int-range-inclusive 1 5)}))))
(testing "ranges to remove partially overlap at end of starting set"
(is (= #{(int-range-inclusive 1 5)}
(range/difference #{(int-range-inclusive 1 10)}
#{(int-range-inclusive 6 10)}))))
(testing "ranges to remove partially overlap at multiple starting sets"
(is (= #{(int-range-inclusive 1 5)
(int-range-inclusive 34 40)}
(range/difference #{(int-range-inclusive 1 10)
(int-range-inclusive 15 20)
(int-range-inclusive 30 40)}
#{(int-range-inclusive 6 33)}))))
(testing "disjoint ranges in starting set"
(testing "empty set of ranges to remove"
(is (= #{(int-range-inclusive 1 10)
(int-range-inclusive 20 30)}
(range/difference #{(int-range-inclusive 1 10)
(int-range-inclusive 20 30)}
#{}))))))
(testing "variadic"
(testing "empty starting set"
(is (= #{}
(range/difference #{}
#{(int-range-inclusive 6 10)}
#{(int-range-inclusive 16 100)}))))
(testing "ranges to remove partially overlap at beginning of starting set"
(is (= #{(int-range-inclusive 6 10)}
(range/difference #{(int-range-inclusive 1 10)}
#{(int-range-inclusive 1 5)}
#{(int-range-inclusive -3 4)}))))
(testing "ranges to remove have some empty sets"
(is (= #{(int-range-inclusive 1 10)}
(range/difference #{(int-range-inclusive 1 10)}
#{}
#{})))
(is (= #{(int-range-inclusive 1 5)}
(range/difference #{(int-range-inclusive 1 10)}
#{(int-range-inclusive 6 10)}
#{}))))
(testing "ranges to remove partially overlap at end of starting set"
(is (= #{(int-range-inclusive 1 5)}
(range/difference #{(int-range-inclusive 1 10)}
#{(int-range-inclusive 6 10)}
#{(int-range-inclusive 14 19)}))))
(testing "disjoint ranges in starting set"
(testing "empty set of ranges to remove"
(is (= #{(int-range-inclusive 1 10)
(int-range-inclusive 20 30)}
(range/difference #{(int-range-inclusive 1 10)
(int-range-inclusive 20 30)}
#{}
#{(int-range-inclusive 11 14)
(int-range-inclusive 16 17)})))))))