117 lines
3.4 KiB
Go
117 lines
3.4 KiB
Go
package entry
|
||
|
||
import "strings"
|
||
|
||
// DiffOp sagt, was mit einer Zeile im Vergleich zweier Fassungen passiert ist.
|
||
type DiffOp int
|
||
|
||
const (
|
||
DiffEqual DiffOp = iota
|
||
DiffRemove
|
||
DiffAdd
|
||
)
|
||
|
||
// DiffLine ist eine Zeile im Ergebnis von DiffLines.
|
||
type DiffLine struct {
|
||
Op DiffOp
|
||
Text string
|
||
}
|
||
|
||
// maxDiffCells begrenzt die Größe der Vergleichstabelle (Zeilen_alt ×
|
||
// Zeilen_neu). Jenseits davon bräuchte eine genaue Zeilenzuordnung mehr
|
||
// Speicher, als für eine einzelne Vergleichsseite sinnvoll ist — dann liefert
|
||
// DiffLines nur noch "alles Alte weg, alles Neue dazu" statt einer
|
||
// Zeile-für-Zeile-Zuordnung. Bei zwei mittelgroßen Wiki-Einträgen (je ein paar
|
||
// hundert Zeilen) wird diese Grenze nicht erreicht.
|
||
const maxDiffCells = 4_000_000
|
||
|
||
// DiffLines vergleicht zwei Fassungen eines Eintrags zeilenweise.
|
||
//
|
||
// Klassischer Ansatz über die längste gemeinsame Teilfolge (LCS): Zeilen, die
|
||
// in beiden Fassungen in derselben Reihenfolge vorkommen, gelten als
|
||
// unverändert; alles dazwischen als entfernt bzw. hinzugefügt. Ein
|
||
// verschobener, aber unveränderter Absatz wird dabei nicht zwingend als
|
||
// "gleich" erkannt, wenn sich in seiner Umgebung etwas geändert hat — für die
|
||
// Frage "was hat sich bei diesem Schreibvorgang geändert" reicht das.
|
||
func DiffLines(old, newer string) []DiffLine {
|
||
oldLines := splitLines(old)
|
||
newLines := splitLines(newer)
|
||
|
||
if len(oldLines)*len(newLines) > maxDiffCells {
|
||
return coarseDiff(oldLines, newLines)
|
||
}
|
||
return lcsDiff(oldLines, newLines)
|
||
}
|
||
|
||
func splitLines(s string) []string {
|
||
if s == "" {
|
||
return nil
|
||
}
|
||
return strings.Split(s, "\n")
|
||
}
|
||
|
||
// coarseDiff dient als Rückfallebene für außergewöhnlich lange Einträge: keine
|
||
// Zeilenzuordnung, nur der komplette alte Text als entfernt und der
|
||
// komplette neue als hinzugefügt.
|
||
func coarseDiff(oldLines, newLines []string) []DiffLine {
|
||
lines := make([]DiffLine, 0, len(oldLines)+len(newLines))
|
||
for _, z := range oldLines {
|
||
lines = append(lines, DiffLine{Op: DiffRemove, Text: z})
|
||
}
|
||
for _, z := range newLines {
|
||
lines = append(lines, DiffLine{Op: DiffAdd, Text: z})
|
||
}
|
||
return lines
|
||
}
|
||
|
||
// lcsDiff berechnet die Zeilenzuordnung über eine klassische LCS-Tabelle.
|
||
//
|
||
// dp[i][j] ist die Länge der längsten gemeinsamen Teilfolge von a[i:] und
|
||
// b[j:], rückwärts aufgebaut. Der anschließende Vorwärtslauf liest daraus die
|
||
// eigentliche Zuordnung ab: Stimmen die Zeilen überein, sind sie Teil der
|
||
// gemeinsamen Folge; sonst wird der Seite gefolgt, die die längere
|
||
// Teilfolge verspricht.
|
||
func lcsDiff(a, b []string) []DiffLine {
|
||
n, m := len(a), len(b)
|
||
|
||
dp := make([][]int, n+1)
|
||
for i := range dp {
|
||
dp[i] = make([]int, m+1)
|
||
}
|
||
for i := n - 1; i >= 0; i-- {
|
||
for j := m - 1; j >= 0; j-- {
|
||
switch {
|
||
case a[i] == b[j]:
|
||
dp[i][j] = dp[i+1][j+1] + 1
|
||
case dp[i+1][j] >= dp[i][j+1]:
|
||
dp[i][j] = dp[i+1][j]
|
||
default:
|
||
dp[i][j] = dp[i][j+1]
|
||
}
|
||
}
|
||
}
|
||
|
||
var lines []DiffLine
|
||
i, j := 0, 0
|
||
for i < n && j < m {
|
||
switch {
|
||
case a[i] == b[j]:
|
||
lines = append(lines, DiffLine{Op: DiffEqual, Text: a[i]})
|
||
i++
|
||
j++
|
||
case dp[i+1][j] >= dp[i][j+1]:
|
||
lines = append(lines, DiffLine{Op: DiffRemove, Text: a[i]})
|
||
i++
|
||
default:
|
||
lines = append(lines, DiffLine{Op: DiffAdd, Text: b[j]})
|
||
j++
|
||
}
|
||
}
|
||
for ; i < n; i++ {
|
||
lines = append(lines, DiffLine{Op: DiffRemove, Text: a[i]})
|
||
}
|
||
for ; j < m; j++ {
|
||
lines = append(lines, DiffLine{Op: DiffAdd, Text: b[j]})
|
||
}
|
||
return lines
|
||
}
|