Skip to content

Commit 6a20959

Browse files
committed
feat(myers-diff): init commit
Signed-off-by: Swarit Pandey <[email protected]>
1 parent 0252d8c commit 6a20959

File tree

4 files changed

+249
-0
lines changed

4 files changed

+249
-0
lines changed

go.mod

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module github.com/swarit-pandey/diff
2+
3+
go 1.21.4
4+
5+
require github.com/olekukonko/tablewriter v0.0.5
6+
7+
require github.com/mattn/go-runewidth v0.0.9 // indirect

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
2+
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
3+
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
4+
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=

main.go

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/olekukonko/tablewriter"
10+
)
11+
12+
type EditAction interface{}
13+
14+
type Keep struct {
15+
line string
16+
}
17+
18+
type Insert struct {
19+
line string
20+
}
21+
22+
type Remove struct {
23+
line string
24+
}
25+
26+
type Frontier struct {
27+
x int
28+
history []EditAction
29+
}
30+
31+
// Myers diff algorithm
32+
// Amazing read: blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/ and other parts
33+
// Few more reads: [https://www.nathaniel.ai/myers-diff/] [https://epxx.co/artigos/diff_en.html]
34+
// Complexity: O((N+M)D) where N and M are the lengths of the sequences and D is the number of edits
35+
// Space: O(N+M)
36+
// Reference implementation in Python: https://gist.github.com/adamnew123456/37923cf53f51d6b9af32a539cdfa7cc4
37+
func myersDiff(aLines, bLines []string) []EditAction {
38+
frontier := make(map[int]Frontier)
39+
frontier[1] = Frontier{0, []EditAction{}}
40+
41+
aMax := len(aLines)
42+
bMax := len(bLines)
43+
for d := 0; d <= aMax+bMax; d++ {
44+
for k := -d; k <= d; k += 2 {
45+
goDown := k == -d || (k != d && frontier[k-1].x < frontier[k+1].x)
46+
47+
var oldX int
48+
var history []EditAction
49+
50+
if goDown {
51+
oldX = frontier[k+1].x
52+
history = append([]EditAction{}, frontier[k+1].history...)
53+
} else {
54+
oldX = frontier[k-1].x + 1
55+
history = append([]EditAction{}, frontier[k-1].history...)
56+
}
57+
58+
y := oldX - k
59+
60+
if 1 <= y && y <= bMax && goDown {
61+
history = append(history, Insert{bLines[y-1]})
62+
} else if 1 <= oldX && oldX <= aMax {
63+
history = append(history, Remove{aLines[oldX-1]})
64+
}
65+
66+
for oldX < aMax && y < bMax && aLines[oldX] == bLines[y] {
67+
history = append(history, Keep{aLines[oldX]})
68+
oldX++
69+
y++
70+
}
71+
72+
if oldX >= aMax && y >= bMax {
73+
return history
74+
} else {
75+
frontier[k] = Frontier{oldX, history}
76+
}
77+
}
78+
}
79+
80+
panic("No file found")
81+
}
82+
83+
func printHorizontal(aLines, bLines []string, diff []EditAction) {
84+
table := tablewriter.NewWriter(os.Stdout)
85+
table.SetHeader([]string{"Original", "Change", "Updated"})
86+
87+
aIndex, bIndex := 0, 0
88+
for _, action := range diff {
89+
var row []string
90+
switch e := action.(type) {
91+
case Keep:
92+
row = []string{aLines[aIndex], " ", bLines[bIndex]}
93+
aIndex++
94+
bIndex++
95+
case Insert:
96+
row = []string{" ", "\033[32m+\033[0m", e.line}
97+
bIndex++
98+
case Remove:
99+
row = []string{e.line, "\033[31m-\033[0m", " "}
100+
aIndex++
101+
}
102+
103+
table.Append(row)
104+
}
105+
106+
table.Render()
107+
}
108+
109+
func main() {
110+
if len(os.Args) < 2 {
111+
fmt.Println("Usage:", os.Args[0], "getdiff <string1> <string2> or <file1>.txt <file2>.txt")
112+
os.Exit(1)
113+
}
114+
115+
mode := os.Args[1]
116+
117+
switch mode {
118+
case "getdiff": // CL args
119+
if len(os.Args) != 4 {
120+
fmt.Println("Usage: getdiff <string1> <string2>")
121+
os.Exit(1)
122+
}
123+
aLines := strings.Split(os.Args[2], "")
124+
bLines := strings.Split(os.Args[3], "")
125+
diff := myersDiff(aLines, bLines)
126+
// printDiff(aLines, bLines, diff)
127+
printHorizontal(aLines, bLines, diff)
128+
default: // File args
129+
if len(os.Args) != 3 {
130+
fmt.Println("Usage:", os.Args[0], "<file1> <file2>")
131+
os.Exit(1)
132+
}
133+
aLines := readLines(os.Args[1])
134+
bLines := readLines(os.Args[2])
135+
diff := myersDiff(aLines, bLines)
136+
// printDiff(aLines, bLines, diff)
137+
printHorizontal(aLines, bLines, diff)
138+
}
139+
}
140+
141+
func readLines(filePath string) []string {
142+
file, err := os.Open(filePath)
143+
if err != nil {
144+
panic(err)
145+
}
146+
defer file.Close()
147+
148+
var lines []string
149+
scanner := bufio.NewScanner(file)
150+
for scanner.Scan() {
151+
lines = append(lines, strings.TrimSpace(scanner.Text()))
152+
}
153+
154+
if err := scanner.Err(); err != nil {
155+
panic(err)
156+
}
157+
158+
return lines
159+
}
160+
161+
// No table print (a bit confusing)
162+
/* func printDiff(aLines, bLines []string, diff []EditAction) {
163+
aIndex, bIndex := 0, 0
164+
165+
for _, action := range diff {
166+
var original, updated, change string
167+
168+
switch e := action.(type) {
169+
case Keep:
170+
original = aLines[aIndex]
171+
updated = bLines[bIndex]
172+
change = " "
173+
aIndex++
174+
bIndex++
175+
case Insert:
176+
original = ""
177+
updated = e.line
178+
change = "\033[32m+\033[0m" // Green plus
179+
bIndex++
180+
case Remove:
181+
original = e.line
182+
updated = ""
183+
change = "\033[31m-\033[0m" // Red minus
184+
aIndex++
185+
}
186+
187+
fmt.Printf("%-15s %-15s %-3s\n", original, updated, change)
188+
}
189+
} */

