// // 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 : 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: 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(_ lhs: [Value], _ rhs: [Value]) -> [DiffStep] { 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(diff: [DiffStep], 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(lhs: SectionedValues, rhs: SectionedValues) -> [SectionedDiffStep] { if lhs.sections == rhs.sections { let allResults: [[SectionedDiffStep]] = (0..] = 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] = [] var sectionDeletions: [SectionedDiffStep] = [] 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..] = 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, y: SectionedValues`, /// `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(diff: [SectionedDiffStep], toSectionedValues lhs: SectionedValues) -> SectionedValues { 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( _ table: [[Int]], _ x: [Value], _ y: [Value], _ i: Int, _ j: Int, _ currentResults: ([DiffStep], [DiffStep]) ) -> Result<([DiffStep], [DiffStep])> { 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{ case done(T) case call(() -> Result) } fileprivate struct MemoizedSequenceComparison { 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] { return Dwifft.diff(self, other) } /// Deprecated in favor of `Dwifft.apply`. @available(*, deprecated) public func apply(_ diff: [DiffStep]) -> [Element] { return Dwifft.apply(diff: diff, toArray: self) } }