diff --git a/deps.edn b/deps.edn index efa6d45..aa4bcf9 100644 --- a/deps.edn +++ b/deps.edn @@ -1,5 +1,4 @@ {:deps {org.clojure/clojure {:mvn/version "1.11.2"} - org.clojure/core.match {:mvn/version "1.1.1"} org.threeten/threeten-extra {:mvn/version "1.8.0"}} :paths ["src"] :aliases {:test {:extra-paths ["test"] diff --git a/src/challenge/core.clj b/src/challenge/core.clj index b0df51f..cf8d174 100644 --- a/src/challenge/core.clj +++ b/src/challenge/core.clj @@ -5,23 +5,23 @@ (extend-type LocalDateRange range/DiscreteValueRange - (abuts [this other] + (abuts? [^LocalDateRange this ^LocalDateRange other] (.abuts this other)) - (value-before [this] + (value-before ^LocalDate [^LocalDateRange this] (if (.isUnboundedStart this) (.getStart this) (.. this getStart (minusDays 1)))) - (value-after [this] + (value-after [^LocalDateRange this] (if (.isUnboundedEnd this) (.getEnd this) (.. this getEndInclusive (plusDays 1)))) - (start [this] + (start ^LocalDate [^LocalDateRange this] (.getStart this)) - (end [this] + (end ^LocalDate [^LocalDateRange this] (.getEndInclusive this)) (range-type [_this] :local-date-range) - (union [this other] + (union [this ^LocalDateRange other] (when-not (.isConnected this other) (throw (ex-info "Cannot union non-connecting ranges" {}))) (.union this other))) diff --git a/src/challenge/discrete_value_range.clj b/src/challenge/discrete_value_range.clj index 726e573..67f0fca 100644 --- a/src/challenge/discrete_value_range.clj +++ b/src/challenge/discrete_value_range.clj @@ -1,7 +1,5 @@ (ns challenge.discrete-value-range - (:require - [clojure.core.match :refer [match]] - [clojure.set :as set])) + (:require [clojure.set :as set])) (defprotocol DiscreteValueRange "A protocol for generic behavior on Ranges over @@ -26,15 +24,23 @@ that before/after values against other values that the range is over. " - (abuts [this other]) - (value-before [this]) - (value-after [this]) - (start [this]) - (end [this]) - (range-type [this]) - (union [this other])) + (abuts? [this other] "Does this range abuts the specified range") + (value-before [this] "The discrete value that is considered to be directly before the start value") + (value-after [this] "The discrete value that is considered to be directly after the end value") + (start [this] "The starting value (inclusive) for this range") + (end [this] "The ending value (inclusive) for this range") + (range-type [this] + "Type of the range. Used in dispatch of multimethod to construct new ranges of the type.") + (union [this other] + "Calculates the range that is the union of this range and the other range. + The two ranges should overlap or abut each other.")) -(defn ->range-boundaries* [range] +(defmulti ->discrete-value-range (fn [range-type _start _end] + range-type)) + +(defn ->range-boundaries* + "Construct range boundary 'fencepost' markers for the range" + [range] [{:value (start range) :boundary-type :start :type (range-type range) @@ -46,6 +52,9 @@ (def ->range-boundaries (memoize ->range-boundaries*)) +(def untagged-range-boundary-compare + (juxt :value (comp {:start 0 :end 1} :boundary-type))) + (defn- ordered-range-values "Builds an ordered list or 'fenceposts' for the start and end of all given ranges, to prepare to produces a consolidated @@ -61,12 +70,9 @@ [ranges] (->> ranges (mapcat ->range-boundaries) - (sort-by (juxt :value (comp {:start 0 :end 1} :boundary-type))))) + (sort-by untagged-range-boundary-compare))) -(defmulti ->discrete-value-range (fn [range-type _start _end] - range-type)) - -(defn- combine-overlapping-ranges +(defn- combine-overlapping-ranges-xf "transducer to find and combine overlapping ranges by looking at ordered range value markers. @@ -97,11 +103,11 @@ result) :end (do (.pop stack) - (if (not (.empty stack)) - result - (xf result (->discrete-value-range type @start value))))))))))) + (if (.empty stack) + (xf result (->discrete-value-range type @start value)) + result))))))))) -(defn- combine-abutting-ranges +(defn- combine-abutting-ranges-xf "transducer to join ranges where the start and end of two ranges are consective discrete values. @@ -109,82 +115,97 @@ combined." [] (fn [xf] - (let [prev (volatile! nil)] + (let [previous-range (volatile! nil)] (fn ([] (xf)) - ([result] (if @prev - (xf (xf result @prev)) + ([result] (if-let [prev @previous-range] + (xf (xf result prev)) (xf result))) ([result input] - (cond - (reduced? input) result + (let [prev @previous-range] + (cond + (reduced? input) result - (nil? @prev) (do - (vreset! prev input) - result) + (nil? prev) (do + (vreset! previous-range input) + result) - (abuts @prev input) - (let [item (union @prev input)] - (vreset! prev item) - result) + (abuts? prev input) + (let [item (union prev input)] + (vreset! previous-range item) + result) - :else (let [item @prev] - (vreset! prev input) - (xf result item)))))))) + :else (let [item prev] + (vreset! previous-range input) + (xf result item))))))))) (def consolidate-ranges-xf (comp - (combine-overlapping-ranges) - (combine-abutting-ranges))) + (combine-overlapping-ranges-xf) + (combine-abutting-ranges-xf))) -(defn consolidate [ranges] +(defn consolidate + "Take a set of ranges and consolidate/collapse the ranges + into the minimal set of ranges needed to represent the original + range set" + [ranges] (into #{} consolidate-ranges-xf (ordered-range-values ranges))) -(defn walk-range-boundaries [range-boundary-items] - (let [close-working-range (fn [range-type start end ranges] - (conj! ranges (->discrete-value-range range-type - start - end)))] - (loop [[boundary-item & boundary-items] range-boundary-items - in-filter-range nil - in-source-range nil - ranges (transient #{}) - close-fn nil] - (match [boundary-item] - [nil] - (persistent! ranges) +(defn- walk-tagged-range-boundaries-xf + "transducer to create the set of ranges for the difference function + by walking range boundaries items that have been taged as either + belonging to the the source set, or the set of ranges to filter + out of the source set, and construct the set of resulting ranges + with the ranges in the filter ranges item removed from the source + set of ranges." + [] + (let [close-working-range (fn [range-type start end] + (->discrete-value-range range-type + start + end)) + in-filter-range (volatile! nil) + in-source-range (volatile! nil) + close-fn (volatile! nil)] + (fn [xf] + (fn + ([] (xf)) + ([result] (xf result)) + ([result boundary-item] + (case [(:boundary-type boundary-item) (:range-source-type boundary-item)] + [:start :source-range] + (do + (vreset! in-source-range true) + (vreset! close-fn (partial close-working-range + (:type boundary-item) + (:value boundary-item))) + result) - [{:boundary-type :start - :range-source-type :source-range - :type range-type - :value new-working-range-start-value}] - (recur boundary-items in-filter-range true ranges (partial close-working-range - range-type - new-working-range-start-value)) + [:end :source-range] + (let [close @close-fn] + (vreset! in-source-range false) + (vreset! close-fn nil) + (if @in-filter-range + result + (xf result (close (:value boundary-item))))) - [{:boundary-type :end - :range-source-type :source-range - :value value}] - (if in-filter-range - (recur boundary-items in-filter-range false ranges nil) - (recur boundary-items in-filter-range false (close-fn value ranges) nil)) + [:start :filter-range] + (do + (vreset! in-filter-range true) + (if-let [close @close-fn] + (do + (vreset! close-fn nil) + (xf result (close (:prev-value boundary-item)))) + result)) - [{:boundary-type :start - :range-source-type :filter-range - :prev-value prev-value}] - (if close-fn - (recur boundary-items true in-source-range (close-fn prev-value ranges) nil) - (recur boundary-items true in-source-range ranges close-fn)) + [:end :filter-range] - [{:boundary-type :end - :range-source-type :filter-range - :type range-type - :next-value new-working-range-start-value}] - (if in-source-range - (recur boundary-items false in-source-range ranges (partial close-working-range - range-type - new-working-range-start-value)) - (recur boundary-items false in-source-range ranges close-fn)))))) + (do + (vreset! in-filter-range false) + (when @in-source-range + (vreset! close-fn (partial close-working-range + (:type boundary-item) + (:next-value boundary-item)))) + result))))))) (def ^:private range-boundary-source-compare "Sorts range boundary items @@ -219,6 +240,6 @@ 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))) + (into #{} (walk-tagged-range-boundaries-xf) 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 92fcaa5..9a0a3d0 100644 --- a/test/challenge/discrete_value_range_test.clj +++ b/test/challenge/discrete_value_range_test.clj @@ -6,13 +6,17 @@ (defrecord IntInclusiveDiscreteValueRange [^int start ^int end] range/DiscreteValueRange - (abuts [_this other] - (or (= 1 (abs (- start (.end other)))) - (= 1 (abs (- end (.start other)))))) + (range/abuts? [_this other] + (or (= 1 (abs (- start (range/end other)))) + (= 1 (abs (- end (range/start other)))))) (value-before [__this] - (dec start)) + (if (= start Integer/MIN_VALUE) + Integer/MIN_VALUE + (dec start))) (value-after [_this] - (inc end)) + (if (= end Integer/MAX_VALUE) + Integer/MAX_VALUE + (inc end))) (start [_this] start) (end [_this] @@ -20,12 +24,14 @@ (range-type [_this] :int-range-inclusive) (union [this other] - (when-not (range/abuts 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-type this) - (min start (.start other)) - (max end (.end other)))) + (range/->discrete-value-range (range/range-type this) + (min start (range/start other)) + (max end (range/end other)))) Object (toString [_this] @@ -45,15 +51,15 @@ (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 "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" @@ -134,260 +140,260 @@ (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}])))) + (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)} - (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}]))) + (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)} - (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}])))) + (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 (= #{} - (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}])))) + (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)} - (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}])))) + (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)} - (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}])))) + (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)} - (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}])))) + (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)} - (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}])))) + (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)} - (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}])))) + (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 (= #{} - (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}])))) + (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)} - (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}]))))) + (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"