main_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestMyersDiff(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
aLines []string
12+
bLines []string
13+
expected []EditAction
14+
}{
15+
{
16+
name: "No changes",
17+
aLines: []string{"line1", "line2", "line3"},
18+
bLines: []string{"line1", "line2", "line3"},
19+
expected: []EditAction{Keep{"line1"}, Keep{"line2"}, Keep{"line3"}},
20+
},
21+
{
22+
name: "Insertion",
23+
aLines: []string{"line1", "line2"},
24+
bLines: []string{"line1", "line2", "line3"},
25+
expected: []EditAction{Keep{"line1"}, Keep{"line2"}, Insert{"line3"}},
26+
},
27+
{
28+
name: "Removal",
29+
aLines: []string{"line1", "line2", "line3"},
30+
bLines: []string{"line1", "line2"},
31+
expected: []EditAction{Keep{"line1"}, Keep{"line2"}, Remove{"line3"}},
32+
},
33+
{
34+
name: "Replacement",
35+
aLines: []string{"line1", "line2", "line3"},
36+
bLines: []string{"line1", "newLine2", "line3"},
37+
expected: []EditAction{Keep{"line1"}, Remove{"line2"}, Insert{"newLine2"}, Keep{"line3"}},
38+
},
39+
}
40+
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
result := myersDiff(tt.aLines, tt.bLines)
44+
if !reflect.DeepEqual(result, tt.expected) {
45+
t.Errorf("myersDiff() = %v, want %v", result, tt.expected)
46+
}
47+
})
48+
}
49+
}

0 commit comments

Comments
 (0)