diff --git a/src/challenge/core.clj b/src/challenge/core.clj index e1f4927..b55f937 100644 --- a/src/challenge/core.clj +++ b/src/challenge/core.clj @@ -1,6 +1,36 @@ (ns challenge.core - (:require [clojure.set :as set]) - (:import (java.time LocalDate Period) + (:require [challenge.discrete-value-range :as range]) + (:import (java.time LocalDate) (org.threeten.extra LocalDateRange))) -(defn difference [& input]) +(extend-type LocalDateRange + range/DiscreteValueRange + (abuts [this other] + (.abuts this other)) + (value-before [this] + (if (.isUnboundedStart this) + (.getStart this) + (.. this getStart (minusDays 1)))) + (value-after [this] + (if (.isUnboundedEnd this) + (.getEnd this) + (.. this getEndInclusive (plusDays 1)))) + (start [this] + (.getStart this)) + (end [this] + (.getEndInclusive this)) + (range-type [_this] + :local-date-range) + (union [this other] + (when-not (.isConnected this other) + (throw (ex-info "Cannot union non-connecting ranges" {}))) + (.union this other)) + (before [^LocalDateRange this ^LocalDate x] + (.isBefore this x)) + (after [this ^LocalDate x] + (.isAfter (.getEndInclusive this) x))) + +(defmethod range/->discrete-value-range :local-date-range [_ start end] + (LocalDateRange/ofClosed start end)) + +(def difference range/difference) diff --git a/src/challenge/discrete_value_range.clj b/src/challenge/discrete_value_range.clj index 31ea654..fb9ff08 100644 --- a/src/challenge/discrete_value_range.clj +++ b/src/challenge/discrete_value_range.clj @@ -1,5 +1,7 @@ (ns challenge.discrete-value-range - (:require [clojure.core.match :refer [match]])) + (:require + [clojure.core.match :refer [match]] + [clojure.set :as set])) (defprotocol DiscreteValueRange "A protocol for generic behavior on Ranges over @@ -180,3 +182,40 @@ range-type new-working-range-start-value)) (recur boundary-items false in-source-range ranges close-fn)))))) + +(def ^:private range-boundary-source-compare + "Sorts range boundary items + + Sorts by (in precidence order) + 1. :value; lower values first + 2. :boundary-type; :start before :end + 3. :range-source-type + - filter-ranges should 'wrap' source ranges + - for starts, the filter-ranges should be before source-ranges + - and for ends, the source-ranges should be before the filter ranges" + (juxt :value + (comp {:start 0 :end 1} :boundary-type) + (comp {[:filter-range :start] 0 + [:source-range :start] 1 + [:source-range :end] 2 + [:filter-range :end] 3} + (juxt :range-source-type :boundary-type)))) + +(defn difference + ([range-set] + (consolidate range-set)) + ([range-set range-sets-to-remove] + (let [range-set-boundaries (->> range-set + consolidate + ordered-range-values + (map #(assoc % :range-source-type :source-range))) + range-sets-to-remove-boundaries (->> range-sets-to-remove + consolidate + ordered-range-values + (map #(assoc % :range-source-type :filter-range))) + all-range-set-boundaries (->> (into range-set-boundaries + range-sets-to-remove-boundaries) + (sort-by range-boundary-source-compare))] + (walk-range-boundaries all-range-set-boundaries))) + ([range-set range-set-to-remove & additional-range-sets-to-remove] + (difference range-set (apply set/union (conj additional-range-sets-to-remove range-set-to-remove))))) diff --git a/test/challenge/discrete_value_range_test.clj b/test/challenge/discrete_value_range_test.clj index cbd3b31..ca7c86d 100644 --- a/test/challenge/discrete_value_range_test.clj +++ b/test/challenge/discrete_value_range_test.clj @@ -86,19 +86,19 @@ ;; test against integer ranges for easy of expression and interpretation (deftest consolidate-ranges (testing "combines overlapping ranges" - (is (= [(int-range-inclusive 5 5)] + (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)] + (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)] + (is (= #{(int-range-inclusive 2 11)} (range/consolidate [(int-range-inclusive 2 4) (int-range-inclusive 3 7) (int-range-inclusive 5 5) @@ -106,13 +106,13 @@ (int-range-inclusive 5 5)])))) (testing "conjoins abutting ranges" - (is (= [(int-range-inclusive 0 9)] + (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)] + (is (= #{(int-range-inclusive 0 9)} (range/consolidate [(int-range-inclusive 0 1) (int-range-inclusive 2 3) (int-range-inclusive 4 5) @@ -120,8 +120,8 @@ (int-range-inclusive 5 5)])))) (testing "combines overlapping ranges and conjoins abutting ranges" - (is (= [(int-range-inclusive 0 7) - (int-range-inclusive 13 17)] + (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) @@ -176,6 +176,28 @@ :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 @@ -243,6 +265,28 @@ :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) @@ -343,3 +387,90 @@ :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)})))))))