(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 (range/abuts? [_this other] (or (= 1 (abs (- start (range/end other)))) (= 1 (abs (- end (range/start other)))))) (value-before [__this] (if (= start Integer/MIN_VALUE) Integer/MIN_VALUE (dec start))) (value-after [_this] (if (= end Integer/MAX_VALUE) Integer/MAX_VALUE (inc end))) (start [_this] start) (end [_this] end) (range-type [_this] :int-range-inclusive) (union [this other] (when-not (or (range/abuts? this other) (or (<= start (range/start other) end) (<= (range/start other) start (range/end other)))) (throw (ex-info "Cannot union non-abutting ranges" {}))) (range/->discrete-value-range (range/range-type this) (min start (range/start other)) (max end (range/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 (= #{} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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 (= #{} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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 (= #{} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)} (into #{} (#'range/walk-tagged-range-boundaries-xf) [{: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)})))))))