summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/unheard/interval.clj88
-rw-r--r--src/unheard/interval_test.clj178
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))