From ad431241afd98a7f466488ea6d1a8174c5266897 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Mon, 8 Jun 2026 15:55:13 +0200 Subject: [PATCH] Fix O(n^2) apply/revert; add lcs-length, edit-distance, similarity Replace Array.concat in apply and revert with element-wise push-back to avoid quadratic allocation. Add lcs-length, edit-distance, and similarity functions for text comparison use cases. --- diff.carp | 35 +++++++++++++++++++++--- tests/diff.carp | 72 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/diff.carp b/diff.carp index 08f2f96..7462d79 100644 --- a/diff.carp +++ b/diff.carp @@ -76,14 +76,17 @@ (doc deletions "Filters a diff to only the Deletion hunks.") (defn deletions [d] (Array.copy-filter &deleted? d)) + (defn- push-all! [res xs] + (for [j 0 (Array.length xs)] (Array.push-back! res @(Array.unsafe-nth xs j)))) + (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)])) + (Eq xs) (push-all! &res xs) + (Insertion xs) (push-all! &res xs) (Deletion _) ())) res)) @@ -93,11 +96,35 @@ (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)])) + (Eq xs) (push-all! &res xs) (Insertion _) () - (Deletion xs) (set! res (Array.concat &[res (Array.copy xs)])))) + (Deletion xs) (push-all! &res xs))) res)) + (doc lcs-length + "Returns the length of the longest common subsequence of two arrays.") + (defn lcs-length [old new] + (let-do [d &(diff old new) + len 0] + (for [i 0 (Array.length d)] + (match-ref (Array.unsafe-nth d i) + (Eq xs) (set! len (+ len (Array.length xs))) + _ ())) + len)) + + (doc edit-distance + "Returns the edit distance (number of insertions plus deletions) between two arrays.") + (defn edit-distance [old new] + (- (+ (Array.length old) (Array.length new)) (* 2 (lcs-length old new)))) + + (doc similarity + "Returns a similarity ratio between 0.0 and 1.0 for two arrays, based on their longest common subsequence. Two empty arrays have similarity 1.0.") + (defn similarity [old new] + (let [total (+ (Array.length old) (Array.length new))] + (if (= total 0) + 1.0 + (/ (Double.from-int (* 2 (lcs-length old new))) (Double.from-int total))))) + (defn- emit-hunk [output old-start old-count new-start new-count hunk-lines] (let [header (String.concat &[@"@@ -" diff --git a/tests/diff.carp b/tests/diff.carp index 187bdee..cb3cf66 100644 --- a/tests/diff.carp +++ b/tests/diff.carp @@ -246,4 +246,74 @@ g h i j -KK")) "unified generates multiple hunks for distant changes"))) +KK")) "unified generates multiple hunks for distant changes") + + ; lcs-length tests + (assert-equal test + 3 + (Diff.lcs-length &[1 2 3] &[1 2 3]) + "lcs-length of identical arrays is their length") + (assert-equal test + 2 + (Diff.lcs-length &[1 4 3] &[1 2 3]) + "lcs-length of partially matching arrays") + (assert-equal test + 0 + (Diff.lcs-length &[1 2 3] &[4 5 6]) + "lcs-length of completely different arrays is 0") + (assert-equal test + 0 + (Diff.lcs-length &[] &(the (Array Int) [])) + "lcs-length of two empty arrays is 0") + (assert-equal test + 0 + (Diff.lcs-length &[] &[1 2 3]) + "lcs-length with one empty array is 0") + (assert-equal test + 2 + (Diff.lcs-length &[1 2] &[1 2 3 4]) + "lcs-length when one is prefix of other") + + ; edit-distance tests + (assert-equal test + 0 + (Diff.edit-distance &[1 2 3] &[1 2 3]) + "edit-distance of identical arrays is 0") + (assert-equal test + 2 + (Diff.edit-distance &[1 4 3] &[1 2 3]) + "edit-distance with one substitution is 2 (delete + insert)") + (assert-equal test + 6 + (Diff.edit-distance &[1 2 3] &[4 5 6]) + "edit-distance of completely different arrays") + (assert-equal test + 0 + (Diff.edit-distance &[] &(the (Array Int) [])) + "edit-distance of two empty arrays is 0") + (assert-equal test + 3 + (Diff.edit-distance &[] &[1 2 3]) + "edit-distance with one empty array") + (assert-equal test + 2 + (Diff.edit-distance &[1 2] &[1 2 3 4]) + "edit-distance when one is prefix of other") + + ; similarity tests + (assert-equal test + 1.0 + (Diff.similarity &[1 2 3] &[1 2 3]) + "similarity of identical arrays is 1.0") + (assert-equal test + 0.0 + (Diff.similarity &[1 2 3] &[4 5 6]) + "similarity of completely different arrays is 0.0") + (assert-equal test + 1.0 + (Diff.similarity &[] &(the (Array Int) [])) + "similarity of two empty arrays is 1.0") + (assert-equal test + 0.0 + (Diff.similarity &[] &[1 2 3]) + "similarity with one empty array is 0.0")))