summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-11-26 15:10:20 -0500
committerJake Zerrer <him@jakezerrer.com>2025-12-01 16:02:53 -0500
commitbc500bb5dc9f32b7b50ce39c5a98df13322e4167 (patch)
treec123662150db8dd6752156e0c22d2f6859fa4047
parent322b66627fd619c2ce0f2a35eae043e9304ee8bc (diff)
Create `paste` operator
-rw-r--r--src/unheard/cycles.clj38
-rw-r--r--test/unheard/cycles_test.clj62
2 files changed, 98 insertions, 2 deletions
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj
index 29cad91..3cbfb97 100644
--- a/src/unheard/cycles.clj
+++ b/src/unheard/cycles.clj
@@ -92,6 +92,44 @@
(defn scalar? [x]
(not (and (map? x) (:type x))))
+(defn paste
+ "Paste operator: replaces scalar values in a template pattern with provided values.
+
+ Takes a template pattern and a sequence of replacement values. Each scalar
+ in the template (in depth-first order) is replaced by the corresponding value
+ from the replacement sequence. If a replacement value is nil, the original
+ scalar is preserved.
+
+ This allows separating rhythmic structure from content. Any values can be used
+ as placeholders in the template (commonly keywords like :_ or numbers).
+
+ Examples:
+ (paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d)
+ => (f :c (f :e :g) :b :d)
+
+ (paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d)
+ => (f :c (f :e :_) :b :d)
+
+ (paste (l 1 2 1) :a :b :c)
+ => (l :a :b :c)"
+ [template & values]
+ (let [values-seq (atom (seq values))]
+ (letfn [(replace-scalars [node]
+ (if (scalar? node)
+ (let [replacement (first @values-seq)]
+ (swap! values-seq rest)
+ (if (nil? replacement)
+ node
+ replacement))
+ ;; Non-scalar: check if :v is a vector (l, f, p) or single value (rate, elongate, rep)
+ (let [v (:v node)]
+ (if (vector? v)
+ ;; Combinators with multiple children
+ (assoc node :v (mapv replace-scalars v))
+ ;; Modifiers with single child
+ (assoc node :v (replace-scalars v))))))]
+ (replace-scalars template))))
+
(defn get-weight
"Returns the weight of a node. Elongated nodes have their specified weight,
all other nodes have a default weight of 1."
diff --git a/test/unheard/cycles_test.clj b/test/unheard/cycles_test.clj
index e2e1889..c395755 100644
--- a/test/unheard/cycles_test.clj
+++ b/test/unheard/cycles_test.clj
@@ -1,6 +1,6 @@
(ns unheard.cycles-test
(:require [clojure.test :refer [deftest is testing]]
- [unheard.cycles :refer [l f p rate elongate rep unfold]]))
+ [unheard.cycles :refer [l f p rate elongate rep paste unfold]]))
(deftest unfold-tests
(testing "single scalar"
@@ -219,4 +219,62 @@
(testing "rep of list"
(is (= [[0 1/4 :a] [1/4 1/2 :b]
[1/2 3/4 :a] [3/4 1 :b]]
- (unfold 1 (rep 2 (l :a :b)))))))
+ (unfold 1 (rep 2 (l :a :b))))))
+
+ (testing "paste - simple replacement in list"
+ (is (= (l :a :b :c)
+ (paste (l :_ :_ :_) :a :b :c))))
+
+ (testing "paste - simple replacement in fork"
+ (is (= (f :c (f :e :g) :b :d)
+ (paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d))))
+
+ (testing "paste - with nil preserves original"
+ (is (= (f :c (f :e :_) :b :d)
+ (paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d))))
+
+ (testing "paste - multiple nils"
+ (is (= (f :_ (f :e :_) :b :_)
+ (paste (f :_ (f :_ :_) :_ :_) nil :e nil :b nil))))
+
+ (testing "paste - with numbers as template"
+ (is (= (l :a :b :c)
+ (paste (l 1 2 1) :a :b :c))))
+
+ (testing "paste - nested structures"
+ (is (= (l :x (l :y :z) :w)
+ (paste (l :_ (l :_ :_) :_) :x :y :z :w))))
+
+ (testing "paste - with parallel"
+ (is (= (p :a :b :c)
+ (paste (p :_ :_ :_) :a :b :c))))
+
+ (testing "paste - complex nested with parallel"
+ (is (= (l :a (p :b :c) :d)
+ (paste (l :_ (p :_ :_) :_) :a :b :c :d))))
+
+ (testing "paste - with modifiers preserved"
+ (is (= (l (rate 2 :a) :b)
+ (paste (l (rate 2 :_) :_) :a :b))))
+
+ (testing "paste - with elongate"
+ (is (= (l (elongate 2 :x) :y :z)
+ (paste (l (elongate 2 :_) :_ :_) :x :y :z))))
+
+ (testing "paste - with rep"
+ (is (= (l (rep 3 :a) :b)
+ (paste (l (rep 3 :_) :_) :a :b))))
+
+ (testing "paste - unfolds correctly"
+ (is (= [[0 1/3 :c] [1/3 2/3 :e] [2/3 1 :g]]
+ (unfold 1 (paste (l :_ :_ :_) :c :e :g)))))
+
+ (testing "paste - complex rhythm unfolds correctly"
+ (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d]
+ [4 5 :c] [5 6 :g] [6 7 :b] [7 8 :d]]
+ (unfold 1 (paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d)))))
+
+ (testing "paste - with nil unfolds correctly"
+ (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d]
+ [4 5 :c] [5 6 :_] [6 7 :b] [7 8 :d]]
+ (unfold 1 (paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d))))))