Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions diff.carp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
(Deletion x) (match @b (Deletion y) (= &x &y) _ false)))
(implements = Diff.=)

(doc diff
"Computes the diff between two arrays, returning a sequence of Eq, Insertion, and Deletion hunks.")
(defn diff [old new]
(let-do [oidxm {}
overlap {}
Expand Down Expand Up @@ -48,6 +50,7 @@
[(Eq (Array.slice new ssn (+ ssn sl)))]])
(diff &(Array.suffix old (+ sso sl)) &(Array.suffix new (+ ssn sl)))]))))

(doc string-diff "Diffs two strings word by word, splitting on whitespace.")
(defn string-diff [old new] (diff &(String.words old) &(String.words new)))

(doc line-diff "Diffs two strings line by line, splitting on newlines.")
Expand All @@ -57,16 +60,44 @@
(doc char-diff "Diffs two strings character by character.")
(defn char-diff [old new] (diff &(String.chars old) &(String.chars new)))

(doc eq? "Returns true if the diff hunk is an Eq.")
(defn eq? [d] (match @d (Eq _) true _ false))

(doc inserted? "Returns true if the diff hunk is an Insertion.")
(defn inserted? [d] (match @d (Insertion _) true _ false))

(doc deleted? "Returns true if the diff hunk is a Deletion.")
(defn deleted? [d] (match @d (Deletion _) true _ false))

(doc eq "Filters a diff to only the Eq hunks.")
(defn eq [d] (Array.copy-filter &eq? d))
(doc insertions "Filters a diff to only the Insertion hunks.")
(defn insertions [d] (Array.copy-filter &inserted? d))
(doc deletions "Filters a diff to only the Deletion hunks.")
(defn deletions [d] (Array.copy-filter &deleted? d))

(doc apply
"Reconstructs the 'new' array from a diff by collecting Eq and Insertion elements.")
(defn apply [d]
(let-do [res []]
(for [i 0 (Array.length d)]
(match-ref (Array.unsafe-nth d i)
(Eq xs) (set! res (Array.concat &[res (Array.copy xs)]))
(Insertion xs) (set! res (Array.concat &[res (Array.copy xs)]))
(Deletion _) ()))
res))

(doc revert
"Reconstructs the 'old' array from a diff by collecting Eq and Deletion elements.")
(defn revert [d]
(let-do [res []]
(for [i 0 (Array.length d)]
(match-ref (Array.unsafe-nth d i)
(Eq xs) (set! res (Array.concat &[res (Array.copy xs)]))
(Insertion _) ()
(Deletion xs) (set! res (Array.concat &[res (Array.copy xs)]))))
res))

(defn- emit-hunk [output old-start old-count new-start new-count hunk-lines]
(let [header (String.concat
&[@"@@ -"
Expand Down
91 changes: 91 additions & 0 deletions tests/diff.carp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,97 @@
&(diff &[1 2 3 4] &[3 4])
"new is a suffix of old")

; apply tests
(assert-equal test
&[1 2 3]
&(Diff.apply &(diff &[1 2 3] &[1 2 3]))
"apply on equal arrays returns the same array")
(assert-equal test
&[1 2 3]
&(Diff.apply &(diff &[1 4 3] &[1 2 3]))
"apply reconstructs the new array")
(assert-equal test
&(the (Array Int) [])
&(Diff.apply &(diff &[1 2 3] &[]))
"apply on all-deletion diff returns empty")
(assert-equal test
&[1 2 3]
&(Diff.apply &(diff &[] &[1 2 3]))
"apply on all-insertion diff returns the inserted elements")
(assert-equal test
&(the (Array Int) [])
&(Diff.apply &(diff &[] &(the (Array Int) [])))
"apply on empty diff returns empty")
(assert-equal test
&[1 2 3 4]
&(Diff.apply &(diff &[1 2] &[1 2 3 4]))
"apply with appended elements")

; apply/revert on hand-built mixed diffs — demonstrates
; reconstructing either side from the diff structure alone
(assert-equal test
&[1 2 10 20 4 30]
&(Diff.apply
&[(Eq [1 2])
(Insertion [10 20])
(Deletion [3])
(Eq [4])
(Deletion [5 6])
(Insertion [30])])
"apply on mixed diff collects Eq+Insertion, skips Deletion")
(assert-equal test
&[1 2 3 4 5 6]
&(Diff.revert
&[(Eq [1 2])
(Insertion [10 20])
(Deletion [3])
(Eq [4])
(Deletion [5 6])
(Insertion [30])])
"revert on mixed diff collects Eq+Deletion, skips Insertion")
(assert-equal test
&[@"hello" @"brave" @"new" @"world"]
&(Diff.apply
&[(Eq [@"hello"])
(Deletion [@"cruel" @"old"])
(Insertion [@"brave" @"new"])
(Eq [@"world"])])
"apply on string mixed diff reconstructs the new side")
(assert-equal test
&[@"hello" @"cruel" @"old" @"world"]
&(Diff.revert
&[(Eq [@"hello"])
(Deletion [@"cruel" @"old"])
(Insertion [@"brave" @"new"])
(Eq [@"world"])])
"revert on string mixed diff reconstructs the old side")

; revert tests
(assert-equal test
&[1 2 3]
&(Diff.revert &(diff &[1 2 3] &[1 2 3]))
"revert on equal arrays returns the same array")
(assert-equal test
&[1 4 3]
&(Diff.revert &(diff &[1 4 3] &[1 2 3]))
"revert reconstructs the old array")
(assert-equal test
&[1 2 3]
&(Diff.revert &(diff &[1 2 3] &[]))
"revert on all-deletion diff returns the deleted elements")
(assert-equal test
&(the (Array Int) [])
&(Diff.revert &(diff &[] &[1 2 3]))
"revert on all-insertion diff returns empty")
(assert-equal test
&(the (Array Int) [])
&(Diff.revert &(diff &[] &(the (Array Int) [])))
"revert on empty diff returns empty")
(assert-equal test
&[1 2 3 4]
&(Diff.revert &(diff &[1 2 3 4] &[1 2]))
"revert with removed suffix")

; line-diff tests
(assert-equal test
&[(Eq [@"a"]) (Insertion [@"new"]) (Deletion [@"old"]) (Eq [@"b"])]
Expand Down