# Myers' Diff Algorithm implementation # Based on "An O(ND) Difference Algorithm and Its Variations" by Eugene W. Myers class_name GdDiffTool extends RefCounted const DIV_ADD :int = 214 const DIV_SUB :int = 215 class Edit: enum Type { EQUAL, INSERT, DELETE } var type: Type var character: int func _init(t: Type, chr: int) -> void: type = t character = chr # Main entry point - returns [ldiff, rdiff] static func string_diff(left: Variant, right: Variant) -> Array[PackedInt32Array]: var lb := PackedInt32Array() if left == null else str(left).to_utf32_buffer().to_int32_array() var rb := PackedInt32Array() if right == null else str(right).to_utf32_buffer().to_int32_array() # Early exit for identical strings if lb == rb: return [lb.duplicate(), rb.duplicate()] var edits := _myers_diff(lb, rb) return _edits_to_diff_format(edits) # Core Myers' algorithm static func _myers_diff(a: PackedInt32Array, b: PackedInt32Array) -> Array[Edit]: var n := a.size() var m := b.size() var max_d := n + m # V array stores the furthest reaching x coordinate for each k-line # We need indices from -max_d to max_d, so we offset by max_d var v := PackedInt32Array() v.resize(2 * max_d + 1) v.fill(-1) v[max_d + 1] = 0 # k=1 starts at x=0 var trace := [] # Store V arrays for each d to backtrack later # Find the edit distance for d in range(0, max_d + 1): # Store current V for backtracking trace.append(v.duplicate()) for k in range(-d, d + 1, 2): var k_offset := k + max_d # Decide whether to move down or right var x: int if k == -d or (k != d and v[k_offset - 1] < v[k_offset + 1]): x = v[k_offset + 1] # Move down (insert from b) else: x = v[k_offset - 1] + 1 # Move right (delete from a) var y := x - k # Follow diagonal as far as possible (matching characters) while x < n and y < m and a[x] == b[y]: x += 1 y += 1 v[k_offset] = x # Check if we've reached the end if x >= n and y >= m: return _backtrack(a, b, trace, d, max_d) # Should never reach here for valid inputs return [] # Backtrack through the edit graph to build the edit script static func _backtrack(a: PackedInt32Array, b: PackedInt32Array, trace: Array, d: int, max_d: int) -> Array[Edit]: var edits: Array[Edit] = [] var x := a.size() var y := b.size() # Walk backwards through each d value for depth in range(d, -1, -1): var v: PackedInt32Array = trace[depth] var k := x - y var k_offset := k + max_d # Determine previous k var prev_k: int if k == -depth or (k != depth and v[k_offset - 1] < v[k_offset + 1]): prev_k = k + 1 else: prev_k = k - 1 var prev_k_offset := prev_k + max_d var prev_x := v[prev_k_offset] var prev_y := prev_x - prev_k # Extract diagonal (equal) characters while x > prev_x and y > prev_y: x -= 1 y -= 1 #var char_array := PackedInt32Array([a[x]]) edits.insert(0, Edit.new(Edit.Type.EQUAL, a[x])) # Record the edit operation if depth > 0: if x == prev_x: # Insert from b y -= 1 #var char_array := PackedInt32Array([b[y]]) edits.insert(0, Edit.new(Edit.Type.INSERT, b[y])) else: # Delete from a x -= 1 #var char_array := PackedInt32Array([a[x]]) edits.insert(0, Edit.new(Edit.Type.DELETE, a[x])) return edits # Convert edit script to the DIV_ADD/DIV_SUB format static func _edits_to_diff_format(edits: Array[Edit]) -> Array[PackedInt32Array]: var ldiff := PackedInt32Array() var rdiff := PackedInt32Array() for edit in edits: match edit.type: Edit.Type.EQUAL: ldiff.append(edit.character) rdiff.append(edit.character) Edit.Type.INSERT: ldiff.append(DIV_ADD) ldiff.append(edit.character) rdiff.append(DIV_SUB) rdiff.append(edit.character) Edit.Type.DELETE: ldiff.append(DIV_SUB) ldiff.append(edit.character) rdiff.append(DIV_ADD) rdiff.append(edit.character) return [ldiff, rdiff] # prototype static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStringArray: var text1Words := text1.split(" ") var text2Words := text2.split(" ") var text1WordCount := text1Words.size() var text2WordCount := text2Words.size() var solutionMatrix := Array() for i in text1WordCount+1: var ar := Array() for n in text2WordCount+1: ar.append(0) solutionMatrix.append(ar) for i in range(text1WordCount-1, 0, -1): for j in range(text2WordCount-1, 0, -1): if text1Words[i] == text2Words[j]: solutionMatrix[i][j] = solutionMatrix[i + 1][j + 1] + 1; else: solutionMatrix[i][j] = max(solutionMatrix[i + 1][j], solutionMatrix[i][j + 1]); var i := 0 var j := 0 var lcsResultList := PackedStringArray(); while (i < text1WordCount && j < text2WordCount): if text1Words[i] == text2Words[j]: @warning_ignore("return_value_discarded") lcsResultList.append(text2Words[j]) i += 1 j += 1 else: if (solutionMatrix[i + 1][j] >= solutionMatrix[i][j + 1]): i += 1 else: j += 1 return lcsResultList static func markTextDifferences(text1 :String, text2 :String, lcsList :PackedStringArray, insertColor :Color, deleteColor:Color) -> String: var stringBuffer := "" if text1 == null and lcsList == null: return stringBuffer var text1Words := text1.split(" ") var text2Words := text2.split(" ") var i := 0 var j := 0 var word1LastIndex := 0 var word2LastIndex := 0 for k in lcsList.size(): while i < text1Words.size() and j < text2Words.size(): if text1Words[i] == lcsList[k] and text2Words[j] == lcsList[k]: stringBuffer += "" + lcsList[k] + " " word1LastIndex = i + 1 word2LastIndex = j + 1 i = text1Words.size() j = text2Words.size() else: if text1Words[i] != lcsList[k]: while i < text1Words.size() and text1Words[i] != lcsList[k]: stringBuffer += "" + text1Words[i] + " " i += 1 else: if text2Words[j] != lcsList[k]: while j < text2Words.size() and text2Words[j] != lcsList[k]: stringBuffer += "" + text2Words[j] + " " j += 1 i = word1LastIndex j = word2LastIndex while word1LastIndex < text1Words.size(): stringBuffer += "" + text1Words[word1LastIndex] + " " word1LastIndex += 1 while word2LastIndex < text2Words.size(): stringBuffer += "" + text2Words[word2LastIndex] + " " word2LastIndex += 1 return stringBuffer