forked from lambdaisland/deep-diff2
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiff_impl.cljc
More file actions
290 lines (240 loc) · 8.35 KB
/
diff_impl.cljc
File metadata and controls
290 lines (240 loc) · 8.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
(ns lambdaisland.deep-diff2.diff-impl
(:require [clojure.data :as data]
[lambdaisland.clj-diff.core :as seq-diff]))
(declare diff diff-similar)
(defrecord Mismatch [- +])
(defrecord Deletion [-])
(defrecord Insertion [+])
(defprotocol Diff
(-diff-similar [x y]))
;; For property based testing
(defprotocol Undiff
(left-undiff [x])
(right-undiff [x]))
(defn shift-insertions [ins]
(reduce (fn [res idx]
(let [offset (apply + (map count (vals res)))]
(assoc res (+ idx offset) (get ins idx))))
{}
(sort (keys ins))))
(defn replacements
"Given a set of deletion indexes and a map of insertion index to value sequence,
match up deletions and insertions into replacements, returning a map of
replacements, a set of deletions, and a map of insertions."
[[del ins]]
;; Loop over deletions, if they match up with an insertion, turn them into a
;; replacement. This could be a reduce over (sort del) tbh but it's already a
;; lot more readable than the first version.
(loop [rep {}
del del
del-rest (sort del)
ins ins]
(if-let [d (first del-rest)]
(if-let [i (seq (get ins d))] ;; matching insertion
(recur (assoc rep d (first i))
(disj del d)
(next del-rest)
(update ins d next))
(if-let [i (seq (get ins (dec d)))]
(recur (assoc rep d (first i))
(disj del d)
(next del-rest)
(-> ins
(dissoc (dec d))
(assoc d (seq (concat (next i)
(get ins d))))))
(recur rep
del
(next del-rest)
ins)))
[rep del (into {}
(remove (comp nil? val))
(shift-insertions ins))])))
(defn del+ins
"Wrapper around clj-diff that returns deletions and insertions as a set and map
respectively."
[exp act]
(let [{del :- ins :+} (seq-diff/diff exp act)]
[(into #{} del)
(into {} (map (fn [[k & vs]] [k (vec vs)])) ins)]))
(defn diff-seq-replacements [replacements s]
(map-indexed
(fn [idx v]
(if (contains? replacements idx)
(diff v (get replacements idx))
v))
s))
(defn diff-seq-deletions [del s]
(map
(fn [v idx]
(if (contains? del idx)
(->Deletion v)
v))
s
(range)))
(defn diff-seq-insertions [ins s]
(reduce (fn [res [idx vs]]
(concat (take (inc idx) res) (map ->Insertion vs) (drop (inc idx) res)))
s
ins))
(defn diff-seq [exp act]
(let [[rep del ins] (replacements (del+ins exp act))]
(->> exp
(diff-seq-replacements rep)
(diff-seq-deletions del)
(diff-seq-insertions ins)
(into []))))
(defn diff-set [exp act]
(into
(into #{}
(map (fn [e]
(if (contains? act e)
e
(->Deletion e))))
exp)
(map ->Insertion)
(remove #(contains? exp %) act)))
(defn diff-map [exp act]
(first
(let [exp-ks (keys exp)
act-ks (keys act)
[del ins] (del+ins exp-ks act-ks)]
(reduce
(fn [[m idx] k]
[(cond-> m
(contains? del idx)
(assoc (->Deletion k) (get exp k))
(not (contains? del idx))
(assoc k (diff (get exp k) (get act k)))
(contains? ins idx)
(into (map (juxt ->Insertion (partial get act))) (get ins idx)))
(inc idx)])
[(if (contains? ins -1)
(into {} (map (juxt ->Insertion (partial get act))) (get ins -1))
{}) 0]
exp-ks))))
(defn primitive? [x]
(or (number? x) (string? x) (boolean? x) (inst? x) (keyword? x) (symbol? x)))
(defn diff-atom [exp act]
(if (= exp act)
exp
(->Mismatch exp act)))
(defn diff-similar [x y]
(if (primitive? x)
(diff-atom x y)
(-diff-similar x y)))
(defn diffable? [exp]
(satisfies? Diff exp))
;; ClojureScript has this, Clojure doesn't
#?(:clj
(defn array? [x]
(and x (.isArray (class x)))))
(defn diff [exp act]
(cond
(nil? exp)
(diff-atom exp act)
(and (diffable? exp)
(= (data/equality-partition exp) (data/equality-partition act)))
(diff-similar exp act)
(array? exp)
(diff-seq exp act)
(record? exp)
(diff-map exp act)
:else
(diff-atom exp act)))
(extend-protocol Diff
#?(:clj java.util.Set :cljs cljs.core/PersistentHashSet)
(-diff-similar [exp act]
(diff-set exp act))
#?@(:clj
[java.util.List
(-diff-similar [exp act] (diff-seq exp act))
java.util.Map
(-diff-similar [exp act] (diff-map exp act))]
:cljs
[cljs.core/List
(-diff-similar [exp act] (diff-seq exp act))
cljs.core/PersistentVector
(-diff-similar [exp act] (diff-seq exp act))
cljs.core/EmptyList
(-diff-similar [exp act] (diff-seq exp act))
cljs.core/PersistentHashMap
(-diff-similar [exp act] (diff-map exp act))
cljs.core/PersistentArrayMap
(-diff-similar [exp act] (diff-map exp act))]))
(extend-protocol Undiff
Mismatch
(left-undiff [m] (get m :-))
(right-undiff [m] (get m :+))
Insertion
(right-undiff [m] (get m :+))
Deletion
(left-undiff [m] (get m :-))
nil
(left-undiff [m] m)
(right-undiff [m] m)
#?(:clj Object :cljs default)
(left-undiff [m] m)
(right-undiff [m] m)
#?@(:clj
[java.util.List
(left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s)))
(right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s)))
java.util.Set
(left-undiff [s] (set (left-undiff (seq s))))
(right-undiff [s] (set (right-undiff (seq s))))
java.util.Map
(left-undiff [m]
(into {}
(comp (remove #(instance? Insertion (key %)))
(map (juxt (comp left-undiff key) (comp left-undiff val))))
m))
(right-undiff [m]
(into {}
(comp (remove #(instance? Deletion (key %)))
(map (juxt (comp right-undiff key) (comp right-undiff val))))
m))]
:cljs
[cljs.core/List
(left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s)))
(right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s)))
cljs.core/EmptyList
(left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s)))
(right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s)))
cljs.core/PersistentHashSet
(left-undiff [s] (set (left-undiff (seq s))))
(right-undiff [s] (set (right-undiff (seq s))))
cljs.core/PersistentTreeSet
(left-undiff [s] (set (left-undiff (seq s))))
(right-undiff [s] (set (right-undiff (seq s))))
cljs.core/PersistentVector
(left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s)))
(right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s)))
cljs.core/KeySeq
(left-undiff [s] (map left-undiff (remove #(instance? Insertion %) s)))
(right-undiff [s] (map right-undiff (remove #(instance? Deletion %) s)))
cljs.core/PersistentArrayMap
(left-undiff [m]
(into {}
(comp (remove #(instance? Insertion (key %)))
(map (juxt (comp left-undiff key) (comp left-undiff val))))
m))
(right-undiff [m]
(into {}
(comp (remove #(instance? Deletion (key %)))
(map (juxt (comp right-undiff key) (comp right-undiff val))))
m))
cljs.core/PersistentHashMap
(left-undiff [m]
(into {}
(comp (remove #(instance? Insertion (key %)))
(map (juxt (comp left-undiff key) (comp left-undiff val))))
m))
(right-undiff [m]
(into {}
(comp (remove #(instance? Deletion (key %)))
(map (juxt (comp right-undiff key) (comp right-undiff val))))
m))
cljs.core/UUID
(left-undiff [m] m)
(right-undiff [m] m)]))