You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
13 KiB
321 lines
13 KiB
//
|
|
// Dwifft.swift
|
|
// Dwifft
|
|
//
|
|
// Created by Jack Flintermann on 3/14/15.
|
|
// Copyright (c) 2015 jflinter. All rights reserved.
|
|
//
|
|
|
|
/// These get returned from calls to Dwifft.diff(). They represent insertions or deletions
|
|
/// that need to happen to transform one array into another.
|
|
public enum DiffStep<Value> : CustomDebugStringConvertible {
|
|
/// An insertion.
|
|
case insert(Int, Value)
|
|
/// A deletion.
|
|
case delete(Int, Value)
|
|
|
|
public var debugDescription: String {
|
|
switch(self) {
|
|
case let .insert(i, j):
|
|
return "+\(j)@\(i)"
|
|
case let .delete(i, j):
|
|
return "-\(j)@\(i)"
|
|
}
|
|
}
|
|
|
|
/// The index to be inserted or deleted.
|
|
public var idx: Int {
|
|
switch(self) {
|
|
case let .insert(i, _):
|
|
return i
|
|
case let .delete(i, _):
|
|
return i
|
|
}
|
|
}
|
|
|
|
/// The value to be inserted or deleted.
|
|
public var value: Value {
|
|
switch(self) {
|
|
case let .insert(j):
|
|
return j.1
|
|
case let .delete(j):
|
|
return j.1
|
|
}
|
|
}
|
|
}
|
|
|
|
/// These get returned from calls to Dwifft.diff(). They represent insertions or deletions
|
|
/// that need to happen to transform one `SectionedValues` into another.
|
|
public enum SectionedDiffStep<Section, Value>: CustomDebugStringConvertible {
|
|
/// An insertion, at a given section and row.
|
|
case insert(Int, Int, Value)
|
|
/// An deletion, at a given section and row.
|
|
case delete(Int, Int, Value)
|
|
/// A section insertion, at a given section.
|
|
case sectionInsert(Int, Section)
|
|
/// A section deletion, at a given section.
|
|
case sectionDelete(Int, Section)
|
|
|
|
internal var section: Int {
|
|
switch self {
|
|
case let .insert(s, _, _): return s
|
|
case let .delete(s, _, _): return s
|
|
case let .sectionInsert(s, _): return s
|
|
case let .sectionDelete(s, _): return s
|
|
}
|
|
}
|
|
|
|
public var debugDescription: String {
|
|
switch self {
|
|
case let .sectionDelete(s, _): return "ds(\(s))"
|
|
case let .sectionInsert(s, _): return "is(\(s))"
|
|
case let .delete(section, row, _): return "d(\(section) \(row))"
|
|
case let .insert(section, row, _): return "i(\(section) \(row))"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Namespace for the `diff` and `apply` functions.
|
|
public enum Dwifft {
|
|
|
|
/// Returns the sequence of `DiffStep`s required to transform one array into another.
|
|
///
|
|
/// - Parameters:
|
|
/// - lhs: an array
|
|
/// - rhs: another, uh, array
|
|
/// - Returns: the series of transformations that, when applied to `lhs`, will yield `rhs`.
|
|
public static func diff<Value: Equatable>(_ lhs: [Value], _ rhs: [Value]) -> [DiffStep<Value>] {
|
|
if lhs.isEmpty {
|
|
return rhs.enumerated().map(DiffStep.insert)
|
|
} else if rhs.isEmpty {
|
|
return lhs.enumerated().map(DiffStep.delete).reversed()
|
|
}
|
|
|
|
let table = MemoizedSequenceComparison.buildTable(lhs, rhs, lhs.count, rhs.count)
|
|
var result = diffInternal(table, lhs, rhs, lhs.count, rhs.count, ([], []))
|
|
while case let .call(f) = result {
|
|
result = f()
|
|
}
|
|
guard case let .done(accum) = result else { fatalError("unreachable code") }
|
|
return accum.1 + accum.0
|
|
}
|
|
|
|
/// Applies a diff to an array. The following should always be true:
|
|
/// Given `x: [T], y: [T]`, `Dwifft.apply(Dwifft.diff(x, y), toArray: x) == y`
|
|
///
|
|
/// - Parameters:
|
|
/// - diff: a diff, as computed by calling `Dwifft.diff`. Note that you *must* be careful to
|
|
/// not modify said diff before applying it, and to only apply it to the left hand side of a
|
|
/// previous call to `Dwifft.diff`. If not, this can (and probably will) trigger an array out of bounds exception.
|
|
/// - lhs: an array.
|
|
/// - Returns: `lhs`, transformed by `diff`.
|
|
public static func apply<Value>(diff: [DiffStep<Value>], toArray lhs: [Value]) -> [Value] {
|
|
var copy = lhs
|
|
for result in diff {
|
|
switch result {
|
|
case let .delete(idx, _):
|
|
copy.remove(at: idx)
|
|
case let .insert(idx, val):
|
|
copy.insert(val, at: idx)
|
|
}
|
|
}
|
|
return copy
|
|
}
|
|
|
|
/// Returns the sequence of `SectionedDiffStep`s required to transform one `SectionedValues` into another.
|
|
///
|
|
/// - Parameters:
|
|
/// - lhs: a `SectionedValues`
|
|
/// - rhs: another, uh, `SectionedValues`
|
|
/// - Returns: the series of transformations that, when applied to `lhs`, will yield `rhs`.
|
|
public static func diff<Section, Value>(lhs: SectionedValues<Section, Value>, rhs: SectionedValues<Section, Value>) -> [SectionedDiffStep<Section, Value>] {
|
|
if lhs.sections == rhs.sections {
|
|
let allResults: [[SectionedDiffStep<Section, Value>]] = (0..<lhs.sections.count).map { i in
|
|
let lValues = lhs.sectionsAndValues[i].1
|
|
let rValues = rhs.sectionsAndValues[i].1
|
|
let rowDiff = Dwifft.diff(lValues, rValues)
|
|
let results: [SectionedDiffStep<Section, Value>] = rowDiff.map { result in
|
|
switch result {
|
|
case let .insert(j, t): return SectionedDiffStep.insert(i, j, t)
|
|
case let .delete(j, t): return SectionedDiffStep.delete(i, j, t)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
let flattened = allResults.flatMap { $0 }
|
|
let insertions = flattened.filter { result in
|
|
if case .insert = result { return true }
|
|
return false
|
|
}
|
|
let deletions = flattened.filter { result in
|
|
if case .delete = result { return true }
|
|
return false
|
|
}
|
|
return deletions + insertions
|
|
|
|
} else {
|
|
var middleSectionsAndValues = lhs.sectionsAndValues
|
|
let sectionDiff = Dwifft.diff(lhs.sections, rhs.sections)
|
|
var sectionInsertions: [SectionedDiffStep<Section, Value>] = []
|
|
var sectionDeletions: [SectionedDiffStep<Section, Value>] = []
|
|
for result in sectionDiff {
|
|
switch result {
|
|
case let .insert(i, s):
|
|
sectionInsertions.append(SectionedDiffStep.sectionInsert(i, s))
|
|
middleSectionsAndValues.insert((s, []), at: i)
|
|
case let .delete(i, s):
|
|
sectionDeletions.append(SectionedDiffStep.sectionDelete(i, s))
|
|
middleSectionsAndValues.remove(at: i)
|
|
}
|
|
}
|
|
|
|
let middle = SectionedValues(middleSectionsAndValues)
|
|
let rowResults = Dwifft.diff(lhs: middle, rhs: rhs)
|
|
|
|
// we need to calculate a mapping from the final section indices to the original
|
|
// section indices. This lets us perform the deletions before the section deletions,
|
|
// which makes UITableView + UICollectionView happy. See https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/TableView_iPhone/ManageInsertDeleteRow/ManageInsertDeleteRow.html#//apple_ref/doc/uid/TP40007451-CH10-SW9
|
|
var indexMapping = Array(0..<lhs.sections.count)
|
|
for deletion in sectionDeletions {
|
|
indexMapping.remove(at: deletion.section)
|
|
}
|
|
for insertion in sectionInsertions {
|
|
indexMapping.insert(-1, at: insertion.section)
|
|
}
|
|
var mapping = [Int: Int]()
|
|
for (i, j) in indexMapping.enumerated() {
|
|
mapping[i] = j
|
|
}
|
|
|
|
let deletions = rowResults.filter { result in
|
|
if case .delete = result {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
let insertions = rowResults.filter { result in
|
|
if case .insert = result {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
let mappedDeletions: [SectionedDiffStep<Section, Value>] = deletions.map { deletion in
|
|
guard case let .delete(section, row, val) = deletion else { fatalError("not possible") }
|
|
guard let newIndex = mapping[section], newIndex != -1 else { fatalError("not possible") }
|
|
return .delete(newIndex, row, val)
|
|
}
|
|
|
|
return mappedDeletions + sectionDeletions + sectionInsertions + insertions
|
|
|
|
}
|
|
}
|
|
|
|
/// Applies a diff to a `SectionedValues`. The following should always be true:
|
|
/// Given `x: SectionedValues<S,T>, y: SectionedValues<S,T>`,
|
|
/// `Dwifft.apply(Dwifft.diff(lhs: x, rhs: y), toSectionedValues: x) == y`
|
|
///
|
|
/// - Parameters:
|
|
/// - diff: a diff, as computed by calling `Dwifft.diff`. Note that you *must* be careful to
|
|
/// not modify said diff before applying it, and to only apply it to the left hand side of a
|
|
/// previous call to `Dwifft.diff`. If not, this can (and probably will) trigger an array out of bounds exception.
|
|
/// - lhs: a `SectionedValues`.
|
|
/// - Returns: `lhs`, transformed by `diff`.
|
|
public static func apply<Section, Value>(diff: [SectionedDiffStep<Section, Value>], toSectionedValues lhs: SectionedValues<Section, Value>) -> SectionedValues<Section, Value> {
|
|
var sectionsAndValues = lhs.sectionsAndValues
|
|
for result in diff {
|
|
switch result {
|
|
case let .sectionInsert(sectionIndex, val):
|
|
sectionsAndValues.insert((val, []), at: sectionIndex)
|
|
case let .sectionDelete(sectionIndex, _):
|
|
sectionsAndValues.remove(at: sectionIndex)
|
|
case let .insert(sectionIndex, rowIndex, val):
|
|
sectionsAndValues[sectionIndex].1.insert(val, at: rowIndex)
|
|
case let .delete(sectionIndex, rowIndex, _):
|
|
sectionsAndValues[sectionIndex].1.remove(at: rowIndex)
|
|
}
|
|
}
|
|
return SectionedValues(sectionsAndValues)
|
|
}
|
|
|
|
private static func diffInternal<Value: Equatable>(
|
|
_ table: [[Int]],
|
|
_ x: [Value],
|
|
_ y: [Value],
|
|
_ i: Int,
|
|
_ j: Int,
|
|
_ currentResults: ([DiffStep<Value>], [DiffStep<Value>])
|
|
) -> Result<([DiffStep<Value>], [DiffStep<Value>])> {
|
|
if i == 0 && j == 0 {
|
|
return .done(currentResults)
|
|
}
|
|
else {
|
|
return .call {
|
|
var nextResults = currentResults
|
|
if i == 0 {
|
|
nextResults.0 = [DiffStep.insert(j-1, y[j-1])] + nextResults.0
|
|
return diffInternal(table, x, y, i, j-1, nextResults)
|
|
} else if j == 0 {
|
|
nextResults.1 = nextResults.1 + [DiffStep.delete(i-1, x[i-1])]
|
|
return diffInternal(table, x, y, i - 1, j, nextResults)
|
|
} else if table[i][j] == table[i][j-1] {
|
|
nextResults.0 = [DiffStep.insert(j-1, y[j-1])] + nextResults.0
|
|
return diffInternal(table, x, y, i, j-1, nextResults)
|
|
} else if table[i][j] == table[i-1][j] {
|
|
nextResults.1 = nextResults.1 + [DiffStep.delete(i-1, x[i-1])]
|
|
return diffInternal(table, x, y, i - 1, j, nextResults)
|
|
} else {
|
|
return diffInternal(table, x, y, i-1, j-1, nextResults)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate enum Result<T>{
|
|
case done(T)
|
|
case call(() -> Result<T>)
|
|
}
|
|
|
|
fileprivate struct MemoizedSequenceComparison<T: Equatable> {
|
|
static func buildTable(_ x: [T], _ y: [T], _ n: Int, _ m: Int) -> [[Int]] {
|
|
var table = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)
|
|
// using unsafe pointers lets us avoid swift array bounds-checking, which results in a considerable speed boost.
|
|
table.withUnsafeMutableBufferPointer { unsafeTable in
|
|
x.withUnsafeBufferPointer { unsafeX in
|
|
y.withUnsafeBufferPointer { unsafeY in
|
|
for i in 1...n {
|
|
for j in 1...m {
|
|
if unsafeX[i&-1] == unsafeY[j&-1] {
|
|
unsafeTable[i][j] = unsafeTable[i&-1][j&-1] + 1
|
|
} else {
|
|
unsafeTable[i][j] = max(unsafeTable[i&-1][j], unsafeTable[i][j&-1])
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
return table
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - Deprecated
|
|
public extension Array where Element: Equatable {
|
|
|
|
/// Deprecated in favor of `Dwifft.diff`.
|
|
@available(*, deprecated)
|
|
public func diff(_ other: [Element]) -> [DiffStep<Element>] {
|
|
return Dwifft.diff(self, other)
|
|
}
|
|
|
|
/// Deprecated in favor of `Dwifft.apply`.
|
|
@available(*, deprecated)
|
|
public func apply(_ diff: [DiffStep<Element>]) -> [Element] {
|
|
return Dwifft.apply(diff: diff, toArray: self)
|
|
}
|
|
|
|
}
|