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

//
// 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)
}
}