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

5 years ago
  1. //
  2. // Dwifft.swift
  3. // Dwifft
  4. //
  5. // Created by Jack Flintermann on 3/14/15.
  6. // Copyright (c) 2015 jflinter. All rights reserved.
  7. //
  8. /// These get returned from calls to Dwifft.diff(). They represent insertions or deletions
  9. /// that need to happen to transform one array into another.
  10. public enum DiffStep<Value> : CustomDebugStringConvertible {
  11. /// An insertion.
  12. case insert(Int, Value)
  13. /// A deletion.
  14. case delete(Int, Value)
  15. public var debugDescription: String {
  16. switch(self) {
  17. case let .insert(i, j):
  18. return "+\(j)@\(i)"
  19. case let .delete(i, j):
  20. return "-\(j)@\(i)"
  21. }
  22. }
  23. /// The index to be inserted or deleted.
  24. public var idx: Int {
  25. switch(self) {
  26. case let .insert(i, _):
  27. return i
  28. case let .delete(i, _):
  29. return i
  30. }
  31. }
  32. /// The value to be inserted or deleted.
  33. public var value: Value {
  34. switch(self) {
  35. case let .insert(j):
  36. return j.1
  37. case let .delete(j):
  38. return j.1
  39. }
  40. }
  41. }
  42. /// These get returned from calls to Dwifft.diff(). They represent insertions or deletions
  43. /// that need to happen to transform one `SectionedValues` into another.
  44. public enum SectionedDiffStep<Section, Value>: CustomDebugStringConvertible {
  45. /// An insertion, at a given section and row.
  46. case insert(Int, Int, Value)
  47. /// An deletion, at a given section and row.
  48. case delete(Int, Int, Value)
  49. /// A section insertion, at a given section.
  50. case sectionInsert(Int, Section)
  51. /// A section deletion, at a given section.
  52. case sectionDelete(Int, Section)
  53. internal var section: Int {
  54. switch self {
  55. case let .insert(s, _, _): return s
  56. case let .delete(s, _, _): return s
  57. case let .sectionInsert(s, _): return s
  58. case let .sectionDelete(s, _): return s
  59. }
  60. }
  61. public var debugDescription: String {
  62. switch self {
  63. case let .sectionDelete(s, _): return "ds(\(s))"
  64. case let .sectionInsert(s, _): return "is(\(s))"
  65. case let .delete(section, row, _): return "d(\(section) \(row))"
  66. case let .insert(section, row, _): return "i(\(section) \(row))"
  67. }
  68. }
  69. }
  70. /// Namespace for the `diff` and `apply` functions.
  71. public enum Dwifft {
  72. /// Returns the sequence of `DiffStep`s required to transform one array into another.
  73. ///
  74. /// - Parameters:
  75. /// - lhs: an array
  76. /// - rhs: another, uh, array
  77. /// - Returns: the series of transformations that, when applied to `lhs`, will yield `rhs`.
  78. public static func diff<Value: Equatable>(_ lhs: [Value], _ rhs: [Value]) -> [DiffStep<Value>] {
  79. if lhs.isEmpty {
  80. return rhs.enumerated().map(DiffStep.insert)
  81. } else if rhs.isEmpty {
  82. return lhs.enumerated().map(DiffStep.delete).reversed()
  83. }
  84. let table = MemoizedSequenceComparison.buildTable(lhs, rhs, lhs.count, rhs.count)
  85. var result = diffInternal(table, lhs, rhs, lhs.count, rhs.count, ([], []))
  86. while case let .call(f) = result {
  87. result = f()
  88. }
  89. guard case let .done(accum) = result else { fatalError("unreachable code") }
  90. return accum.1 + accum.0
  91. }
  92. /// Applies a diff to an array. The following should always be true:
  93. /// Given `x: [T], y: [T]`, `Dwifft.apply(Dwifft.diff(x, y), toArray: x) == y`
  94. ///
  95. /// - Parameters:
  96. /// - diff: a diff, as computed by calling `Dwifft.diff`. Note that you *must* be careful to
  97. /// not modify said diff before applying it, and to only apply it to the left hand side of a
  98. /// previous call to `Dwifft.diff`. If not, this can (and probably will) trigger an array out of bounds exception.
  99. /// - lhs: an array.
  100. /// - Returns: `lhs`, transformed by `diff`.
  101. public static func apply<Value>(diff: [DiffStep<Value>], toArray lhs: [Value]) -> [Value] {
  102. var copy = lhs
  103. for result in diff {
  104. switch result {
  105. case let .delete(idx, _):
  106. copy.remove(at: idx)
  107. case let .insert(idx, val):
  108. copy.insert(val, at: idx)
  109. }
  110. }
  111. return copy
  112. }
  113. /// Returns the sequence of `SectionedDiffStep`s required to transform one `SectionedValues` into another.
  114. ///
  115. /// - Parameters:
  116. /// - lhs: a `SectionedValues`
  117. /// - rhs: another, uh, `SectionedValues`
  118. /// - Returns: the series of transformations that, when applied to `lhs`, will yield `rhs`.
  119. public static func diff<Section, Value>(lhs: SectionedValues<Section, Value>, rhs: SectionedValues<Section, Value>) -> [SectionedDiffStep<Section, Value>] {
  120. if lhs.sections == rhs.sections {
  121. let allResults: [[SectionedDiffStep<Section, Value>]] = (0..<lhs.sections.count).map { i in
  122. let lValues = lhs.sectionsAndValues[i].1
  123. let rValues = rhs.sectionsAndValues[i].1
  124. let rowDiff = Dwifft.diff(lValues, rValues)
  125. let results: [SectionedDiffStep<Section, Value>] = rowDiff.map { result in
  126. switch result {
  127. case let .insert(j, t): return SectionedDiffStep.insert(i, j, t)
  128. case let .delete(j, t): return SectionedDiffStep.delete(i, j, t)
  129. }
  130. }
  131. return results
  132. }
  133. let flattened = allResults.flatMap { $0 }
  134. let insertions = flattened.filter { result in
  135. if case .insert = result { return true }
  136. return false
  137. }
  138. let deletions = flattened.filter { result in
  139. if case .delete = result { return true }
  140. return false
  141. }
  142. return deletions + insertions
  143. } else {
  144. var middleSectionsAndValues = lhs.sectionsAndValues
  145. let sectionDiff = Dwifft.diff(lhs.sections, rhs.sections)
  146. var sectionInsertions: [SectionedDiffStep<Section, Value>] = []
  147. var sectionDeletions: [SectionedDiffStep<Section, Value>] = []
  148. for result in sectionDiff {
  149. switch result {
  150. case let .insert(i, s):
  151. sectionInsertions.append(SectionedDiffStep.sectionInsert(i, s))
  152. middleSectionsAndValues.insert((s, []), at: i)
  153. case let .delete(i, s):
  154. sectionDeletions.append(SectionedDiffStep.sectionDelete(i, s))
  155. middleSectionsAndValues.remove(at: i)
  156. }
  157. }
  158. let middle = SectionedValues(middleSectionsAndValues)
  159. let rowResults = Dwifft.diff(lhs: middle, rhs: rhs)
  160. // we need to calculate a mapping from the final section indices to the original
  161. // section indices. This lets us perform the deletions before the section deletions,
  162. // 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
  163. var indexMapping = Array(0..<lhs.sections.count)
  164. for deletion in sectionDeletions {
  165. indexMapping.remove(at: deletion.section)
  166. }
  167. for insertion in sectionInsertions {
  168. indexMapping.insert(-1, at: insertion.section)
  169. }
  170. var mapping = [Int: Int]()
  171. for (i, j) in indexMapping.enumerated() {
  172. mapping[i] = j
  173. }
  174. let deletions = rowResults.filter { result in
  175. if case .delete = result {
  176. return true
  177. }
  178. return false
  179. }
  180. let insertions = rowResults.filter { result in
  181. if case .insert = result {
  182. return true
  183. }
  184. return false
  185. }
  186. let mappedDeletions: [SectionedDiffStep<Section, Value>] = deletions.map { deletion in
  187. guard case let .delete(section, row, val) = deletion else { fatalError("not possible") }
  188. guard let newIndex = mapping[section], newIndex != -1 else { fatalError("not possible") }
  189. return .delete(newIndex, row, val)
  190. }
  191. return mappedDeletions + sectionDeletions + sectionInsertions + insertions
  192. }
  193. }
  194. /// Applies a diff to a `SectionedValues`. The following should always be true:
  195. /// Given `x: SectionedValues<S,T>, y: SectionedValues<S,T>`,
  196. /// `Dwifft.apply(Dwifft.diff(lhs: x, rhs: y), toSectionedValues: x) == y`
  197. ///
  198. /// - Parameters:
  199. /// - diff: a diff, as computed by calling `Dwifft.diff`. Note that you *must* be careful to
  200. /// not modify said diff before applying it, and to only apply it to the left hand side of a
  201. /// previous call to `Dwifft.diff`. If not, this can (and probably will) trigger an array out of bounds exception.
  202. /// - lhs: a `SectionedValues`.
  203. /// - Returns: `lhs`, transformed by `diff`.
  204. public static func apply<Section, Value>(diff: [SectionedDiffStep<Section, Value>], toSectionedValues lhs: SectionedValues<Section, Value>) -> SectionedValues<Section, Value> {
  205. var sectionsAndValues = lhs.sectionsAndValues
  206. for result in diff {
  207. switch result {
  208. case let .sectionInsert(sectionIndex, val):
  209. sectionsAndValues.insert((val, []), at: sectionIndex)
  210. case let .sectionDelete(sectionIndex, _):
  211. sectionsAndValues.remove(at: sectionIndex)
  212. case let .insert(sectionIndex, rowIndex, val):
  213. sectionsAndValues[sectionIndex].1.insert(val, at: rowIndex)
  214. case let .delete(sectionIndex, rowIndex, _):
  215. sectionsAndValues[sectionIndex].1.remove(at: rowIndex)
  216. }
  217. }
  218. return SectionedValues(sectionsAndValues)
  219. }
  220. private static func diffInternal<Value: Equatable>(
  221. _ table: [[Int]],
  222. _ x: [Value],
  223. _ y: [Value],
  224. _ i: Int,
  225. _ j: Int,
  226. _ currentResults: ([DiffStep<Value>], [DiffStep<Value>])
  227. ) -> Result<([DiffStep<Value>], [DiffStep<Value>])> {
  228. if i == 0 && j == 0 {
  229. return .done(currentResults)
  230. }
  231. else {
  232. return .call {
  233. var nextResults = currentResults
  234. if i == 0 {
  235. nextResults.0 = [DiffStep.insert(j-1, y[j-1])] + nextResults.0
  236. return diffInternal(table, x, y, i, j-1, nextResults)
  237. } else if j == 0 {
  238. nextResults.1 = nextResults.1 + [DiffStep.delete(i-1, x[i-1])]
  239. return diffInternal(table, x, y, i - 1, j, nextResults)
  240. } else if table[i][j] == table[i][j-1] {
  241. nextResults.0 = [DiffStep.insert(j-1, y[j-1])] + nextResults.0
  242. return diffInternal(table, x, y, i, j-1, nextResults)
  243. } else if table[i][j] == table[i-1][j] {
  244. nextResults.1 = nextResults.1 + [DiffStep.delete(i-1, x[i-1])]
  245. return diffInternal(table, x, y, i - 1, j, nextResults)
  246. } else {
  247. return diffInternal(table, x, y, i-1, j-1, nextResults)
  248. }
  249. }
  250. }
  251. }
  252. }
  253. fileprivate enum Result<T>{
  254. case done(T)
  255. case call(() -> Result<T>)
  256. }
  257. fileprivate struct MemoizedSequenceComparison<T: Equatable> {
  258. static func buildTable(_ x: [T], _ y: [T], _ n: Int, _ m: Int) -> [[Int]] {
  259. var table = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)
  260. // using unsafe pointers lets us avoid swift array bounds-checking, which results in a considerable speed boost.
  261. table.withUnsafeMutableBufferPointer { unsafeTable in
  262. x.withUnsafeBufferPointer { unsafeX in
  263. y.withUnsafeBufferPointer { unsafeY in
  264. for i in 1...n {
  265. for j in 1...m {
  266. if unsafeX[i&-1] == unsafeY[j&-1] {
  267. unsafeTable[i][j] = unsafeTable[i&-1][j&-1] + 1
  268. } else {
  269. unsafeTable[i][j] = max(unsafeTable[i&-1][j], unsafeTable[i][j&-1])
  270. }
  271. }
  272. }
  273. }
  274. }
  275. }
  276. return table
  277. }
  278. }
  279. // MARK: - Deprecated
  280. public extension Array where Element: Equatable {
  281. /// Deprecated in favor of `Dwifft.diff`.
  282. @available(*, deprecated)
  283. public func diff(_ other: [Element]) -> [DiffStep<Element>] {
  284. return Dwifft.diff(self, other)
  285. }
  286. /// Deprecated in favor of `Dwifft.apply`.
  287. @available(*, deprecated)
  288. public func apply(_ diff: [DiffStep<Element>]) -> [Element] {
  289. return Dwifft.apply(diff: diff, toArray: self)
  290. }
  291. }