feat(parser/diff): patience diff from peter-evans/patience
This commit is contained in:
parent
d56fc12e3a
commit
ac75b19801
1
go.mod
1
go.mod
|
@ -12,6 +12,7 @@ require (
|
||||||
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7
|
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7
|
||||||
github.com/mcuadros/go-defaults v1.2.0
|
github.com/mcuadros/go-defaults v1.2.0
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
|
github.com/peter-evans/patience v0.3.0
|
||||||
google.golang.org/grpc v1.71.0
|
google.golang.org/grpc v1.71.0
|
||||||
google.golang.org/protobuf v1.36.5
|
google.golang.org/protobuf v1.36.5
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -76,6 +76,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
|
github.com/peter-evans/patience v0.3.0 h1:rX0JdJeepqdQl1Sk9c9uvorjYYzL2TfgLX1adqYm9cA=
|
||||||
|
github.com/peter-evans/patience v0.3.0/go.mod h1:Kmxu5sY1NmBLFSStvXjX1wS9mIv7wMcP/ubucyMOAu0=
|
||||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
package diff
|
package diff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/peter-evans/patience"
|
||||||
|
)
|
||||||
|
|
||||||
// compareStrings compares two strings character by character, optionally ignoring whitespace.
|
// compareStrings compares two strings character by character, optionally ignoring whitespace.
|
||||||
func compareStrings(str1, str2 string, compareSpace bool) bool {
|
func compareStrings(str1, str2 string, compareSpace bool) bool {
|
||||||
if compareSpace {
|
if compareSpace {
|
||||||
|
@ -44,3 +51,26 @@ func isWhitespace(b byte) bool {
|
||||||
b == 0x85 ||
|
b == 0x85 ||
|
||||||
b == 0xA0
|
b == 0xA0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// typeSymbol returns the associated symbol of a DiffType.
|
||||||
|
func typeSymbol(t patience.DiffType) string {
|
||||||
|
switch t {
|
||||||
|
case patience.Equal:
|
||||||
|
return " "
|
||||||
|
case patience.Insert:
|
||||||
|
return "+ "
|
||||||
|
case patience.Delete:
|
||||||
|
return "- "
|
||||||
|
default:
|
||||||
|
panic("unknown DiffType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiffText returns the source and destination texts (all equalities, insertions and deletions).
|
||||||
|
func DiffText(diffs []patience.DiffLine) string {
|
||||||
|
s := make([]string, len(diffs))
|
||||||
|
for i, l := range diffs {
|
||||||
|
s[i] = fmt.Sprintf("%s%s", typeSymbol(l.Type), l.Text)
|
||||||
|
}
|
||||||
|
return strings.Join(s, "\n")
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/joint-online-judge/JOJ3/internal/stage"
|
"github.com/joint-online-judge/JOJ3/internal/stage"
|
||||||
|
"github.com/peter-evans/patience"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
|
func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
|
||||||
|
@ -80,9 +81,10 @@ func (*Diff) Run(results []stage.ExecutorResult, confAny any) (
|
||||||
resultStr = resultStr[:output.MaxDiffLength]
|
resultStr = resultStr[:output.MaxDiffLength]
|
||||||
truncated = true
|
truncated = true
|
||||||
}
|
}
|
||||||
diffOutput := patienceDiff(
|
answerLines := strings.Split(answerStr, "\n")
|
||||||
answerStr, resultStr, output.CompareSpace,
|
resultLines := strings.Split(resultStr, "\n")
|
||||||
)
|
diffs := patience.Diff(answerLines, resultLines)
|
||||||
|
diffOutput := DiffText(diffs)
|
||||||
diffOutput = strings.TrimSuffix(diffOutput, "\n ")
|
diffOutput = strings.TrimSuffix(diffOutput, "\n ")
|
||||||
if truncated {
|
if truncated {
|
||||||
diffOutput += "\n\n(truncated)"
|
diffOutput += "\n\n(truncated)"
|
||||||
|
|
|
@ -1,245 +0,0 @@
|
||||||
// Copyright 2022 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package diff
|
|
||||||
|
|
||||||
// modified from https://github.com/rogpeppe/go-internal/blob/master/diff/diff.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A pair is a pair of values tracked for both the x and y side of a diff.
|
|
||||||
// It is typically a pair of line indexes.
|
|
||||||
type pair struct{ x, y int }
|
|
||||||
|
|
||||||
// Diff returns an anchored diff of the two texts old and new
|
|
||||||
// in the “unified diff” format. If old and new are identical,
|
|
||||||
// Diff returns a nil slice (no output).
|
|
||||||
//
|
|
||||||
// Unix diff implementations typically look for a diff with
|
|
||||||
// the smallest number of lines inserted and removed,
|
|
||||||
// which can in the worst case take time quadratic in the
|
|
||||||
// number of lines in the texts. As a result, many implementations
|
|
||||||
// either can be made to run for a long time or cut off the search
|
|
||||||
// after a predetermined amount of work.
|
|
||||||
//
|
|
||||||
// In contrast, this implementation looks for a diff with the
|
|
||||||
// smallest number of “unique” lines inserted and removed,
|
|
||||||
// where unique means a line that appears just once in both old and new.
|
|
||||||
// We call this an “anchored diff” because the unique lines anchor
|
|
||||||
// the chosen matching regions. An anchored diff is usually clearer
|
|
||||||
// than a standard diff, because the algorithm does not try to
|
|
||||||
// reuse unrelated blank lines or closing braces.
|
|
||||||
// The algorithm also guarantees to run in O(n log n) time
|
|
||||||
// instead of the standard O(n²) time.
|
|
||||||
//
|
|
||||||
// Some systems call this approach a “patience diff,” named for
|
|
||||||
// the “patience sorting” algorithm, itself named for a solitaire card game.
|
|
||||||
// We avoid that name for two reasons. First, the name has been used
|
|
||||||
// for a few different variants of the algorithm, so it is imprecise.
|
|
||||||
// Second, the name is frequently interpreted as meaning that you have
|
|
||||||
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
|
|
||||||
// when in fact the algorithm is faster than the standard one.
|
|
||||||
func patienceDiff(old, new string, compareSpace bool) string {
|
|
||||||
if len(old) != 0 && old[len(old)-1] != '\n' {
|
|
||||||
old += "\n"
|
|
||||||
}
|
|
||||||
if len(new) != 0 && new[len(new)-1] != '\n' {
|
|
||||||
new += "\n"
|
|
||||||
}
|
|
||||||
x := strings.SplitAfter(old, "\n")
|
|
||||||
y := strings.SplitAfter(new, "\n")
|
|
||||||
|
|
||||||
// Print diff header.
|
|
||||||
var out bytes.Buffer
|
|
||||||
|
|
||||||
// Loop over matches to consider,
|
|
||||||
// expanding each match to include surrounding lines,
|
|
||||||
// and then printing diff chunks.
|
|
||||||
// To avoid setup/teardown cases outside the loop,
|
|
||||||
// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
|
|
||||||
// in the sequence of matches.
|
|
||||||
var (
|
|
||||||
done pair // printed up to x[:done.x] and y[:done.y]
|
|
||||||
chunk pair // start lines of current chunk
|
|
||||||
count pair // number of lines from each side in current chunk
|
|
||||||
ctext []string // lines for current chunk
|
|
||||||
)
|
|
||||||
for _, m := range tgs(x, y) {
|
|
||||||
if m.x < done.x {
|
|
||||||
// Already handled scanning forward from earlier match.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand matching lines as far possible,
|
|
||||||
// establishing that x[start.x:end.x] == y[start.y:end.y].
|
|
||||||
// Note that on the first (or last) iteration we may (or definitely do)
|
|
||||||
// have an empty match: start.x==end.x and start.y==end.y.
|
|
||||||
start := m
|
|
||||||
for start.x > done.x && start.y > done.y && compareStrings(x[start.x-1], y[start.y-1], compareSpace) {
|
|
||||||
start.x--
|
|
||||||
start.y--
|
|
||||||
}
|
|
||||||
end := m
|
|
||||||
for end.x < len(x) && end.y < len(y) && compareStrings(x[end.x], y[end.y], compareSpace) {
|
|
||||||
end.x++
|
|
||||||
end.y++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit the mismatched lines before start into this chunk.
|
|
||||||
// (No effect on first sentinel iteration, when start = {0,0}.)
|
|
||||||
for _, s := range x[done.x:start.x] {
|
|
||||||
ctext = append(ctext, "- "+s)
|
|
||||||
count.x++
|
|
||||||
}
|
|
||||||
for _, s := range y[done.y:start.y] {
|
|
||||||
ctext = append(ctext, "+ "+s)
|
|
||||||
count.y++
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not at EOF and have too few common lines,
|
|
||||||
// the chunk includes all the common lines and continues.
|
|
||||||
const C = 3 // number of context lines
|
|
||||||
if (end.x < len(x) || end.y < len(y)) &&
|
|
||||||
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
|
|
||||||
for _, s := range x[start.x:end.x] {
|
|
||||||
ctext = append(ctext, " "+s)
|
|
||||||
count.x++
|
|
||||||
count.y++
|
|
||||||
}
|
|
||||||
done = end
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// End chunk with common lines for context.
|
|
||||||
if len(ctext) > 0 {
|
|
||||||
n := min(end.x-start.x, C)
|
|
||||||
for _, s := range x[start.x : start.x+n] {
|
|
||||||
ctext = append(ctext, " "+s)
|
|
||||||
count.x++
|
|
||||||
count.y++
|
|
||||||
}
|
|
||||||
done = pair{start.x + n, start.y + n}
|
|
||||||
|
|
||||||
// Format and emit chunk.
|
|
||||||
// Convert line numbers to 1-indexed.
|
|
||||||
// Special case: empty file shows up as 0,0 not 1,0.
|
|
||||||
if count.x > 0 {
|
|
||||||
chunk.x++
|
|
||||||
}
|
|
||||||
if count.y > 0 {
|
|
||||||
chunk.y++
|
|
||||||
}
|
|
||||||
// We do not need this line
|
|
||||||
// fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
|
|
||||||
for _, s := range ctext {
|
|
||||||
out.WriteString(s)
|
|
||||||
}
|
|
||||||
count.x = 0
|
|
||||||
count.y = 0
|
|
||||||
ctext = ctext[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we reached EOF, we're done.
|
|
||||||
if end.x >= len(x) && end.y >= len(y) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise start a new chunk.
|
|
||||||
chunk = pair{end.x - C, end.y - C}
|
|
||||||
for _, s := range x[chunk.x:end.x] {
|
|
||||||
ctext = append(ctext, " "+s)
|
|
||||||
count.x++
|
|
||||||
count.y++
|
|
||||||
}
|
|
||||||
done = end
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// tgs returns the pairs of indexes of the longest common subsequence
|
|
||||||
// of unique lines in x and y, where a unique line is one that appears
|
|
||||||
// once in x and once in y.
|
|
||||||
//
|
|
||||||
// The longest common subsequence algorithm is as described in
|
|
||||||
// Thomas G. Szymanski, “A Special Case of the Maximal Common
|
|
||||||
// Subsequence Problem,” Princeton TR #170 (January 1975),
|
|
||||||
// available at https://research.swtch.com/tgs170.pdf.
|
|
||||||
func tgs(x, y []string) []pair {
|
|
||||||
// Count the number of times each string appears in a and b.
|
|
||||||
// We only care about 0, 1, many, counted as 0, -1, -2
|
|
||||||
// for the x side and 0, -4, -8 for the y side.
|
|
||||||
// Using negative numbers now lets us distinguish positive line numbers later.
|
|
||||||
m := make(map[string]int)
|
|
||||||
for _, s := range x {
|
|
||||||
if c := m[s]; c > -2 {
|
|
||||||
m[s] = c - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, s := range y {
|
|
||||||
if c := m[s]; c > -8 {
|
|
||||||
m[s] = c - 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now unique strings can be identified by m[s] = -1+-4.
|
|
||||||
//
|
|
||||||
// Gather the indexes of those strings in x and y, building:
|
|
||||||
// xi[i] = increasing indexes of unique strings in x.
|
|
||||||
// yi[i] = increasing indexes of unique strings in y.
|
|
||||||
// inv[i] = index j such that x[xi[i]] = y[yi[j]].
|
|
||||||
var xi, yi, inv []int
|
|
||||||
for i, s := range y {
|
|
||||||
if m[s] == -1+-4 {
|
|
||||||
m[s] = len(yi)
|
|
||||||
yi = append(yi, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, s := range x {
|
|
||||||
if j, ok := m[s]; ok && j >= 0 {
|
|
||||||
xi = append(xi, i)
|
|
||||||
inv = append(inv, j)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply Algorithm A from Szymanski's paper.
|
|
||||||
// In those terms, A = J = inv and B = [0, n).
|
|
||||||
// We add sentinel pairs {0,0}, and {len(x),len(y)}
|
|
||||||
// to the returned sequence, to help the processing loop.
|
|
||||||
J := inv
|
|
||||||
n := len(xi)
|
|
||||||
T := make([]int, n)
|
|
||||||
L := make([]int, n)
|
|
||||||
for i := range T {
|
|
||||||
T[i] = n + 1
|
|
||||||
}
|
|
||||||
for i := range n {
|
|
||||||
k := sort.Search(n, func(k int) bool {
|
|
||||||
return T[k] >= J[i]
|
|
||||||
})
|
|
||||||
T[k] = J[i]
|
|
||||||
L[i] = k + 1
|
|
||||||
}
|
|
||||||
k := 0
|
|
||||||
for _, v := range L {
|
|
||||||
if k < v {
|
|
||||||
k = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seq := make([]pair, 2+k)
|
|
||||||
seq[1+k] = pair{len(x), len(y)} // sentinel at end
|
|
||||||
lastj := n
|
|
||||||
for i := n - 1; i >= 0; i-- {
|
|
||||||
if L[i] == k && J[i] < lastj {
|
|
||||||
seq[k] = pair{xi[i], yi[J[i]]}
|
|
||||||
k--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seq[0] = pair{0, 0} // sentinel at start
|
|
||||||
return seq
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user