├── .gitignore ├── LICENSE.txt ├── Package.swift ├── README.md ├── Sources └── Diffing │ ├── CollectionChanges.swift │ ├── CommonPrefix.swift │ ├── CountingIndexCollection.swift │ ├── OrderedCollection.swift │ ├── OrderedCollectionDifference.swift │ ├── RangeReplaceableCollection.swift │ └── TriangularMatrix.swift └── Tests ├── DiffingTests ├── CollectionChangesTests.swift ├── CommonPrefixTests.swift ├── CountingIndexCollectionTests.swift ├── OrderedCollectionDifferenceTests.swift ├── OrderedCollectionTests.swift ├── RangeReplaceableCollectionTests.swift ├── TriangularMatrixTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | 205 | ### Runtime Library Exception to the Apache 2.0 License: ### 206 | 207 | 208 | As an exception, if you use this Software to compile your source code and 209 | portions of this Software are embedded into the binary product as a result, 210 | you may redistribute such product without providing attribution as would 211 | otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. 212 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Diffing", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Diffing", 12 | targets: ["Diffing"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "Diffing", 23 | dependencies: []), 24 | .testTarget( 25 | name: "DiffingTests", 26 | dependencies: ["Diffing"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ordered Collection Diffing 2 | 3 | This repository holds the original prototype implementing ordered collection diffing as [proposed to swift-evolution](https://github.com/apple/swift-evolution/pull/968) and [pitched in the swift forums](https://forums.swift.org/t/ordered-collection-diffing/18933). 4 | 5 | This code has been adapted into a [PR against the standard library](https://github.com/apple/swift/pull/21845) and now only exist as a historical artifact for the curious. -------------------------------------------------------------------------------- /Sources/Diffing/CollectionChanges.swift: -------------------------------------------------------------------------------- 1 | /// A collection of changes between a source and target collection. 2 | /// 3 | /// It can be used to traverse the [longest common subsequence][lcs] of 4 | /// source and target: 5 | /// 6 | /// let changes = CollectionChanges(from: source, to target) 7 | /// for case let .match(s, t) in changes { 8 | /// // use `s`, `t` 9 | /// } 10 | /// 11 | /// It can also be used to traverse the [shortest edit script][ses] of 12 | /// remove and insert operations: 13 | /// 14 | /// let changes = CollectionChanges(from: source, to target) 15 | /// for c in changes { 16 | /// switch c { 17 | /// case let .removed(s): 18 | /// // use `s` 19 | /// case let .inserted(t): 20 | /// // use `t` 21 | /// case .matched: continue 22 | /// } 23 | /// } 24 | /// 25 | /// [lcs]: http://en.wikipedia.org/wiki/Longest_common_subsequence_problem 26 | /// [ses]: http://en.wikipedia.org/wiki/Edit_distance 27 | /// 28 | /// - Note: `CollectionChanges` holds a reference to state used to run the 29 | /// difference algorithm, which can be exponentially larger than the 30 | /// changes themselves. 31 | struct CollectionChanges< 32 | SourceIndex : Comparable, TargetIndex : Comparable 33 | > { 34 | typealias Endpoint = (x: SourceIndex, y: TargetIndex) 35 | 36 | /// An encoding of change elements as an array of index pairs stored in 37 | /// `pathStorage[pathStartIndex...]`. 38 | /// 39 | /// This encoding allows the same storage to be used to run the difference 40 | /// algorithm, report the result, and repeat in place using 41 | /// `formChanges`. 42 | /// 43 | /// The collection of changes between XABCD and XYCD is: 44 | /// 45 | /// [.match(0..<1, 0..<1), .remove(1..<3), .insert(1..<2), 46 | /// .match(3..<5, 2..<4)] 47 | /// 48 | /// Which gets encoded as: 49 | /// 50 | /// [(0, 0), (1, 1), (3, 1), (3, 2), (5, 4)] 51 | /// 52 | /// You can visualize it as a two-dimensional path composed of remove 53 | /// (horizontal), insert (vertical), and match (diagonal) segments: 54 | /// 55 | /// X A B C D 56 | /// X \ _ _ 57 | /// Y | 58 | /// C \ 59 | /// D \ 60 | /// 61 | private var pathStorage: [Endpoint] 62 | 63 | /// The index in `pathStorage` of the first segment in the difference path. 64 | private var pathStartIndex: Int 65 | 66 | /// Creates a collection of changes from a difference path. 67 | fileprivate init( 68 | pathStorage: [Endpoint], pathStartIndex: Int 69 | ) { 70 | self.pathStorage = pathStorage 71 | self.pathStartIndex = pathStartIndex 72 | } 73 | 74 | /// Creates an empty collection of changes, i.e. the changes between two 75 | /// empty collections. 76 | init() { 77 | self.pathStorage = [] 78 | self.pathStartIndex = 0 79 | } 80 | } 81 | 82 | extension CollectionChanges { 83 | /// A range of elements removed from the source, inserted in the target, or 84 | /// that the source and target have in common. 85 | enum Element { 86 | case removed(Range) 87 | case inserted(Range) 88 | case matched(Range, Range) 89 | } 90 | } 91 | 92 | extension CollectionChanges : RandomAccessCollection { 93 | typealias Index = Int 94 | 95 | var startIndex: Index { 96 | return 0 97 | } 98 | 99 | var endIndex: Index { 100 | return Swift.max(0, pathStorage.endIndex - pathStartIndex - 1) 101 | } 102 | 103 | func index(after i: Index) -> Index { 104 | return i + 1 105 | } 106 | 107 | func index(before i: Index) -> Index { 108 | return i - 1 109 | } 110 | 111 | subscript(position: Index) -> Element { 112 | precondition((startIndex..( 141 | from source: Source, to target: Target, by areEquivalent: (Source.Element, Target.Element) -> Bool 142 | ) where 143 | Source.Element == Target.Element, 144 | Source.Index == SourceIndex, 145 | Target.Index == TargetIndex 146 | { 147 | self.init() 148 | formChanges(from: source, to: target, by: areEquivalent) 149 | } 150 | 151 | /// Replaces `self` with the collection of changes between `source` 152 | /// and `target`. 153 | /// 154 | /// - Runtime: O(*n* * *d*), where *n* is `source.count + target.count` and 155 | /// *d* is the minimal number of inserted and removed elements. 156 | /// - Space: O(*d*²), where *d* is the minimal number of inserted and 157 | /// removed elements. 158 | mutating func formChanges< 159 | Source : OrderedCollection, Target : OrderedCollection 160 | >( 161 | from source: Source, to target: Target, by areEquivalent: (Source.Element, Target.Element) -> Bool 162 | ) where 163 | Source.Element == Target.Element, 164 | Source.Index == SourceIndex, 165 | Target.Index == TargetIndex 166 | { 167 | let pathStart = (x: source.startIndex, y: target.startIndex) 168 | let pathEnd = (x: source.endIndex, y: target.endIndex) 169 | let matches = source.commonPrefix(with: target, by: areEquivalent) 170 | let (x, y) = (matches.0.endIndex, matches.1.endIndex) 171 | 172 | if pathStart == pathEnd { 173 | pathStorage.removeAll(keepingCapacity: true) 174 | pathStartIndex = 0 175 | } else if x == pathEnd.x || y == pathEnd.y { 176 | pathStorage.removeAll(keepingCapacity: true) 177 | pathStorage.append(pathStart) 178 | if pathStart != (x, y) && pathEnd != (x, y) { 179 | pathStorage.append((x, y)) 180 | } 181 | pathStorage.append(pathEnd) 182 | pathStartIndex = 0 183 | } else { 184 | formChangesCore(from: source, to: target, x: x, y: y, by: areEquivalent) 185 | } 186 | } 187 | 188 | /// The core difference algorithm. 189 | /// 190 | /// - Precondition: There is at least one difference between `a` and `b` 191 | /// - Runtime: O(*n* * *d*), where *n* is `a.count + b.count` and 192 | /// *d* is the number of inserts and removes. 193 | /// - Space: O(*d* * *d*), where *d* is the number of inserts and removes. 194 | @inline(__always) 195 | private mutating func formChangesCore< 196 | Source : OrderedCollection, Target : OrderedCollection 197 | >( 198 | from a: Source, 199 | to b: Target, 200 | x: Source.Index, 201 | y: Target.Index, 202 | by areEquivalent: (Source.Element, Target.Element) -> Bool 203 | ) where 204 | Source.Element == Target.Element, 205 | Source.Index == SourceIndex, 206 | Target.Index == TargetIndex 207 | { 208 | // Written to correspond, as closely as possible, to the psuedocode in 209 | // Myers, E. "An O(ND) Difference Algorithm and Its Variations". 210 | // 211 | // See "FIGURE 2: The Greedy LCS/SES Algorithm" on p. 6 of the [paper]. 212 | // 213 | // Note the following differences from the psuedocode in FIGURE 2: 214 | // 215 | // 1. FIGURE 2 relies on both *A* and *B* being Arrays. In a generic 216 | // context, it isn't true that *y = x - k*, as *x*, *y*, *k* could 217 | // all be different types, so we store both *x* and *y* in *V*. 218 | // 2. FIGURE 2 only reports the length of the LCS/SES. Reporting a 219 | // solution path requires storing a copy of *V* (the search frontier) 220 | // after each iteration of the outer loop. 221 | // 3. FIGURE 2 stops the search after *MAX* iterations. We run the loop 222 | // until a solution is found. We also guard against incrementing past 223 | // the end of *A* and *B*, both to satisfy the termination condition 224 | // and because that would violate preconditions on collection. 225 | // 226 | // [paper]: http://www.xmailserver.org/diff2.pdf 227 | var (x, y) = (x, y) 228 | let (n, m) = (a.endIndex, b.endIndex) 229 | 230 | var v = SearchState(consuming: &pathStorage) 231 | 232 | v.appendFrontier(repeating: (x, y)) 233 | var d = 1 234 | var delta = 0 235 | outer: while true { 236 | v.appendFrontier(repeating: (n, m)) 237 | for k in stride(from: -d, through: d, by: 2) { 238 | if k == -d || (k != d && v[d - 1, k - 1].x < v[d - 1, k + 1].x) { 239 | (x, y) = v[d - 1, k + 1] 240 | if y != m { b.formIndex(after: &y) } 241 | } else { 242 | (x, y) = v[d - 1, k - 1] 243 | if x != n { a.formIndex(after: &x) } 244 | } 245 | 246 | let matches = a[x.. { 266 | typealias Endpoint = (x: SourceIndex, y: TargetIndex) 267 | 268 | /// The search frontier for each iteration. 269 | /// 270 | /// The nth iteration of the core algorithm requires storing n + 1 search 271 | /// path endpoints. Thus, the shape of the storage required is a triangle. 272 | private var endpoints = LowerTriangularMatrix() 273 | 274 | /// Creates an instance, taking the capacity of `storage` for itself. 275 | /// 276 | /// - Postcondition: `storage` is empty. 277 | init(consuming storage: inout [Endpoint]) { 278 | storage.removeAll(keepingCapacity: true) 279 | swap(&storage, &endpoints.storage) 280 | } 281 | 282 | /// Returns the endpoint of the search frontier for iteration `d` on 283 | /// diagonal `k`. 284 | subscript(d: Int, k: Int) -> Endpoint { 285 | get { 286 | assert((-d...d).contains(k)) 287 | assert((d + k) % 2 == 0) 288 | return endpoints[d, (d + k) / 2] 289 | } 290 | set { 291 | assert((-d...d).contains(k)) 292 | assert((d + k) % 2 == 0) 293 | endpoints[d, (d + k) / 2] = newValue 294 | } 295 | } 296 | 297 | /// Adds endpoints initialized to `repeatedValue` for the search frontier of 298 | /// the next iteration. 299 | mutating func appendFrontier(repeating repeatedValue: Endpoint) { 300 | endpoints.appendRow(repeating: repeatedValue) 301 | } 302 | } 303 | 304 | extension SearchState { 305 | /// Removes and returns `CollectionChanges`, leaving `SearchState` empty. 306 | /// 307 | /// - Precondition: There is at least one difference between `a` and `b` 308 | mutating func removeCollectionChanges< 309 | Source : OrderedCollection, Target : OrderedCollection 310 | >( 311 | a: Source, b: Target, d: Int, delta: Int 312 | ) -> CollectionChanges 313 | where Source.Index == SourceIndex, Target.Index == TargetIndex 314 | { 315 | // Calculating the difference path is very similar to running the core 316 | // algorithm in reverse: 317 | // 318 | // var k = delta 319 | // for d in (1...d).reversed() { 320 | // if k == -d || (k != d && self[d - 1, k - 1].x < self[d - 1, k + 1].x) { 321 | // // insert of self[d - 1, k + 1].y 322 | // k += 1 323 | // } else { 324 | // // remove of self[d - 1, k - 1].x 325 | // k -= 1 326 | // } 327 | // } 328 | // 329 | // It is more complicated below because: 330 | // 331 | // 1. We want to include segments for matches 332 | // 2. We want to coallesce consecutive like segments 333 | // 3. We don't want to allocate, so we're overwriting the elements of 334 | // endpoints.storage we've finished reading. 335 | 336 | let pathStart = (a.startIndex, b.startIndex) 337 | let pathEnd = (a.endIndex, b.endIndex) 338 | 339 | // `endpoints.storage` may need space for an additional element in order 340 | // to store the difference path when `d == 1`. 341 | // 342 | // `endpoints.storage` has `(d + 1) * (d + 2) / 2` elements stored, 343 | // but a difference path requires up to `2 + d * 2` elements[^1]. 344 | // 345 | // If `d == 1`: 346 | // 347 | // (1 + 1) * (1 + 2) / 2 < 2 + 1 * 2 348 | // 3 < 4 349 | // 350 | // `d == 1` is the only special case because: 351 | // 352 | // - It's a precondition that `d > 0`. 353 | // - Once `d >= 2` `endpoints.storage` will have sufficient space: 354 | // 355 | // (d + 1) * (d + 2) / 2 = 2 + d * 2 356 | // d * d - d - 2 = 0 357 | // (d - 2) * (d + 1) = 0 358 | // d = 2; d = -1 359 | // 360 | // [1]: An endpoint for every remove, insert, and match segment. (Recall 361 | // *d* is the minimal number of inserted and removed elements). If there 362 | // are no consecutive removes or inserts and every remove or insert is 363 | // sandwiched between matches, the path will need `2 + d * 2` elements. 364 | assert(d > 0, "Must be at least one difference between `a` and `b`") 365 | if d == 1 { 366 | endpoints.storage.append(pathEnd) 367 | } 368 | 369 | var i = endpoints.storage.endIndex - 1 370 | // `isInsertion` tracks whether the element at `endpoints.storage[i]` 371 | // is an insertion (`true`), a removal (`false`), or a match (`nil`). 372 | var isInsertion: Bool? = nil 373 | var k = delta 374 | endpoints.storage[i] = pathEnd 375 | for d in (1...d).reversed() { 376 | if k == -d || (k != d && self[d - 1, k - 1].x < self[d - 1, k + 1].x) { 377 | let (x, y) = self[d - 1, k + 1] 378 | 379 | // There was match before this insert, so add a segment. 380 | if x != endpoints.storage[i].x { 381 | i -= 1; endpoints.storage[i] = (x, b.index(after: y)) 382 | isInsertion = nil 383 | } 384 | 385 | // If the previous segment is also an insert, overwrite it. 386 | if isInsertion != .some(true) { i -= 1 } 387 | endpoints.storage[i] = (x, y) 388 | 389 | isInsertion = true 390 | k += 1 391 | } else { 392 | let (x, y) = self[d - 1, k - 1] 393 | 394 | // There was a match before this remove, so add a segment. 395 | if y != endpoints.storage[i].y { 396 | i -= 1; endpoints.storage[i] = (a.index(after: x), y) 397 | isInsertion = nil 398 | } 399 | 400 | // If the previous segment is also a remove, overwrite it. 401 | if isInsertion != .some(false) { i -= 1 } 402 | endpoints.storage[i] = (x, y) 403 | 404 | isInsertion = false 405 | k -= 1 406 | } 407 | } 408 | 409 | if pathStart != endpoints.storage[i] { 410 | i -= 1; endpoints.storage[i] = pathStart 411 | } 412 | 413 | let pathStorage = endpoints.storage 414 | endpoints.storage = [] 415 | return CollectionChanges(pathStorage: pathStorage, pathStartIndex: i) 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /Sources/Diffing/CommonPrefix.swift: -------------------------------------------------------------------------------- 1 | extension OrderedCollection { 2 | /// Returns a pair of subsequences containing the initial elements that 3 | /// `self` and `other` have in common. 4 | public func commonPrefix( 5 | with other: Other, by areEquivalent: (Element, Other.Element) -> Bool 6 | ) -> (SubSequence, Other.SubSequence) where Element == Other.Element { 7 | let (s1, s2) = (startIndex, other.startIndex) 8 | let (e1, e2) = (endIndex, other.endIndex) 9 | var (i1, i2) = (s1, s2) 10 | while i1 != e1 && i2 != e2 { 11 | if !areEquivalent(self[i1], other[i2]) { break } 12 | formIndex(after: &i1) 13 | other.formIndex(after: &i2) 14 | } 15 | return (self[s1.. : Equatable { 3 | /// The position in the underlying collection. 4 | let base: Base 5 | /// The offset from the start index of the collection or `nil` if `self` is 6 | /// the end index. 7 | let offset: Int? 8 | } 9 | 10 | extension CountingIndex : Comparable { 11 | static func <(lhs: CountingIndex, rhs: CountingIndex) -> Bool { 12 | return (lhs.base, lhs.offset ?? Int.max) 13 | < (rhs.base, rhs.offset ?? Int.max) 14 | } 15 | } 16 | 17 | /// A collection that counts the offset of its indices from its start index. 18 | /// 19 | /// You can use `CountingIndexCollection` with algorithms on `Collection` to 20 | /// calculate offsets of significance: 21 | /// 22 | /// if let i = CountingIndexCollection("Café").index(of: "f") { 23 | /// print(i.offset) 24 | /// } 25 | /// // Prints "2" 26 | /// 27 | /// - Note: The offset of `endIndex` is `nil` 28 | struct CountingIndexCollection { 29 | let base: Base 30 | 31 | init(_ base: Base) { 32 | self.base = base 33 | } 34 | } 35 | 36 | extension CountingIndexCollection : Collection { 37 | typealias Index = CountingIndex 38 | typealias Element = Base.Element 39 | 40 | var startIndex: Index { 41 | return Index(base: base.startIndex, offset: base.isEmpty ? nil : 0) 42 | } 43 | 44 | var endIndex: Index { 45 | return Index(base: base.endIndex, offset: nil) 46 | } 47 | 48 | func index(after i: Index) -> Index { 49 | let next = base.index(after: i.base) 50 | return Index( 51 | base: next, offset: next == base.endIndex ? nil : i.offset! + 1) 52 | } 53 | 54 | subscript(position: Index) -> Element { 55 | return base[position.base] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Diffing/OrderedCollection.swift: -------------------------------------------------------------------------------- 1 | /// An ordered collection treats the structural positions of its elements as 2 | /// part of its interface. Differences in order always affect whether two 3 | /// instances are equal. 4 | /// 5 | /// For example, a tree is an ordered collection; a dictionary is not. 6 | //@available(swift, introduced: 5.1) 7 | public protocol OrderedCollection : Collection 8 | where SubSequence : OrderedCollection 9 | { 10 | /// Returns a Boolean value indicating whether this ordered collection and 11 | /// another ordered collection contain equivalent elements in the same 12 | /// order, using the given predicate as the equivalence test. 13 | /// 14 | /// The predicate must be a *equivalence relation* over the elements. That 15 | /// is, for any elements `a`, `b`, and `c`, the following conditions must 16 | /// hold: 17 | /// 18 | /// - `areEquivalent(a, a)` is always `true`. (Reflexivity) 19 | /// - `areEquivalent(a, b)` implies `areEquivalent(b, a)`. (Symmetry) 20 | /// - If `areEquivalent(a, b)` and `areEquivalent(b, c)` are both `true`, 21 | /// then `areEquivalent(a, c)` is also `true`. (Transitivity) 22 | /// 23 | /// - Parameters: 24 | /// - other: An ordered collection to compare to this ordered collection. 25 | /// - areEquivalent: A predicate that returns `true` if its two arguments 26 | /// are equivalent; otherwise, `false`. 27 | /// - Returns: `true` if this ordered collection and `other` contain 28 | /// equivalent items, using `areEquivalent` as the equivalence test; 29 | /// otherwise, `false.` 30 | /// 31 | /// - Complexity: O(*m*), where *m* is the lesser of the length of the 32 | /// ordered collection and the length of `other`. 33 | func elementsEqual( 34 | _ other: C, by areEquivalent: (Element, C.Element) throws -> Bool 35 | ) rethrows -> Bool where C : OrderedCollection 36 | } 37 | 38 | extension OrderedCollection { 39 | /// Returns the difference needed to produce the receiver's state from the 40 | /// parameter's state, using the provided closure to establish equivalence 41 | /// between elements. 42 | /// 43 | /// This function does not infer element moves, but they can be computed 44 | /// using `OrderedCollectionDifference.inferringMoves()` if 45 | /// desired. 46 | /// 47 | /// Implementation is an optimized variation of the algorithm described by 48 | /// E. Myers (1986). 49 | /// 50 | /// - Parameters: 51 | /// - other: The base state. 52 | /// - areEquivalent: A closure that returns whether the two 53 | /// parameters are equivalent. 54 | /// 55 | /// - Returns: The difference needed to produce the reciever's state from 56 | /// the parameter's state. 57 | /// 58 | /// - Complexity: O(*n* * *d*), where *n* is `other.count + self.count` and 59 | /// *d* is the number of differences between the two ordered collections. 60 | public func difference( 61 | from other: C, by areEquivalent: (Element, C.Element) -> Bool 62 | ) -> OrderedCollectionDifference 63 | where C : OrderedCollection, C.Element == Self.Element 64 | { 65 | var rawChanges: [OrderedCollectionDifference.Change] = [] 66 | 67 | let source = CountingIndexCollection(other) 68 | let target = CountingIndexCollection(self) 69 | for c in CollectionChanges(from: source, to: target, by: areEquivalent) { 70 | switch c { 71 | case let .removed(r): 72 | for i in source.indices[r] { 73 | rawChanges.append( 74 | .remove( 75 | offset: i.offset!, 76 | element: source[i], 77 | associatedWith: nil)) 78 | } 79 | case let .inserted(r): 80 | for i in target.indices[r] { 81 | rawChanges.append( 82 | .insert( 83 | offset: i.offset!, 84 | element: target[i], 85 | associatedWith: nil)) 86 | } 87 | case .matched: break 88 | } 89 | } 90 | 91 | return OrderedCollectionDifference(validatedChanges: rawChanges) 92 | } 93 | } 94 | 95 | extension OrderedCollection where Element : Equatable { 96 | /// Returns a Boolean value indicating whether this ordered collection and 97 | /// another ordered collection contain the same elements in the same order. 98 | /// 99 | /// This example tests whether one countable range shares the same elements 100 | /// as another countable range and an array. 101 | /// 102 | /// let a = 1...3 103 | /// let b = 1...10 104 | /// 105 | /// print(a.elementsEqual(b)) 106 | /// // Prints "false" 107 | /// print(a.elementsEqual([1, 2, 3])) 108 | /// // Prints "true" 109 | /// 110 | /// - Parameter other: An ordered collection to compare to this ordered 111 | /// collection. 112 | /// - Returns: `true` if this ordered collection and `other` contain the 113 | /// same elements in the same order. 114 | /// 115 | /// - Complexity: O(*m*), where *m* is the lesser of the `count` of the 116 | /// ordered collection and the `count` of `other`. 117 | public func elementsEqual(_ other: C) -> Bool 118 | where C : OrderedCollection, C.Element == Element 119 | { 120 | return self.elementsEqual(other, by: ==) 121 | } 122 | 123 | /// Returns the difference needed to produce the receiver's state from the 124 | /// parameter's state, using equality to establish equivalence between 125 | /// elements. 126 | /// 127 | /// This function does not infer element moves, but they can be computed 128 | /// using `OrderedCollectionDifference.inferringMoves()` if 129 | /// desired. 130 | /// 131 | /// Implementation is an optimized variation of the algorithm described by 132 | /// E. Myers (1986). 133 | /// 134 | /// - Parameters: 135 | /// - other: The base state. 136 | /// 137 | /// - Returns: The difference needed to produce the reciever's state from 138 | /// the parameter's state. 139 | /// 140 | /// - Complexity: O(*n* * *d*), where *n* is `other.count + self.count` and 141 | /// *d* is the number of differences between the two ordered collections. 142 | public func difference(from other: C) -> OrderedCollectionDifference 143 | where C: OrderedCollection, C.Element == Self.Element 144 | { 145 | return difference(from: other, by: ==) 146 | } 147 | } 148 | 149 | // extension BidirectionalCollection : OrderedCollection {} 150 | // Implies the following: 151 | extension Array : OrderedCollection {} 152 | extension ArraySlice : OrderedCollection {} 153 | extension ClosedRange : OrderedCollection where Bound : Strideable, Bound.Stride : SignedInteger {} 154 | extension CollectionOfOne : OrderedCollection {} 155 | extension ContiguousArray : OrderedCollection {} 156 | extension EmptyCollection : OrderedCollection {} 157 | extension Range : OrderedCollection where Bound : Strideable, Bound.Stride : SignedInteger {} 158 | extension String : OrderedCollection {} 159 | extension Substring : OrderedCollection {} 160 | extension UnsafeBufferPointer : OrderedCollection {} 161 | extension UnsafeMutableBufferPointer : OrderedCollection {} 162 | import Foundation 163 | #if swift(>=5.0) 164 | extension DataProtocol : OrderedCollection {} 165 | #else 166 | extension Data : OrderedCollection {} 167 | #endif 168 | extension IndexPath : OrderedCollection {} 169 | 170 | // Unidirectional collection adoption of OrderedCollection: 171 | extension CountingIndexCollection : OrderedCollection where Base : OrderedCollection {} 172 | extension Slice : OrderedCollection where Base : OrderedCollection {} 173 | extension UnsafeMutableRawBufferPointer : OrderedCollection {} 174 | extension UnsafeRawBufferPointer : OrderedCollection {} 175 | -------------------------------------------------------------------------------- /Sources/Diffing/OrderedCollectionDifference.swift: -------------------------------------------------------------------------------- 1 | /// A type that represents the difference between two ordered collection states. 2 | // @available(swift, introduced: 5.1) 3 | public struct OrderedCollectionDifference { 4 | /// A type that represents a single change to an ordered collection. 5 | /// 6 | /// The `offset` of each `insert` refers to the offset of its `element` in 7 | /// the final state after the difference is fully applied. The `offset` of 8 | /// each `remove` refers to the offset of its `element` in the original 9 | /// state. Non-`nil` values of `associatedWith` refer to the offset of the 10 | /// complementary change. 11 | public enum Change { 12 | case insert(offset: Int, element: ChangeElement, associatedWith: Int?) 13 | case remove(offset: Int, element: ChangeElement, associatedWith: Int?) 14 | 15 | // Internal common field accessors 16 | var offset: Int { 17 | get { 18 | switch self { 19 | case .insert(offset: let o, element: _, associatedWith: _): 20 | return o 21 | case .remove(offset: let o, element: _, associatedWith: _): 22 | return o 23 | } 24 | } 25 | } 26 | var element: ChangeElement { 27 | get { 28 | switch self { 29 | case .insert(offset: _, element: let e, associatedWith: _): 30 | return e 31 | case .remove(offset: _, element: let e, associatedWith: _): 32 | return e 33 | } 34 | } 35 | } 36 | var associatedOffset: Int? { 37 | get { 38 | switch self { 39 | case .insert(offset: _, element: _, associatedWith: let o): 40 | return o 41 | case .remove(offset: _, element: _, associatedWith: let o): 42 | return o 43 | } 44 | } 45 | } 46 | } 47 | 48 | // The public initializer calls this function to ensure that its parameter 49 | // meets the conditions set in its documentation. 50 | private static func validateChanges(_ changes : C) -> Bool where C:Collection, C.Element == Change { 51 | if changes.count == 0 { return true } 52 | 53 | var insertAssocToOffset = Dictionary() 54 | var removeOffsetToAssoc = Dictionary() 55 | var insertOffset = Set() 56 | var removeOffset = Set() 57 | 58 | for c in changes { 59 | let offset = c.offset 60 | if offset < 0 { return false } 61 | 62 | switch c { 63 | case .remove(_, _, _): 64 | if removeOffset.contains(offset) { return false } 65 | removeOffset.insert(offset) 66 | case .insert(_, _, _): 67 | if insertOffset.contains(offset) { return false } 68 | insertOffset.insert(offset) 69 | } 70 | 71 | if let assoc = c.associatedOffset { 72 | if assoc < 0 { return false } 73 | switch c { 74 | case .remove(_, _, _): 75 | if removeOffsetToAssoc[offset] != nil { return false } 76 | removeOffsetToAssoc[offset] = assoc 77 | case .insert(_, _, _): 78 | if insertAssocToOffset[assoc] != nil { return false } 79 | insertAssocToOffset[assoc] = offset 80 | } 81 | } 82 | } 83 | 84 | return removeOffsetToAssoc == insertAssocToOffset 85 | } 86 | 87 | /// Creates an instance from a collection of changes. 88 | /// 89 | /// For clients interested in the difference between two ordered 90 | /// collections, see `OrderedCollection.difference(from:)`. 91 | /// 92 | /// To guarantee that instances are unambiguous and safe for compatible base 93 | /// states, this initializer will fail unless its parameter meets to the 94 | /// following requirements: 95 | /// 96 | /// 1) All insertion offsets are unique 97 | /// 2) All removal offsets are unique 98 | /// 3) All offset associations between insertions and removals are symmetric 99 | /// 100 | /// - Parameter changes: A collection of changes that represent a transition 101 | /// between two states. 102 | /// 103 | /// - Complexity: O(*n* * log(*n*)), where *n* is the length of the 104 | /// parameter. 105 | public init?(_ c: C) where C:Collection, C.Element == Change { 106 | if !OrderedCollectionDifference.validateChanges(c) { 107 | return nil 108 | } 109 | 110 | self.init(validatedChanges: c) 111 | } 112 | 113 | // Internal initializer for use by algorithms that cannot produce invalid 114 | // collections of changes. These include the Myers' diff algorithm and 115 | // the move inferencer. 116 | init(validatedChanges c: C) where C:Collection, C.Element == Change { 117 | let changes = c.sorted { (a, b) -> Bool in 118 | switch (a, b) { 119 | case (.remove(_, _, _), .insert(_, _, _)): 120 | return true 121 | case (.insert(_, _, _), .remove(_, _, _)): 122 | return false 123 | default: 124 | return a.offset < b.offset 125 | } 126 | } 127 | 128 | // Find first insertion via binary search 129 | let firstInsertIndex: Int 130 | if changes.count == 0 { 131 | firstInsertIndex = 0 132 | } else { 133 | var range = 0...changes.count 134 | while range.lowerBound != range.upperBound { 135 | let i = (range.lowerBound + range.upperBound) / 2 136 | switch changes[i] { 137 | case .insert(_, _, _): 138 | range = range.lowerBound...i 139 | case .remove(_, _, _): 140 | range = (i + 1)...range.upperBound 141 | } 142 | } 143 | firstInsertIndex = range.lowerBound 144 | } 145 | 146 | removals = Array(changes[0...Change 179 | 180 | // Opaque index type is isomorphic to Int 181 | public struct Index: Comparable, Hashable { 182 | public static func < (lhs: OrderedCollectionDifference.Index, rhs: OrderedCollectionDifference.Index) -> Bool { 183 | return lhs.i < rhs.i 184 | } 185 | 186 | let i: Int 187 | init(_ index: Int) { 188 | i = index 189 | } 190 | } 191 | 192 | public var startIndex: OrderedCollectionDifference.Index { 193 | return Index(0) 194 | } 195 | 196 | public var endIndex: OrderedCollectionDifference.Index { 197 | return Index(removals.count + insertions.count) 198 | } 199 | 200 | public func index(after index: OrderedCollectionDifference.Index) -> OrderedCollectionDifference.Index { 201 | return Index(index.i + 1) 202 | } 203 | 204 | public subscript(position: OrderedCollectionDifference.Index) -> Element { 205 | return position.i < removals.count ? removals[removals.count - (position.i + 1)] : insertions[position.i - removals.count] 206 | } 207 | 208 | public func index(before index: OrderedCollectionDifference.Index) -> OrderedCollectionDifference.Index { 209 | return Index(index.i - 1) 210 | } 211 | 212 | public func formIndex(_ index: inout OrderedCollectionDifference.Index, offsetBy distance: Int) { 213 | index = Index(index.i + distance) 214 | } 215 | 216 | public func distance(from start: OrderedCollectionDifference.Index, to end: OrderedCollectionDifference.Index) -> Int { 217 | return end.i - start.i 218 | } 219 | } 220 | 221 | extension OrderedCollectionDifference.Change: Equatable where ChangeElement: Equatable {} 222 | 223 | extension OrderedCollectionDifference: Equatable where ChangeElement: Equatable {} 224 | 225 | extension OrderedCollectionDifference.Change: Hashable where ChangeElement: Hashable {} 226 | 227 | extension OrderedCollectionDifference: Hashable where ChangeElement: Hashable { 228 | 229 | /// Infers which `ChangeElement`s have been both inserted and removed only 230 | /// once and returns a new difference with those associations. 231 | /// 232 | /// - Returns: an instance with all possible moves inferred. 233 | /// 234 | /// - Complexity: O(*n*) where *n* is `self.count` 235 | public func inferringMoves() -> OrderedCollectionDifference { 236 | let removeDict: [ChangeElement:Int?] = { 237 | var res = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count)) 238 | for r in removals { 239 | let element = r.element 240 | if res[element] != .none { 241 | res[element] = .some(.none) 242 | } else { 243 | res[element] = .some(r.offset) 244 | } 245 | } 246 | return res.filter { (_, v) -> Bool in v != .none } 247 | }() 248 | 249 | let insertDict: [ChangeElement:Int?] = { 250 | var res = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count)) 251 | for i in insertions { 252 | let element = i.element 253 | if res[element] != .none { 254 | res[element] = .some(.none) 255 | } else { 256 | res[element] = .some(i.offset) 257 | } 258 | } 259 | return res.filter { (_, v) -> Bool in v != .none } 260 | }() 261 | 262 | return OrderedCollectionDifference.init(validatedChanges:map({ (c: OrderedCollectionDifference.Change) -> OrderedCollectionDifference.Change in 263 | switch c { 264 | case .remove(offset: let o, element: let e, associatedWith: _): 265 | if removeDict[e] == nil { 266 | return c 267 | } 268 | if let assoc = insertDict[e] { 269 | return .remove(offset: o, element: e, associatedWith: assoc) 270 | } 271 | case .insert(offset: let o, element: let e, associatedWith: _): 272 | if insertDict[e] == nil { 273 | return c 274 | } 275 | if let assoc = removeDict[e] { 276 | return .insert(offset: o, element: e, associatedWith: assoc) 277 | } 278 | } 279 | return c 280 | })) 281 | } 282 | } 283 | 284 | extension OrderedCollectionDifference.Change: Codable where ChangeElement: Codable { 285 | private enum CodingKeys: String, CodingKey { 286 | case offset 287 | case element 288 | case associatedOffset 289 | case isRemove 290 | } 291 | 292 | public init(from decoder: Decoder) throws { 293 | let values = try decoder.container(keyedBy: CodingKeys.self) 294 | let offset = try values.decode(Int.self, forKey: .offset) 295 | let element = try values.decode(ChangeElement.self, forKey: .element) 296 | let associatedOffset = try values.decode(Int?.self, forKey: .associatedOffset) 297 | let isRemove = try values.decode(Bool.self, forKey: .isRemove) 298 | if isRemove { 299 | self = .remove(offset: offset, element: element, associatedWith: associatedOffset) 300 | } else { 301 | self = .insert(offset: offset, element: element, associatedWith: associatedOffset) 302 | } 303 | } 304 | 305 | public func encode(to encoder: Encoder) throws { 306 | var container = encoder.container(keyedBy: CodingKeys.self) 307 | switch self { 308 | case .remove(_, _, _): 309 | try container.encode(true, forKey: .isRemove) 310 | case .insert(_, _, _): 311 | try container.encode(false, forKey: .isRemove) 312 | } 313 | 314 | try container.encode(offset, forKey: .offset) 315 | try container.encode(element, forKey: .element) 316 | try container.encode(associatedOffset, forKey: .associatedOffset) 317 | } 318 | } 319 | 320 | extension OrderedCollectionDifference: Codable where ChangeElement: Codable {} 321 | -------------------------------------------------------------------------------- /Sources/Diffing/RangeReplaceableCollection.swift: -------------------------------------------------------------------------------- 1 | extension RangeReplaceableCollection { 2 | @inline(__always) private static func fastApplicationEnumeration( 3 | of diff: OrderedCollectionDifference, 4 | _ f: (OrderedCollectionDifference.Change) -> Void 5 | ) { 6 | let totalRemoves = diff.removals.count 7 | let totalInserts = diff.insertions.count 8 | var enumeratedRemoves = 0 9 | var enumeratedInserts = 0 10 | 11 | while enumeratedRemoves < totalRemoves || enumeratedInserts < totalInserts { 12 | let consume: OrderedCollectionDifference.Change 13 | if enumeratedRemoves < diff.removals.count && enumeratedInserts < diff.insertions.count { 14 | let removeOffset = diff.removals[enumeratedRemoves].offset 15 | let insertOffset = diff.insertions[enumeratedInserts].offset 16 | if removeOffset - enumeratedRemoves <= insertOffset - enumeratedInserts { 17 | consume = diff.removals[enumeratedRemoves] 18 | } else { 19 | consume = diff.insertions[enumeratedInserts] 20 | } 21 | } else if enumeratedRemoves < totalRemoves { 22 | consume = diff.removals[enumeratedRemoves] 23 | } else if enumeratedInserts < totalInserts { 24 | consume = diff.insertions[enumeratedInserts] 25 | } else { 26 | // Not reached, loop should have exited. 27 | preconditionFailure() 28 | } 29 | 30 | f(consume) 31 | 32 | switch consume { 33 | case .remove(_, _, _): 34 | enumeratedRemoves += 1 35 | case .insert(_, _, _): 36 | enumeratedInserts += 1 37 | } 38 | } 39 | } 40 | 41 | /// Applies a difference to a collection. 42 | /// 43 | /// - Parameter difference: The difference to be applied. 44 | /// 45 | /// - Returns: An instance representing the state of the receiver with the 46 | /// difference applied, or `nil` if the difference is incompatible with 47 | /// the receiver's state. 48 | /// 49 | /// - Complexity: O(*n* + *c*), where *n* is `self.count` and *c* is the 50 | /// number of changes contained by the parameter. 51 | // @available(swift, introduced: 5.1) 52 | public func applying(_ difference: OrderedCollectionDifference) -> Self? { 53 | var result = Self() 54 | var enumeratedRemoves = 0 55 | var enumeratedInserts = 0 56 | var enumeratedOriginals = 0 57 | var currentIndex = self.startIndex 58 | 59 | func append(into target: inout Self, contentsOf source: Self, from index: inout Self.Index, count: Int) { 60 | let start = index 61 | source.formIndex(&index, offsetBy: count) 62 | target.append(contentsOf: source[start.. Int { 5 | return n * (n + 1) / 2 6 | } 7 | 8 | /// A square matrix that only provides subscript access to elements on, or 9 | /// below, the main diagonal. 10 | /// 11 | /// A [lower triangular matrix] can be dynamically grown: 12 | /// 13 | /// var m = LowerTriangularMatrix() 14 | /// m.appendRow(repeating: 1) 15 | /// m.appendRow(repeating: 2) 16 | /// m.appendRow(repeating: 3) 17 | /// 18 | /// assert(Array(m.rowMajorOrder) == [ 19 | /// 1, 20 | /// 2, 2, 21 | /// 3, 3, 3, 22 | /// ]) 23 | /// 24 | /// [lower triangular matrix]: http://en.wikipedia.org/wiki/Triangular_matrix 25 | struct LowerTriangularMatrix { 26 | /// The matrix elements stored in [row major order][rmo]. 27 | /// 28 | /// [rmo]: http://en.wikipedia.org/wiki/Row-_and_column-major_order 29 | var storage: [Element] = [] 30 | 31 | /// The dimension of the matrix. 32 | /// 33 | /// Being a square matrix, the number of rows and columns are equal. 34 | var dimension: Int = 0 35 | 36 | subscript(row: Int, column: Int) -> Element { 37 | get { 38 | assert((0...row).contains(column)) 39 | return storage[triangularNumber(row) + column] 40 | } 41 | set { 42 | assert((0...row).contains(column)) 43 | storage[triangularNumber(row) + column] = newValue 44 | } 45 | } 46 | 47 | mutating func appendRow(repeating repeatedValue: Element) { 48 | dimension += 1 49 | storage.append(contentsOf: repeatElement(repeatedValue, count: dimension)) 50 | } 51 | } 52 | 53 | extension LowerTriangularMatrix { 54 | /// A collection that visits the elements in the matrix in [row major 55 | /// order][rmo]. 56 | /// 57 | /// [rmo]: http://en.wikipedia.org/wiki/Row-_and_column-major_order 58 | struct RowMajorOrder : RandomAccessCollection { 59 | var base: LowerTriangularMatrix 60 | 61 | var startIndex: Int { 62 | return base.storage.startIndex 63 | } 64 | 65 | var endIndex: Int { 66 | return base.storage.endIndex 67 | } 68 | 69 | func index(after i: Int) -> Int { 70 | return i + 1 71 | } 72 | 73 | func index(before i: Int) -> Int { 74 | return i - 1 75 | } 76 | 77 | subscript(position: Int) -> Element { 78 | return base.storage[position] 79 | } 80 | } 81 | 82 | var rowMajorOrder: RowMajorOrder { 83 | return RowMajorOrder(base: self) 84 | } 85 | 86 | subscript(row r: Int) -> Slice { 87 | return rowMajorOrder[triangularNumber(r)..( 92 | from source: C1, 93 | to target: C2, 94 | changeCount expectedChangeCount: Int, 95 | matchCount expectedMatchCount: Int, 96 | segmentCount expectedSegmentCount: Int 97 | ) where C1.Element == C2.Element, C1.Element : Equatable { 98 | let m = " from: \(source) to: \(target)" 99 | 100 | var changeCount = 0 101 | var matchCount = 0 102 | var segmentCount = 0 103 | for segment in CollectionChanges(from: source, to: target, by: ==) { 104 | switch segment { 105 | case let .removed(x): 106 | changeCount += source[x].count 107 | case let .inserted(y): 108 | changeCount += target[y].count 109 | case let .matched(x, y): 110 | XCTAssert( 111 | source[x].elementsEqual(target[y]), "elementsEqual" + m) 112 | matchCount += source[x].count 113 | } 114 | segmentCount += 1 115 | } 116 | 117 | XCTAssertEqual( 118 | changeCount, expectedChangeCount, "changeCount" + m) 119 | XCTAssertEqual( 120 | matchCount, expectedMatchCount, "matchCount" + m) 121 | XCTAssertEqual( 122 | segmentCount, expectedSegmentCount, "segmentCount" + m) 123 | } 124 | 125 | // array 126 | for (source, target, changeCount, matchCount, segmentCount) in tests { 127 | checkChanges( 128 | from: Array(source), 129 | to: Array(target), 130 | changeCount: changeCount, 131 | matchCount: matchCount, 132 | segmentCount: segmentCount) 133 | } 134 | 135 | // string 136 | for (source, target, changeCount, matchCount, segmentCount) in tests { 137 | checkChanges( 138 | from: source, 139 | to: target, 140 | changeCount: changeCount, 141 | matchCount: matchCount, 142 | segmentCount: segmentCount) 143 | } 144 | } 145 | 146 | func testFormChanges() { 147 | func checkFormChanges( 148 | _ difference: inout CollectionChanges, 149 | from source: C1, 150 | to target: C2, 151 | changeCount expectedChangeCount: Int, 152 | matchCount expectedMatchCount: Int, 153 | segmentCount expectedSegmentCount: Int 154 | ) where C1.Element == C2.Element, C1.Element : Equatable { 155 | let m = " from: \(source) to: \(target)" 156 | 157 | var changeCount = 0 158 | var matchCount = 0 159 | var segmentCount = 0 160 | difference.formChanges(from: source, to: target, by: ==) 161 | for segment in difference { 162 | switch segment { 163 | case let .removed(x): 164 | changeCount += source[x].count 165 | case let .inserted(y): 166 | changeCount += target[y].count 167 | case let .matched(x, y): 168 | XCTAssert( 169 | source[x].elementsEqual(target[y]), "elementsEqual" + m) 170 | matchCount += source[x].count 171 | } 172 | segmentCount += 1 173 | } 174 | 175 | XCTAssertEqual( 176 | changeCount, expectedChangeCount, "changeCount" + m) 177 | XCTAssertEqual( 178 | matchCount, expectedMatchCount, "matchCount" + m) 179 | XCTAssertEqual( 180 | segmentCount, expectedSegmentCount, "segmentCount" + m) 181 | } 182 | 183 | // array 184 | var difference = CollectionChanges() 185 | for (source, target, changeCount, matchCount, segmentCount) in tests { 186 | checkFormChanges( 187 | &difference, 188 | from: Array(source), 189 | to: Array(target), 190 | changeCount: changeCount, 191 | matchCount: matchCount, 192 | segmentCount: segmentCount) 193 | } 194 | 195 | // string 196 | var difference2 = CollectionChanges() 197 | for (source, target, changeCount, matchCount, segmentCount) in tests { 198 | checkFormChanges( 199 | &difference2, 200 | from: source, 201 | to: target, 202 | changeCount: changeCount, 203 | matchCount: matchCount, 204 | segmentCount: segmentCount) 205 | } 206 | } 207 | 208 | static var allTests = [ 209 | ("testChanges", testChanges), 210 | ("testFormChanges", testFormChanges), 211 | ] 212 | } 213 | -------------------------------------------------------------------------------- /Tests/DiffingTests/CommonPrefixTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Diffing 3 | 4 | class MismatchTests : XCTestCase { 5 | func testCommonPrefix() { 6 | func checkCommonPrefix< 7 | C1 : OrderedCollection, C2 : OrderedCollection, Expect : Sequence 8 | >(_ c1: C1, _ c2: C2, expect: Expect) 9 | where C1.Element == C2.Element, 10 | C1.Element == Expect.Element, 11 | C1.Element : Equatable 12 | { 13 | let p1 = c1.commonPrefix(with: c2, by: ==) 14 | let p2 = c2.commonPrefix(with: c1, by: ==) 15 | XCTAssert(p1.0.elementsEqual(p1.1)) 16 | XCTAssert(p2.0.elementsEqual(p2.1)) 17 | XCTAssert(p1.0.elementsEqual(p2.0)) 18 | XCTAssert(p1.0.elementsEqual(expect)) 19 | } 20 | 21 | checkCommonPrefix("", "", expect: "") 22 | checkCommonPrefix("abc", "abc", expect: "abc") 23 | checkCommonPrefix("abc", "ab", expect: "ab") 24 | checkCommonPrefix("abc", "abde", expect: "ab") 25 | checkCommonPrefix("xabc".dropFirst(), "abde", expect: "ab") 26 | } 27 | 28 | static var allTests = [ 29 | ("testCommonPrefix", testCommonPrefix), 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /Tests/DiffingTests/CountingIndexCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Diffing 3 | 4 | class CountingIndexCollectionTests : XCTestCase { 5 | func testCountingIndexCollection() { 6 | let empty = CountingIndexCollection([]) 7 | XCTAssertEqual(Array(empty.indices), []) 8 | XCTAssertEqual(empty.startIndex, CountingIndex(base: 0, offset: nil)) 9 | XCTAssertEqual(empty.endIndex, CountingIndex(base: 0, offset: nil)) 10 | 11 | let abc = CountingIndexCollection(["A", "B", "C"]) 12 | XCTAssertEqual( 13 | Array(abc.indices), 14 | [ 15 | CountingIndex(base: 0, offset: 0), 16 | CountingIndex(base: 1, offset: 1), 17 | CountingIndex(base: 2, offset: 2) 18 | ] 19 | ) 20 | XCTAssertEqual(abc.startIndex, CountingIndex(base: 0, offset: 0)) 21 | XCTAssertEqual(abc.endIndex, CountingIndex(base: 3, offset: nil)) 22 | 23 | let cba = CountingIndexCollection(["A", "B", "C"].reversed()) 24 | XCTAssertEqual( 25 | Array(cba.indices), 26 | [ 27 | CountingIndex(base: ReversedCollection.Index(3), offset: 0), 28 | CountingIndex(base: ReversedCollection.Index(2), offset: 1), 29 | CountingIndex(base: ReversedCollection.Index(1), offset: 2) 30 | ] 31 | ) 32 | XCTAssertEqual(cba.startIndex, CountingIndex(base: ReversedCollection.Index(3), offset: 0)) 33 | XCTAssertEqual(cba.endIndex, CountingIndex(base: ReversedCollection.Index(0), offset: nil)) 34 | } 35 | 36 | static var allTests = [ 37 | ("testCountingIndexCollection", testCountingIndexCollection), 38 | ] 39 | } -------------------------------------------------------------------------------- /Tests/DiffingTests/OrderedCollectionDifferenceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Diffing 3 | 4 | final class OrderedCollectionDifferenceTests: XCTestCase { 5 | func testEmpty() { 6 | guard let diff = OrderedCollectionDifference([]) else { 7 | XCTFail() 8 | return 9 | } 10 | XCTAssertEqual(0, diff.insertions.count) 11 | XCTAssertEqual(0, diff.removals.count) 12 | XCTAssertEqual(true, diff.isEmpty) 13 | 14 | var c = 0 15 | diff.forEach({ _ in c += 1 }) 16 | XCTAssertEqual(0, c) 17 | } 18 | 19 | // Create diffs with 100 changes ranging from 0 inserts and 100 removes to 20 | // 100 inserts and 0 removes and verify that the counts are accurate. 21 | func testInsertionCounts() { 22 | for i in 0..<100 { 23 | var c = [OrderedCollectionDifference.Change]() 24 | 25 | var insertions = 0 26 | for insertIndex in 0..(c) else { 38 | XCTFail() 39 | return 40 | } 41 | 42 | XCTAssertEqual(insertions, diff.insertions.count) 43 | XCTAssertEqual(removals, diff.removals.count) 44 | } 45 | } 46 | 47 | func testValidChanges() { 48 | // Base case: one insert and one remove with legal offsets 49 | XCTAssertNotNil(OrderedCollectionDifference.init([ 50 | .insert(offset: 0, element: 0, associatedWith: nil), 51 | .remove(offset: 0, element: 0, associatedWith: nil) 52 | ])) 53 | 54 | // Code coverage: 55 | // • non-first change .remove has legal associated offset 56 | // • non-first change .insert has legal associated offset 57 | XCTAssertNotNil(OrderedCollectionDifference.init([ 58 | .remove(offset: 1, element: 0, associatedWith: 0), 59 | .remove(offset: 0, element: 0, associatedWith: 1), 60 | .insert(offset: 0, element: 0, associatedWith: 1), 61 | .insert(offset: 1, element: 0, associatedWith: 0) 62 | ])) 63 | } 64 | 65 | func testInvalidChanges() { 66 | // Base case: two inserts sharing the same offset 67 | XCTAssertNil(OrderedCollectionDifference.init([ 68 | .insert(offset: 0, element: 0, associatedWith: nil), 69 | .insert(offset: 0, element: 0, associatedWith: nil) 70 | ])) 71 | 72 | // Base case: two removes sharing the same offset 73 | XCTAssertNil(OrderedCollectionDifference.init([ 74 | .remove(offset: 0, element: 0, associatedWith: nil), 75 | .remove(offset: 0, element: 0, associatedWith: nil) 76 | ])) 77 | 78 | // Base case: illegal insertion offset 79 | XCTAssertNil(OrderedCollectionDifference.init([ 80 | .insert(offset: -1, element: 0, associatedWith: nil) 81 | ])) 82 | 83 | // Base case: illegal remove offset 84 | XCTAssertNil(OrderedCollectionDifference.init([ 85 | .remove(offset: -1, element: 0, associatedWith: nil) 86 | ])) 87 | 88 | // Base case: two inserts sharing same associated offset 89 | XCTAssertNil(OrderedCollectionDifference.init([ 90 | .insert(offset: 0, element: 0, associatedWith: 0), 91 | .insert(offset: 1, element: 0, associatedWith: 0) 92 | ])) 93 | 94 | // Base case: two removes sharing same associated offset 95 | XCTAssertNil(OrderedCollectionDifference.init([ 96 | .remove(offset: 0, element: 0, associatedWith: 0), 97 | .remove(offset: 1, element: 0, associatedWith: 0) 98 | ])) 99 | 100 | // Base case: insert with illegal associated offset 101 | XCTAssertNil(OrderedCollectionDifference.init([ 102 | .insert(offset: 0, element: 0, associatedWith: -1) 103 | ])) 104 | 105 | // Base case: remove with illegal associated offset 106 | XCTAssertNil(OrderedCollectionDifference.init([ 107 | .remove(offset: 1, element: 0, associatedWith: -1) 108 | ])) 109 | 110 | // Code coverage: non-first change has illegal offset 111 | XCTAssertNil(OrderedCollectionDifference.init([ 112 | .remove(offset: 0, element: 0, associatedWith: nil), 113 | .insert(offset: -1, element: 0, associatedWith: nil) 114 | ])) 115 | 116 | // Code coverage: non-first change has illegal associated offset 117 | XCTAssertNil(OrderedCollectionDifference.init([ 118 | .remove(offset: 0, element: 0, associatedWith: nil), 119 | .insert(offset: 0, element: 0, associatedWith: -1) 120 | ])) 121 | } 122 | 123 | func testForEachOrder() { 124 | let safelyOrderedChanges: [OrderedCollectionDifference.Change] = [ 125 | .remove(offset: 2, element: 0, associatedWith: nil), 126 | .remove(offset: 1, element: 0, associatedWith: 0), 127 | .remove(offset: 0, element: 0, associatedWith: 1), 128 | .insert(offset: 0, element: 0, associatedWith: 1), 129 | .insert(offset: 1, element: 0, associatedWith: 0), 130 | .insert(offset: 2, element: 0, associatedWith: nil), 131 | ] 132 | let diff = OrderedCollectionDifference.init(safelyOrderedChanges)! 133 | var enumerationOrderedChanges = [OrderedCollectionDifference.Change]() 134 | diff.forEach { c in 135 | enumerationOrderedChanges.append(c) 136 | } 137 | XCTAssert(safelyOrderedChanges == enumerationOrderedChanges) 138 | } 139 | 140 | func testBadAssociations() { 141 | // .remove(1) → .insert(1) 142 | // ↑ ↓ 143 | // .insert(0) ← .remove(0) 144 | XCTAssertNil(OrderedCollectionDifference.init([ 145 | .remove(offset: 1, element: 0, associatedWith: 1), 146 | .remove(offset: 0, element: 0, associatedWith: 0), 147 | .insert(offset: 0, element: 0, associatedWith: 1), 148 | .insert(offset: 1, element: 0, associatedWith: 0) 149 | ])) 150 | 151 | // Coverage: duplicate remove offsets both with assocs 152 | XCTAssertNil(OrderedCollectionDifference.init([ 153 | .remove(offset: 0, element: 0, associatedWith: 1), 154 | .remove(offset: 0, element: 0, associatedWith: 0), 155 | ])) 156 | 157 | // Coverage: duplicate insert assocs 158 | XCTAssertNil(OrderedCollectionDifference.init([ 159 | .insert(offset: 0, element: 0, associatedWith: 1), 160 | .insert(offset: 1, element: 0, associatedWith: 1), 161 | ])) 162 | } 163 | 164 | // Full-coverage test for OrderedCollectionDifference.Change.==() 165 | func testChangeEquality() { 166 | // Differs by type: 167 | XCTAssertFalse( 168 | OrderedCollectionDifference.Change.insert(offset: 0, element: 0, associatedWith: 0) == 169 | OrderedCollectionDifference.Change.remove(offset: 0, element: 0, associatedWith: 0) 170 | ) 171 | 172 | // Differs by type in the other direction: 173 | XCTAssertFalse( 174 | OrderedCollectionDifference.Change.remove(offset: 0, element: 0, associatedWith: 0) == 175 | OrderedCollectionDifference.Change.insert(offset: 0, element: 0, associatedWith: 0) 176 | ) 177 | 178 | // Insert differs by offset 179 | XCTAssertFalse( 180 | OrderedCollectionDifference.Change.insert(offset: 0, element: 0, associatedWith: 0) == 181 | OrderedCollectionDifference.Change.insert(offset: 1, element: 0, associatedWith: 0) 182 | ) 183 | 184 | // Insert differs by element 185 | XCTAssertFalse( 186 | OrderedCollectionDifference.Change.insert(offset: 0, element: 0, associatedWith: 0) == 187 | OrderedCollectionDifference.Change.insert(offset: 0, element: 1, associatedWith: 0) 188 | ) 189 | 190 | // Insert differs by association 191 | XCTAssertFalse( 192 | OrderedCollectionDifference.Change.insert(offset: 0, element: 0, associatedWith: 0) == 193 | OrderedCollectionDifference.Change.insert(offset: 0, element: 0, associatedWith: 1) 194 | ) 195 | 196 | // Remove differs by offset 197 | XCTAssertFalse( 198 | OrderedCollectionDifference.Change.remove(offset: 0, element: 0, associatedWith: 0) == 199 | OrderedCollectionDifference.Change.remove(offset: 1, element: 0, associatedWith: 0) 200 | ) 201 | 202 | // Remove differs by element 203 | XCTAssertFalse( 204 | OrderedCollectionDifference.Change.remove(offset: 0, element: 0, associatedWith: 0) == 205 | OrderedCollectionDifference.Change.remove(offset: 0, element: 1, associatedWith: 0) 206 | ) 207 | 208 | // Remove differs by association 209 | XCTAssertFalse( 210 | OrderedCollectionDifference.Change.remove(offset: 0, element: 0, associatedWith: 0) == 211 | OrderedCollectionDifference.Change.remove(offset: 0, element: 0, associatedWith: 1) 212 | ) 213 | } 214 | 215 | func testHashableConformance() { 216 | let _ = Set>(); 217 | } 218 | 219 | func testMoveInference() { 220 | let n = OrderedCollectionDifference.init([ 221 | .insert(offset: 3, element: "Sike", associatedWith: nil), 222 | .insert(offset: 4, element: "Sike", associatedWith: nil), 223 | .insert(offset: 2, element: "Hello", associatedWith: nil), 224 | .remove(offset: 6, element: "Hello", associatedWith: nil), 225 | .remove(offset: 8, element: "Goodbye", associatedWith: nil), 226 | .remove(offset: 9, element: "Sike", associatedWith: nil), 227 | ]) 228 | let w = OrderedCollectionDifference.init([ 229 | .insert(offset: 3, element: "Sike", associatedWith: nil), 230 | .insert(offset: 4, element: "Sike", associatedWith: nil), 231 | .insert(offset: 2, element: "Hello", associatedWith: 6), 232 | .remove(offset: 6, element: "Hello", associatedWith: 2), 233 | .remove(offset: 8, element: "Goodbye", associatedWith: nil), 234 | .remove(offset: 9, element: "Sike", associatedWith: nil), 235 | ]) 236 | XCTAssertEqual(w, n?.inferringMoves()) 237 | } 238 | 239 | func testDemo3Way() { 240 | let base = "Is\nit\ntime\nalready?" 241 | let theirs = "Hi\nthere\nis\nit\ntime\nalready?" 242 | let mine = "Is\nit\nreview\ntime\nalready?" 243 | 244 | // Split the contents of the sources into lines 245 | let baseLines = base.components(separatedBy: "\n") 246 | let theirLines = theirs.components(separatedBy: "\n") 247 | let myLines = mine.components(separatedBy: "\n") 248 | 249 | // Create a difference from base to theirs 250 | let diff = theirLines.difference(from:baseLines) 251 | 252 | // Apply it to mine, if possible 253 | guard let patchedLines = myLines.applying(diff) else { 254 | print("Merge conflict applying patch, manual merge required") 255 | return 256 | } 257 | 258 | // Reassemble the result 259 | let patched = patchedLines.joined(separator: "\n") 260 | XCTAssertEqual(patched, "Hi\nthere\nis\nit\nreview\ntime\nalready?") 261 | print(patched) 262 | } 263 | 264 | func testDemoReverse() { 265 | let diff = OrderedCollectionDifference([])! 266 | let reversed = OrderedCollectionDifference( 267 | diff.map({(change) -> OrderedCollectionDifference.Change in 268 | switch change { 269 | case .insert(offset: let o, element: let e, associatedWith: let a): 270 | return .remove(offset: o, element: e, associatedWith: a) 271 | case .remove(offset: let o, element: let e, associatedWith: let a): 272 | return .insert(offset: o, element: e, associatedWith: a) 273 | } 274 | }) 275 | )! 276 | print(reversed) 277 | } 278 | 279 | func testApplyByEnumeration() { 280 | let base = "Is\nit\ntime\nalready?" 281 | let theirs = "Hi\nthere\nis\nit\ntime\nalready?" 282 | 283 | // Split the contents of the sources into lines 284 | var arr = base.components(separatedBy: "\n") 285 | let theirLines = theirs.components(separatedBy: "\n") 286 | 287 | // Create a difference from base to theirs 288 | let diff = theirLines.difference(from:arr) 289 | 290 | for c in diff { 291 | switch c { 292 | case .remove(offset: let o, element: _, associatedWith: _): 293 | arr.remove(at: o) 294 | case .insert(offset: let o, element: let e, associatedWith: _): 295 | arr.insert(e, at: o) 296 | } 297 | } 298 | 299 | XCTAssertEqual(arr, theirLines) 300 | } 301 | 302 | static var allTests = [ 303 | ("testEmpty", testEmpty), 304 | ("testInsertionCounts", testInsertionCounts), 305 | ("testValidChanges", testValidChanges), 306 | ("testInvalidChanges", testInvalidChanges), 307 | ("testForEachOrder", testForEachOrder), 308 | ("testBadAssociations", testBadAssociations), 309 | ("testChangeEquality", testChangeEquality), 310 | ("testHashableConformance", testHashableConformance), 311 | ("testMoveInference", testMoveInference), 312 | ("testDemo3Way", testDemo3Way), 313 | ("testDemoReverse", testDemoReverse), 314 | ("testApplyByEnumeration", testApplyByEnumeration), 315 | ] 316 | } 317 | -------------------------------------------------------------------------------- /Tests/DiffingTests/OrderedCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Diffing 3 | 4 | final class OrderedCollectionTests: XCTestCase { 5 | func testEmpty() { 6 | let a = [Int]() 7 | let b = [Int]() 8 | let diff = b.difference(from: a) 9 | 10 | XCTAssertEqual(diff, a.difference(from: a)) 11 | XCTAssertEqual(true, diff.isEmpty) 12 | } 13 | 14 | func testDifference() { 15 | let expectedChanges: [( 16 | source: [String], 17 | target: [String], 18 | changes: [OrderedCollectionDifference.Change], 19 | line: UInt 20 | )] = [ 21 | (target: 22 | ["Hannukah", "Menorah", "Dreidel", 23 | "Xmas", "Tree", "Lights", "Presents", 24 | "New Years", "Champagne"], 25 | source: 26 | ["Hannukah", "Menorah", "Dreidel", 27 | "Xmas", "Tree", "Lights", "Presents", 28 | "New Years", "Champagne"], 29 | changes: [], 30 | line: #line), 31 | 32 | (target: 33 | ["Hannukah", "Menorah", "Dreidel", 34 | "Xmas", "Tree", "Lights", "Presents", 35 | "New Years", "Champagne"], 36 | source: 37 | ["Hannukah", "Menorah", "Dreidel", 38 | "Xmas", "Tree", "Presents", 39 | "New Years", "Champagne"], 40 | changes: [ 41 | .remove(offset: 5, element: "Lights", associatedWith: nil) 42 | ], 43 | line: #line), 44 | 45 | (target: 46 | ["Hannukah", "Menorah", "Dreidel", 47 | "Xmas", "Tree", "Lights", "Presents", 48 | "New Years", "Champagne"], 49 | source: 50 | ["Hannukah", "Menorah", "Dreidel", "Gelt", 51 | "Xmas", "Tree", "Lights", "Presents", 52 | "New Years", "Champagne"], 53 | changes: [ 54 | .insert(offset: 3, element: "Gelt", associatedWith: nil) 55 | ], 56 | line: #line), 57 | 58 | (target: 59 | ["Hannukah", "Menorah", "Dreidel", 60 | "Xmas", "Tree", "Lights", "Presents", 61 | "New Years", "Champagne"], 62 | source: 63 | ["Hannukah", "Menorah", "Dreidel", 64 | "Xmas", "Presents", "Tree", "Lights", 65 | "New Years", "Champagne"], 66 | changes: [ 67 | .remove(offset: 6, element: "Presents", associatedWith: 4), 68 | .insert(offset: 4, element: "Presents", associatedWith: 6) 69 | ], 70 | line: #line), 71 | 72 | (target: 73 | ["Hannukah", "Menorah", "Dreidel", 74 | "Xmas", "Tree", "Lights", "Presents", 75 | "New Years", "Champagne"], 76 | source: 77 | ["Hannukah", "Menorah", "Dreidel", 78 | "Xmas", "Lights", "Presents", "Tree", 79 | "New Years", "Champagne"], 80 | changes: [ 81 | .remove(offset: 4, element: "Tree", associatedWith: 6), 82 | .insert(offset: 6, element: "Tree", associatedWith: 4) 83 | ], 84 | line: #line), 85 | 86 | (target: 87 | ["Hannukah", "Menorah", "Dreidel", 88 | "Xmas", "Tree", "Lights", "Presents", 89 | "New Years", "Champagne"], 90 | source: 91 | ["Hannukah", "Menorah", "Dreidel", "Presents", 92 | "Xmas", "Tree", "Lights", 93 | "New Years", "Champagne"], 94 | changes: [ 95 | .remove(offset: 6, element: "Presents", associatedWith: 3), 96 | .insert(offset: 3, element: "Presents", associatedWith: 6) 97 | ], 98 | line: #line), 99 | 100 | (target: 101 | ["Hannukah", "Menorah", "Dreidel", 102 | "Xmas", "Tree", "Lights", "Presents", 103 | "New Years", "Champagne"], 104 | source: 105 | ["Hannukah", "Menorah", "Dreidel", 106 | "Xmas", "Tree", "Lights", 107 | "New Years", "Champagne", "Presents"], 108 | changes: [ 109 | .remove(offset: 6, element: "Presents", associatedWith: 8), 110 | .insert(offset: 8, element: "Presents", associatedWith: 6) 111 | ], 112 | line: #line), 113 | 114 | (target: 115 | ["Hannukah", "Menorah", "Dreidel", 116 | "Xmas", "Tree", "Lights", "Presents", 117 | "New Years", "Champagne"], 118 | source: 119 | ["Xmas", "Tree", "Lights", "Presents", 120 | "New Years", "Champagne"], 121 | changes: [ 122 | .remove(offset: 2, element: "Dreidel", associatedWith: nil), 123 | .remove(offset: 1, element: "Menorah", associatedWith: nil), 124 | .remove(offset: 0, element: "Hannukah", associatedWith: nil) 125 | ], 126 | line: #line), 127 | 128 | (target: 129 | ["Hannukah", "Menorah", "Dreidel", 130 | "Xmas", "Tree", "Lights", "Presents"], 131 | source: 132 | ["Hannukah", "Menorah", "Dreidel", 133 | "Xmas", "Tree", "Lights", "Presents", 134 | "New Years", "Champagne"], 135 | changes: [ 136 | .insert(offset: 7, element: "New Years", associatedWith: nil), 137 | .insert(offset: 8, element: "Champagne", associatedWith: nil) 138 | ], 139 | line: #line), 140 | 141 | (target: 142 | ["Hannukah", "Menorah", "Dreidel", 143 | "Xmas", "Tree", "Lights", "Presents", 144 | "New Years", "Champagne"], 145 | source: 146 | ["New Years", "Champagne", 147 | "Hannukah", "Menorah", "Dreidel", 148 | "Xmas", "Tree", "Lights", "Presents"], 149 | changes: [ 150 | .remove(offset: 8, element: "Champagne", associatedWith: 1), 151 | .remove(offset: 7, element: "New Years", associatedWith: 0), 152 | .insert(offset: 0, element: "New Years", associatedWith: 7), 153 | .insert(offset: 1, element: "Champagne", associatedWith: 8) 154 | ], 155 | line: #line), 156 | 157 | (target: 158 | ["Hannukah", "Menorah", "Dreidel", 159 | "Xmas", "Tree", "Lights", "Presents", 160 | "New Years", "Champagne"], 161 | source: 162 | ["Xmas", "Tree", "Lights", "Presents", 163 | "New Years", "Champagne", 164 | "Hannukah", "Menorah", "Dreidel"], 165 | changes: [ 166 | .remove(offset: 2, element: "Dreidel", associatedWith: 8), 167 | .remove(offset: 1, element: "Menorah", associatedWith: 7), 168 | .remove(offset: 0, element: "Hannukah", associatedWith: 6), 169 | .insert(offset: 6, element: "Hannukah", associatedWith: 0), 170 | .insert(offset: 7, element: "Menorah", associatedWith: 1), 171 | .insert(offset: 8, element: "Dreidel", associatedWith: 2) 172 | ], 173 | line: #line), 174 | 175 | (target: 176 | ["Hannukah", "Menorah", "Dreidel", "Presents", 177 | "Xmas", "Tree", "Lights", 178 | "New Years", "Champagne"], 179 | source: 180 | ["Xmas", "Tree", "Lights", "Presents", 181 | "New Years", "Champagne"], 182 | changes: [ 183 | .remove(offset: 3, element: "Presents", associatedWith: 3), 184 | .remove(offset: 2, element: "Dreidel", associatedWith: nil), 185 | .remove(offset: 1, element: "Menorah", associatedWith: nil), 186 | .remove(offset: 0, element: "Hannukah", associatedWith: nil), 187 | .insert(offset: 3, element: "Presents", associatedWith: 3) 188 | ], 189 | line: #line), 190 | 191 | (target: 192 | ["Hannukah", "Menorah", "Dreidel", 193 | "Xmas", "Tree", "Lights", "Presents"], 194 | source: 195 | ["Hannukah", "Menorah", "Dreidel", 196 | "Xmas", "Tree", "Presents", 197 | "New Years", "Champagne", "Lights"], 198 | changes: [ 199 | .remove(offset: 5, element: "Lights", associatedWith: 8), 200 | .insert(offset: 6, element: "New Years", associatedWith: nil), 201 | .insert(offset: 7, element: "Champagne", associatedWith: nil), 202 | .insert(offset: 8, element: "Lights", associatedWith: 5) 203 | ], 204 | line: #line), 205 | 206 | (target: 207 | ["Hannukah", "Menorah", "Dreidel", 208 | "Xmas", "Tree", "Lights", "Presents", 209 | "New Years", "Champagne"], 210 | source: 211 | ["Hannukah", "Menorah", "Dreidel", 212 | "Xmas", "Tree", "Lights", "Presents", 213 | "New Years"], 214 | changes: [ 215 | .remove(offset: 8, element: "Champagne", associatedWith: nil) 216 | ], 217 | line: #line), 218 | 219 | (target: 220 | ["Hannukah", "Menorah", "Dreidel", "Presents", 221 | "Xmas", "Tree", "Lights", "Presents", 222 | "New Years", "Champagne", "Presents"], 223 | source: 224 | ["Hannukah", "Menorah", "Dreidel", "Presents", 225 | "Xmas", "Tree", "Lights", "Presents", 226 | "New Years", "Champagne", "Presents"], 227 | changes: [], 228 | line: #line), 229 | 230 | (target: 231 | ["Hannukah", "Menorah", "Dreidel", "Presents", 232 | "Xmas", "Tree", "Lights", "Presents", 233 | "New Years", "Champagne", "Presents"], 234 | source: 235 | ["Hannukah", "Menorah", "Dreidel", 236 | "Xmas", "Tree", "Lights", 237 | "New Years", "Champagne", "Presents"], 238 | changes: [ 239 | .remove(offset: 7, element: "Presents", associatedWith: nil), 240 | .remove(offset: 3, element: "Presents", associatedWith: nil) 241 | ], 242 | line: #line), 243 | 244 | (target: 245 | ["Hannukah", "Menorah", "Dreidel", 246 | "Xmas", "Tree", "Lights", 247 | "New Years", "Champagne", "Presents"], 248 | source: 249 | ["Hannukah", "Menorah", "Dreidel", "Presents", 250 | "Xmas", "Tree", "Lights", "Presents", 251 | "New Years", "Champagne", "Presents"], 252 | changes: [ 253 | .insert(offset: 3, element: "Presents", associatedWith: nil), 254 | .insert(offset: 7, element: "Presents", associatedWith: nil) 255 | ], 256 | line: #line), 257 | 258 | (target: 259 | ["Hannukah", "Menorah", "Dreidel", "Presents", 260 | "Xmas", "Tree", "Lights", 261 | "New Years", "Champagne", "Presents"], 262 | source: 263 | ["Hannukah", "Menorah", "Dreidel", 264 | "Xmas", "Tree", "Lights", "Presents", 265 | "New Years", "Champagne", "Presents"], 266 | changes: [ 267 | .remove(offset: 3, element: "Presents", associatedWith: 6), 268 | .insert(offset: 6, element: "Presents", associatedWith: 3) 269 | ], 270 | line: #line), 271 | 272 | (target: 273 | ["Hannukah", "Menorah", 274 | "Xmas", "Tree", "Lights", "Presents", 275 | "New Years", "Champagne", 276 | "Hannukah", "Dreidel"], 277 | source: 278 | ["Hannukah", "Menorah", 279 | "Xmas", "Tree", "Lights", "Presents", 280 | "New Years", "Champagne", 281 | "Hannukah", "Dreidel"], 282 | changes: [], 283 | line: #line), 284 | 285 | (target: 286 | ["Hannukah", "Menorah", 287 | "Xmas", "Tree", "Lights", "Presents", 288 | "New Years", "Champagne", 289 | "Hannukah", "Dreidel"], 290 | source: 291 | ["Hannukah", "Menorah", 292 | "Xmas", "Tree", "Lights", "Presents", 293 | "New Years", "Champagne"], 294 | changes: [ 295 | .remove(offset: 9, element: "Dreidel", associatedWith: nil), 296 | .remove(offset: 8, element: "Hannukah", associatedWith: nil) 297 | ], 298 | line: #line), 299 | 300 | (target: 301 | ["Hannukah", "Menorah", 302 | "Xmas", "Tree", "Lights", "Presents", 303 | "New Years", "Champagne"], 304 | source: 305 | ["Hannukah", "Menorah", 306 | "Xmas", "Tree", "Lights", "Presents", 307 | "New Years", "Champagne", 308 | "Hannukah", "Dreidel"], 309 | changes: [ 310 | .insert(offset: 8, element: "Hannukah", associatedWith: nil), 311 | .insert(offset: 9, element: "Dreidel", associatedWith: nil) 312 | ], 313 | line: #line), 314 | 315 | (target: 316 | ["Hannukah", "Menorah", 317 | "Xmas", "Tree", "Lights", "Presents", 318 | "New Years", "Champagne", 319 | "Hannukah", "Dreidel"], 320 | source: 321 | ["Xmas", "Tree", "Lights", "Presents", 322 | "Hannukah", "Menorah", 323 | "New Years", "Champagne", 324 | "Hannukah", "Dreidel"], 325 | changes: [ 326 | .remove(offset: 1, element: "Menorah", associatedWith: 5), 327 | .remove(offset: 0, element: "Hannukah", associatedWith: 4), 328 | .insert(offset: 4, element: "Hannukah", associatedWith: 0), 329 | .insert(offset: 5, element: "Menorah", associatedWith: 1) 330 | ], 331 | line: #line), 332 | ] 333 | 334 | for (source, target, expected, line) in expectedChanges { 335 | let actual = source.difference(from: target).inferringMoves() 336 | XCTAssert( 337 | actual == OrderedCollectionDifference(expected), 338 | "\(actual) != \(expected)", 339 | line: line) 340 | } 341 | } 342 | 343 | static var allTests = [ 344 | ("testEmpty", testEmpty), 345 | ("testDifference", testDifference), 346 | ] 347 | } 348 | -------------------------------------------------------------------------------- /Tests/DiffingTests/RangeReplaceableCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Diffing 3 | 4 | class RangeReplaceableCollectionTests : XCTestCase { 5 | 6 | func testBoundaryConditions() { 7 | let a = [1, 2, 3, 4, 5, 6, 7, 8] 8 | for removeMiddle in [false, true] { 9 | for insertMiddle in [false, true] { 10 | for removeLast in [false, true] { 11 | for insertLast in [false, true] { 12 | for removeFirst in [false, true] { 13 | for insertFirst in [false, true] { 14 | var b = a 15 | 16 | // Prepare b 17 | if removeMiddle { b.remove(at: 4) } 18 | if insertMiddle { b.insert(10, at: 4) } 19 | if removeLast { b.removeLast() } 20 | if insertLast { b.append(11) } 21 | if removeFirst { b.removeFirst() } 22 | if insertFirst { b.insert(12, at: 0) } 23 | 24 | // Generate diff 25 | let diff = b.difference(from: a) 26 | 27 | // Validate application 28 | XCTAssertEqual(b, a.applying(diff)!) 29 | }}}}}} 30 | } 31 | 32 | func testFuzzer() { 33 | func makeArray() -> [UInt32] { 34 | var arr = [UInt32]() 35 | for _ in 0.. 0 { 46 | print(""" 47 | // repro: 48 | let a = \(a) 49 | let b = \(b) 50 | let d = b.difference(from: a) 51 | XCTAssertEqual(b, a.applying(d)) 52 | """) 53 | break 54 | } 55 | } 56 | } 57 | 58 | static var allTests = [ 59 | ("testBoundaryConditions", testBoundaryConditions), 60 | ("testFuzzer", testFuzzer), 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /Tests/DiffingTests/TriangularMatrixTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Diffing 3 | 4 | class TriangularMatrixTests : XCTestCase { 5 | func testLowerTriangularMatrix() { 6 | var m = LowerTriangularMatrix() 7 | m.appendRow(repeating: 1) 8 | m.appendRow(repeating: 2) 9 | m.appendRow(repeating: 3) 10 | m.appendRow(repeating: 4) 11 | 12 | XCTAssertEqual(Array(m.rowMajorOrder), [ 13 | 1, 14 | 2, 2, 15 | 3, 3, 3, 16 | 4, 4, 4, 4 17 | ]) 18 | 19 | for i in 0..<4 { 20 | XCTAssertEqual(m[i, 0], i + 1) 21 | XCTAssertEqual(m[i, i], i + 1) 22 | } 23 | } 24 | 25 | static var allTests = [ 26 | ("testLowerTriangularMatrix", testLowerTriangularMatrix), 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /Tests/DiffingTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(CollectionChangesTests.allTests), 7 | testCase(CommonPrefixTests.allTests), 8 | testCase(OrderedCollectionDifferenceTests.allTests), 9 | testCase(OrderedCollectionTests.allTests), 10 | testCase(RangeReplaceableCollectionTests.allTests), 11 | testCase(TriangularMatrixTests.allTests), 12 | ] 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DiffingTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += DiffingTests.allTests() 7 | XCTMain(tests) --------------------------------------------------------------------------------