diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/unheard/interval.clj | 88 | ||||
| -rw-r--r-- | src/unheard/interval_test.clj | 178 |
2 files changed, 266 insertions, 0 deletions
diff --git a/src/unheard/interval.clj b/src/unheard/interval.clj new file mode 100644 index 0000000..2ec5645 --- /dev/null +++ b/src/unheard/interval.clj @@ -0,0 +1,88 @@ +(ns unheard.interval + "Custom IInterval implementation for Clojure ratios with arbitrary values." + (:import [com.brein.time.timeintervals.intervals IInterval] + [com.brein.time.timeintervals.collections ListIntervalCollection])) + +(deftype RatioValueInterval [start end value] + IInterval + (getNormStart [_] start) + (getNormEnd [_] end) + (getUniqueIdentifier [_] (str "[" start "," end "]")) + (compareTo [_ other] + (let [start-cmp (compare start (.getNormStart ^RatioValueInterval other))] + (if (zero? start-cmp) + (compare end (.getNormEnd ^RatioValueInterval other)) + start-cmp))) + Object + (toString [_] (str "[" start ", " end "] -> " value))) + +;; Public API + +(defn ratio-interval + "Create a RatioValueInterval with ratio boundaries and arbitrary value. + + Args: + start - Clojure ratio for interval start (inclusive) + end - Clojure ratio for interval end (inclusive) + value - Arbitrary data to associate with interval + + Returns: + Instance of RatioValueInterval implementing IInterval<Ratio> + + Example: + (ratio-interval 1/4 3/4 {:note :C :velocity 64})" + [start end value] + (->RatioValueInterval start end value)) + +(defn interval-value + "Get the value associated with a RatioValueInterval. + + Args: + interval - A RatioValueInterval instance + + Returns: + The value stored in the interval" + [^RatioValueInterval interval] + (.value interval)) + +(defn overlaps? + "Check if two intervals overlap. + + Two intervals [a,b] and [c,d] overlap if max(a,c) <= min(b,d)" + [^RatioValueInterval iv1 ^RatioValueInterval iv2] + (let [start1 (.getNormStart iv1) + end1 (.getNormEnd iv1) + start2 (.getNormStart iv2) + end2 (.getNormEnd iv2)] + (<= (max (compare start1 start2)) (min (compare end1 end2))))) + +(defn find-overlaps + "Find all intervals in a collection that overlap with the query interval. + + Args: + coll - A collection of intervals (any Iterable) + query - The interval to query for overlaps + + Returns: + A sequence of intervals that overlap with the query" + [coll ^RatioValueInterval query] + (let [q-start (.getNormStart query) + q-end (.getNormEnd query)] + (filter (fn [^RatioValueInterval iv] + (let [start (.getNormStart iv) + end (.getNormEnd iv)] + ;; Intervals overlap if: start <= q-end AND end >= q-start + (and (<= (compare start q-end) 0) + (>= (compare end q-start) 0)))) + coll))) + +(defn create-ratio-interval-collection + "Create a simple ListIntervalCollection for RatioValueInterval instances. + + This uses a simple list-based collection. Use find-overlaps to query for + overlapping intervals. + + Returns: + Configured IntervalCollection ready to accept ratio-interval instances" + [] + (ListIntervalCollection.)) diff --git a/src/unheard/interval_test.clj b/src/unheard/interval_test.clj new file mode 100644 index 0000000..6878923 --- /dev/null +++ b/src/unheard/interval_test.clj @@ -0,0 +1,178 @@ +(ns unheard.interval-test + (:require [unheard.interval :as sut] + [hyperfiddle.rcf :refer [tests]])) + +(tests "Create interval with ratio boundaries and value" + (let [iv (sut/ratio-interval 1/4 3/4 {:note :C, :velocity 64})] + (.getNormStart iv) + := + 1/4 + (.getNormEnd iv) + := + 3/4 (sut/interval-value iv) + := {:note :C, :velocity 64})) + +(tests "Interval unique identifier" + (let [iv (sut/ratio-interval 1/4 3/4 :test)] + (.getUniqueIdentifier iv) + := + "[1/4,3/4]")) + +(tests "Interval comparison - start takes precedence" + (let [iv1 (sut/ratio-interval 1/4 1/2 :first) + iv2 (sut/ratio-interval 1/2 3/4 :second) + iv3 (sut/ratio-interval 1/3 2/3 :third)] + ;; iv1 starts before iv2 + (.compareTo iv1 iv2) + := + -1 + (.compareTo iv2 iv1) + := + 1 + ;; iv1 starts before iv3 + (.compareTo iv1 iv3) + := + -1 + ;; iv3 starts after iv1 but before iv2 + (.compareTo iv3 iv2) + := -1)) + +(tests "Interval comparison - end is tiebreaker when starts are equal" + (let [iv1 (sut/ratio-interval 1/4 1/2 :a) + iv2 (sut/ratio-interval 1/4 3/4 :b)] + ;; Same start, iv1 ends before iv2 + (.compareTo iv1 iv2) + := + -1 (.compareTo iv2 iv1) + := 1)) + +(tests "Interval comparison - equal intervals" + (let [iv1 (sut/ratio-interval 1/4 1/2 :a) + iv2 (sut/ratio-interval 1/4 1/2 :b)] + ;; Same boundaries (values don't matter for comparison) + (.compareTo iv1 iv2) + := + 0)) + +(tests "Find overlaps - basic cases" + (let [coll (sut/create-ratio-interval-collection) + iv1 (sut/ratio-interval 1/4 1/2 :first) + iv2 (sut/ratio-interval 1/2 3/4 :second) + iv3 (sut/ratio-interval 1/3 2/3 :third)] + (.add coll iv1) + (.add coll iv2) + (.add coll iv3) + ;; Query [1/3, 1/2] should overlap with all three + (let [query (sut/ratio-interval 1/3 1/2 nil) + overlaps (sut/find-overlaps coll query)] + (count overlaps) + := + 3) + ;; Query [5/8, 7/8] should overlap with iv2 and iv3 + (let [query (sut/ratio-interval 5/8 7/8 nil) + overlaps (sut/find-overlaps coll query)] + (count overlaps) + := + 2))) + +(tests "Find overlaps - touching intervals at boundaries" + (let [coll (sut/create-ratio-interval-collection) + iv1 (sut/ratio-interval 0 1/4 :left) + iv2 (sut/ratio-interval 1/4 1/2 :right)] + (.add coll iv1) + (.add coll iv2) + ;; Query at exact boundary point should overlap both + (let [query (sut/ratio-interval 1/4 1/4 :boundary) + overlaps (sut/find-overlaps coll query)] + (count overlaps) + := + 2 (set (map sut/interval-value overlaps)) + := #{:left :right}))) + +(tests "Find overlaps - point interval" + (let [coll (sut/create-ratio-interval-collection) + iv1 (sut/ratio-interval 1/4 1/2 :wide) + iv2 (sut/ratio-interval 1/3 1/3 :point)] + (.add coll iv1) + (.add coll iv2) + ;; Point query inside wide interval + (let [query (sut/ratio-interval 1/3 1/3 nil) + overlaps (sut/find-overlaps coll query)] + (count overlaps) + := + 2))) + +(tests "Find overlaps - no overlap" + (let [coll (sut/create-ratio-interval-collection) + iv1 (sut/ratio-interval 1/4 1/2 :first) + iv2 (sut/ratio-interval 3/4 7/8 :second)] + (.add coll iv1) + (.add coll iv2) + ;; Query between the two intervals + (let [query (sut/ratio-interval 5/8 11/16 nil) + overlaps (sut/find-overlaps coll query)] + (count overlaps) + := + 0))) + +(tests "Interval with improper fractions" + (let [iv (sut/ratio-interval 5/4 7/4 :improper)] + (.getNormStart iv) + := + 5/4 + (.getNormEnd iv) + := + 7/4 (sut/interval-value iv) + := :improper)) + +(tests "Interval with negative ratios" + (let [iv (sut/ratio-interval -3/4 -1/4 :negative)] + (.getNormStart iv) + := + -3/4 (.getNormEnd iv) + := -1/4)) + +(tests "Interval toString representation" + (let [iv (sut/ratio-interval 1/4 3/4 {:data :test})] + (str iv) + := + "[1/4, 3/4] -> {:data :test}")) + +(tests "Collection operations" + (let [coll (sut/create-ratio-interval-collection) + iv1 (sut/ratio-interval 1/4 1/2 :first) + iv2 (sut/ratio-interval 1/2 3/4 :second)] + (.add coll iv1) + (.add coll iv2) + (.size coll) + := + 2 + ;; Can iterate over collection + (set (map sut/interval-value coll)) + := + #{:first :second} + ;; Can clear collection + (.clear coll) + (.size coll) + := + 0)) + +(tests "Value field can hold various types" + (let [iv-map (sut/ratio-interval 0 1 {:key :value}) + iv-keyword (sut/ratio-interval 0 1 :keyword) + iv-string (sut/ratio-interval 0 1 "string") + iv-number (sut/ratio-interval 0 1 42) + iv-nil (sut/ratio-interval 0 1 nil)] + (sut/interval-value iv-map) + := + {:key :value} + (sut/interval-value iv-keyword) + := + :keyword + (sut/interval-value iv-string) + := + "string" + (sut/interval-value iv-number) + := + 42 (sut/interval-value iv-nil) + := nil)) |
