tests pass

This commit is contained in:
2026-01-11 20:56:37 -06:00
parent caf5bda249
commit df24a84908
3 changed files with 213 additions and 13 deletions

View File

@@ -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)

View File

@@ -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)))))

View File

@@ -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)
(is (= #{(int-range-inclusive 0 1)
(int-range-inclusive 3 7)
(int-range-inclusive 9 11)]
(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)})))))))