-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathDiff.swift
196 lines (172 loc) · 5.76 KB
/
Diff.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//
// Diff.swift
// TemplateKit
//
// Created by Matias Cudich on 10/30/16.
// Copyright © 2016 Matias Cudich. All rights reserved.
//
import Foundation
public enum DiffStep: Equatable {
case insert(Int)
case delete(Int)
case move(Int, Int)
case update(Int)
}
public func ==(lhs: DiffStep, rhs: DiffStep) -> Bool {
switch (lhs, rhs) {
case let (.insert(lhsIndex), .insert(rhsIndex)):
return lhsIndex == rhsIndex
case let (.delete(lhsIndex), .delete(rhsIndex)):
return lhsIndex == rhsIndex
case let (.move(lhsFromIndex, lhsToIndex), .move(rhsFromIndex, rhsToIndex)):
return lhsFromIndex == rhsFromIndex && lhsToIndex == rhsToIndex
case let (.update(lhsIndex), .update(rhsIndex)):
return lhsIndex == rhsIndex
default:
return false
}
}
enum Counter {
case zero
case one
case many
mutating func increment() {
switch self {
case .zero:
self = .one
case .one:
self = .many
case .many:
break
}
}
}
class SymbolEntry {
var oc: Counter = .zero
var nc: Counter = .zero
var olno = [Int]()
var occursInBoth: Bool {
return oc != .zero && nc != .zero
}
}
enum Entry {
case symbol(SymbolEntry)
case index(Int)
}
// Based on http://dl.acm.org/citation.cfm?id=359467.
//
// And other implementations at:
// * https://github.com/Instagram/IGListKit
// * https://github.com/andre-alves/PHDiff
//
public func diff<T: Hashable>(_ old: [T], _ new: [T]) -> [DiffStep] {
var table = [Int: SymbolEntry]()
var oa = [Entry]()
var na = [Entry]()
// Pass 1 comprises the following: (a) each line i of file N is read in sequence; (b) a symbol
// table entry for each line i is created if it does not already exist; (c) NC for the line's
// symbol table entry is incremented; and (d) NA [i] is set to point to the symbol table entry of
// line i.
for item in new {
let entry = table[item.hashValue] ?? SymbolEntry()
table[item.hashValue] = entry
entry.nc.increment()
na.append(.symbol(entry))
}
// Pass 2 is identical to pass 1 except that it acts on file O, array OA, and counter OC,
// and OLNO for the symbol table entry is set to the line's number.
for (index, item) in old.enumerated() {
let entry = table[item.hashValue] ?? SymbolEntry()
table[item.hashValue] = entry
entry.oc.increment()
entry.olno.append(index)
oa.append(.symbol(entry))
}
// In pass 3 we use observation 1 and process only those lines having NC = OC = 1. Since each
// represents (we assume) the same unmodified line, for each we replace the symbol table pointers
// in NA and OA by the number of the line in the other file. For example, if NA[i] corresponds to
// such a line, we look NA[i] up in the symbol table and set NA[i] to OLNO and OA[OLNO] to i.
// In pass 3 we also "find" unique virtual lines immediately before the first and immediately
// after the last lines of the files.
for (index, item) in na.enumerated() {
if case .symbol(let entry) = item, entry.occursInBoth {
guard entry.olno.count > 0 else { continue }
let oldIndex = entry.olno.removeFirst()
na[index] = .index(oldIndex)
oa[oldIndex] = .index(index)
}
}
// In pass 4, we apply observation 2 and process each line in NA in ascending order: If NA[i]
// points to OA[j] and NA[i + 1] and OA[j + 1] contain identical symbol table entry pointers, then
// OA[j + 1] is set to line i + 1 and NA[i + 1] is set to line j + 1.
var i = 1
while (i < na.count - 1) {
if case .index(let j) = na[i], j + 1 < oa.count {
if case .symbol(let newEntry) = na[i + 1], case .symbol(let oldEntry) = oa[j + 1], newEntry === oldEntry {
na[i + 1] = .index(j + 1)
oa[j + 1] = .index(i + 1)
}
}
i += 1
}
// In pass 5, we also apply observation 2 and process each entry in descending order: if NA[i]
// points to OA[j] and NA[i - 1] and OA[j - 1] contain identical symbol table pointers, then
// NA[i - 1] is replaced by j - 1 and OA[j - 1] is replaced by i - 1.
i = na.count - 1
while (i > 0) {
if case .index(let j) = na[i], j - 1 >= 0 {
if case .symbol(let newEntry) = na[i - 1], case .symbol(let oldEntry) = oa[j - 1], newEntry === oldEntry {
na[i - 1] = .index(j - 1)
oa[j - 1] = .index(i - 1)
}
}
i -= 1
}
var steps = [DiffStep]()
var deleteOffsets = Array(repeating: 0, count: old.count)
var runningOffset = 0
for (index, item) in oa.enumerated() {
deleteOffsets[index] = runningOffset
if case .symbol(_) = item {
steps.append(.delete(index))
runningOffset += 1
}
}
runningOffset = 0
for (index, item) in na.enumerated() {
switch item {
case .symbol(_):
steps.append(.insert(index))
runningOffset += 1
case .index(let oldIndex):
// The object has changed, so it should be updated.
if old[oldIndex] != new[index] {
steps.append(.update(index))
}
let deleteOffset = deleteOffsets[oldIndex]
// The object is not at the expected position, so move it.
if (oldIndex - deleteOffset + runningOffset) != index {
steps.append(.move(oldIndex, index))
}
}
}
// Sort the steps to allow for a single-pass array update.
var insertions = [DiffStep]()
var updates = [DiffStep]()
var indexedDeletions: [[DiffStep]] = Array(repeating: [], count: old.count)
for step in steps {
switch step {
case .insert:
insertions.append(step)
case let .delete(fromIndex):
indexedDeletions[fromIndex].append(step)
case let .move(fromIndex, toIndex):
insertions.append(.insert(toIndex))
indexedDeletions[fromIndex].append(.delete(fromIndex))
case .update:
updates.append(step)
}
}
let deletions = indexedDeletions.flatMap { $0.first }.reversed()
return deletions + insertions + updates
}