├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SortedDifference │ └── SortedDifference.swift └── Tests ├── LinuxMain.swift └── SortedDifferenceTests ├── SortedDifferenceTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | #### 1.x Releases 7 | 8 | - [1.1.0](#110) 9 | - [1.0.0](#100) 10 | 11 | 12 | ## 1.1.0 13 | 14 | Released March 1, 2020 15 | 16 | **New**: the precondition that ids of left and right sequences are strictly increasing is checked in debug builds. 17 | 18 | ## 1.0.0 19 | 20 | Released February 29, 2020 21 | 22 | **Initial release** 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Gwendal Roué 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 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: "SortedDifference", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SortedDifference", 12 | targets: ["SortedDifference"]), 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: "SortedDifference", 23 | dependencies: []), 24 | .testTarget( 25 | name: "SortedDifferenceTests", 26 | dependencies: ["SortedDifference"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SortedDifference 2 | 3 | This Swift package defines the `SortedDifference` sequence, which computes the difference between two sequences of identifiable elements. 4 | 5 | --- 6 | 7 | **Latest release**: [version 1.1.0](https://github.com/groue/SortedDifference/tree/1.1.0) (March 1, 2020) • [Release Notes] 8 | 9 | **Requirements**: Swift 5.1+, Xcode 11.3+ 10 | 11 | **Contact**: Report bugs and ask questions in [Github issues](https://github.com/groue/SortedDifference/issues). 12 | 13 | --- 14 | 15 | - [Motivation] 16 | - [Reference] 17 | 18 | ## Motivation 19 | 20 | SortedDifference eases, for example, the synchronization of a server payload and a local database. 21 | 22 | The sample code below synchronizes the list of database players with the list of players loaded from a remote API server. It uses [Alamofire](https://github.com/Alamofire/Alamofire) and [GRDB](https://github.com/groue/GRDB.swift). 23 | 24 | It has a low complexity, and performs as little database I/O as possible: 25 | 26 | - The api players are sorted in O(n log n). 27 | - Database players, sorted by primary key, are fetched in a single efficient SQL request. 28 | - The iteration of SortedDifference is O(n). 29 | - SortedDifference accepts sequences of different types, which means that it can handle different types for api and database players, and that we do not have to perform conversions from one to the other unless strictly necessary. 30 | - Each insertion and deletion runs one SQL request. 31 | - Each update only runs an SQL request if there are actual changes. 32 | - The whole batch is wrapped in a single database transaction, for guaranteed database integrity, and maximum speed. 33 | - When database and server are already synchronized, a single SQL request is executed, and no write happens in the database. 34 | 35 | ```swift 36 | struct APIPlayer: Decodable, Identifiable { ... } 37 | struct DatabasePlayer: FetchableRecord, PersistableRecord, Identifiable { ... } 38 | 39 | extension DatabasePlayer { 40 | init(_ player: APIPlayer) { ... } 41 | } 42 | 43 | // Load all api players 44 | AF.request("https://example.com/players").responseDecodable(of: [APIPlayer].self) { response in 45 | do { 46 | // Sort api players by id, as required by SortedDifference 47 | let apiPlayers = try response.result.get().sorted(by: { $0.id < $1.id }) 48 | 49 | // Synchronize players in a database transaction 50 | try database.write { db in 51 | // Fetch database players, sorted by id, as required by SortedDifference 52 | let dbPlayers = try DatabasePlayer.orderByPrimaryKey().fetchAll(db) 53 | 54 | for change in SortedDifference(left: dbPlayers, right: apiPlayers) { 55 | switch change { 56 | case let .left(dbPlayer): 57 | // Player exists in the database, but not on the server: delete it 58 | try dbPlayer.delete(db) 59 | 60 | case let .right(apiPlayer): 61 | // Player exists on the server, but not in the database: insert it 62 | let newPlayer = DatabasePlayer(apiPlayer) 63 | try newPlayer.insert(db) 64 | 65 | case let .common(dbPlayer, apiPlayer): 66 | // Player exists on the server and in the database: update if needed 67 | let newPlayer = DatabasePlayer(apiPlayer) 68 | try newPlayer.updateChanges(db, from: dbPlayer) 69 | } 70 | } 71 | } 72 | } catch { 73 | // handle network or database error 74 | } 75 | } 76 | ``` 77 | 78 | 79 | ## Reference 80 | 81 | SortedDifference comes with a general initializer, and two convenience initializers. 82 | 83 | All initializers share a common precondition: **ids of both left and right sequences must be strictly increasing**. In other words, sequences must be sorted by id, and ids must be unique in each sequence. 84 | 85 | For extra safety, this precondition is checked in debug builds (as a Swift assertion). If the precondition is not honored, the behavior of SortedDifference is undefined. 86 | 87 | The general initializer is the less constrained: it lets you provide closures that return identifiers of both left and right elements: 88 | 89 | ```swift 90 | struct SortedDifference: Sequence where 91 | LeftSequence: Sequence, 92 | RightSequence: Sequence, 93 | ID: Comparable 94 | { 95 | init( 96 | left: LeftSequence, 97 | identifiedBy leftID: @escaping (LeftSequence.Element) -> ID, 98 | right: RightSequence, 99 | identifiedBy rightID: @escaping (RightSequence.Element) -> ID) 100 | } 101 | 102 | // Prints: 103 | // - common(Left(id: 1), Right(id: 1)) 104 | // - left(Left(id: 2)) 105 | struct Left { var id: Int } 106 | struct Right { var id: Int } 107 | for change in SortedDifference( 108 | left: [Left(id: 1), Left(id: 2)], 109 | identifiedBy: { $0.id }, 110 | right: [Right(id: 1)], 111 | identifiedBy: { $0.id }) 112 | { 113 | print(change) 114 | } 115 | ``` 116 | 117 | There is a convenience initializer for sequences of Identifiable types: 118 | 119 | ```swift 120 | @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) 121 | extension SortedDifference where 122 | LeftSequence.Element: Identifiable, 123 | RightSequence.Element: Identifiable, 124 | LeftSequence.Element.ID == ID, 125 | RightSequence.Element.ID == ID 126 | { 127 | init(left: LeftSequence, right: RightSequence) 128 | } 129 | 130 | // Prints: 131 | // - common(Left(id: 1), Right(id: 1)) 132 | // - left(Left(id: 2)) 133 | struct Left: Identifiable { var id: Int } 134 | struct Right: Identifiable { var id: Int } 135 | for change in SortedDifference( 136 | left: [Left(id: 1), Left(id: 2)], 137 | right: [Right(id: 1)]) 138 | { 139 | print(change) 140 | } 141 | ``` 142 | 143 | There is a convenience initializer for sequences of Comparable types: 144 | 145 | ```swift 146 | extension SortedDifference where 147 | LeftSequence.Element: Comparable, 148 | RightSequence.Element == LeftSequence.Element, 149 | LeftSequence.Element == ID 150 | { 151 | init(left: LeftSequence, right: RightSequence) 152 | } 153 | 154 | // Prints: 155 | // - common(1, 1) 156 | // - left(2) 157 | for change in SortedDifference(left: [1, 2], right: [1]) { 158 | print(change) 159 | } 160 | ``` 161 | 162 | [Release Notes]: CHANGELOG.md 163 | [Motivation]: #motivation 164 | [Reference]: #reference 165 | -------------------------------------------------------------------------------- /Sources/SortedDifference/SortedDifference.swift: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Gwendal Roué 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | /// Given two sequences (left and right), this sequence tells whether elements 23 | /// are only found on the left, on the right, or on both sides. 24 | public struct SortedDifference: Sequence where 25 | LeftSequence: Sequence, 26 | RightSequence: Sequence, 27 | ID: Comparable 28 | { 29 | private let left: LeftSequence 30 | private let right: RightSequence 31 | private let lID: (LeftSequence.Element) -> ID 32 | private let rID: (RightSequence.Element) -> ID 33 | 34 | /// Given two sequences (left and right), returns a sequence which tells 35 | /// whether elements are only found on the left, on the right, or on 36 | /// both sides. 37 | /// 38 | /// Both input sequences do not have to share the same element type, but 39 | /// their elements must share a common comparable id. 40 | /// 41 | /// For example: 42 | /// 43 | /// // Prints: 44 | /// // - common(Left(id: 1), Right(id: 1)) 45 | /// // - left(Left(id: 2)) 46 | /// struct Left { var id: Int } 47 | /// struct Right { var id: Int } 48 | /// for change in SortedDifference( 49 | /// left: [Left(id: 1), Left(id: 2)], 50 | /// identifiedBy: { $0.id }, 51 | /// right: [Right(id: 1)], 52 | /// identifiedBy: { $0.id }) 53 | /// { 54 | /// print(change) 55 | /// } 56 | /// 57 | /// - precondition: Both input sequences must be sorted by id. 58 | /// - precondition: Ids must be unique in each sequences. 59 | /// - parameters: 60 | /// - left: The left sequence. 61 | /// - right: The right sequence. 62 | /// - leftID: A function that returns the id of a left element. 63 | /// - rightID: A function that returns the id of a right element. 64 | public init( 65 | left: LeftSequence, 66 | identifiedBy leftID: @escaping (LeftSequence.Element) -> ID, 67 | right: RightSequence, 68 | identifiedBy rightID: @escaping (RightSequence.Element) -> ID) 69 | { 70 | self.left = left 71 | self.right = right 72 | self.lID = leftID 73 | self.rID = rightID 74 | } 75 | 76 | public func makeIterator() -> Iterator { 77 | Iterator( 78 | lIterator: left.makeIterator(), 79 | rIterator: right.makeIterator(), 80 | lID: lID, 81 | rID: rID) 82 | } 83 | 84 | public struct Iterator: IteratorProtocol { 85 | private var lIterator: LeftSequence.Iterator 86 | private var rIterator: RightSequence.Iterator 87 | private var lOpt: LeftSequence.Element? 88 | private var rOpt: RightSequence.Element? 89 | private let lID: (LeftSequence.Element) -> ID 90 | private let rID: (RightSequence.Element) -> ID 91 | 92 | init( 93 | lIterator: LeftSequence.Iterator, 94 | rIterator: RightSequence.Iterator, 95 | lID: @escaping (LeftSequence.Element) -> ID, 96 | rID: @escaping (RightSequence.Element) -> ID) 97 | { 98 | self.lIterator = lIterator 99 | self.rIterator = rIterator 100 | self.lID = lID 101 | self.rID = rID 102 | self.lOpt = self.lIterator.next() 103 | self.rOpt = self.rIterator.next() 104 | } 105 | 106 | public mutating func next() -> SortedDifferenceChange? { 107 | switch (lOpt, rOpt) { 108 | case let (lElem?, rElem?): 109 | let (lID, rID) = (self.lID(lElem), self.rID(rElem)) 110 | if lID > rID { 111 | rOpt = rIterator.next() 112 | assertIncreasingRightId(from: rID) 113 | return .right(rElem) 114 | } else if lID == rID { 115 | lOpt = lIterator.next() 116 | rOpt = rIterator.next() 117 | assertIncreasingLeftId(from: lID) 118 | assertIncreasingRightId(from: rID) 119 | return .common(lElem, rElem) 120 | } else { 121 | lOpt = lIterator.next() 122 | assertIncreasingLeftId(from: lID) 123 | return .left(lElem) 124 | } 125 | case let (nil, rElem?): 126 | rOpt = rIterator.next() 127 | assertIncreasingRightId(from: rID(rElem)) 128 | return .right(rElem) 129 | case let (lElem?, nil): 130 | lOpt = lIterator.next() 131 | assertIncreasingLeftId(from: lID(lElem)) 132 | return .left(lElem) 133 | case (nil, nil): 134 | return nil 135 | } 136 | } 137 | 138 | @inline(__always) 139 | private func assertIncreasingLeftId(from previousID: @autoclosure () -> ID) { 140 | assert(lOpt.map { previousID() < lID($0) } ?? true, "Ids of left sequence elements must be stricly increasing") 141 | } 142 | 143 | @inline(__always) 144 | private func assertIncreasingRightId(from previousID: @autoclosure () -> ID) { 145 | assert(rOpt.map { previousID() < rID($0) } ?? true, "Ids of right sequence elements must be stricly increasing") 146 | } 147 | } 148 | } 149 | 150 | @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) 151 | extension SortedDifference where 152 | LeftSequence.Element: Identifiable, 153 | RightSequence.Element: Identifiable, 154 | LeftSequence.Element.ID == ID, 155 | RightSequence.Element.ID == ID 156 | { 157 | /// Given two sequences (left and right), returns a sequence which tells 158 | /// whether elements are only found on the left, on the right, or on 159 | /// both sides. 160 | /// 161 | /// Both input sequences do not have to share the same element type, but 162 | /// their elements must share a common comparable id. 163 | /// 164 | /// For example: 165 | /// 166 | /// // Prints: 167 | /// // - common(Left(id: 1), Right(id: 1)) 168 | /// // - left(Left(id: 2)) 169 | /// struct Left: Identifiable { var id: Int } 170 | /// struct Right: Identifiable { var id: Int } 171 | /// for change in SortedDifference( 172 | /// left: [Left(id: 1), Left(id: 2)], 173 | /// right: [Right(id: 1)]) 174 | /// { 175 | /// print(change) 176 | /// } 177 | /// 178 | /// - precondition: Both input sequences must be sorted by id. 179 | /// - precondition: Ids must be unique in each sequences. 180 | /// - parameters: 181 | /// - left: The left sequence. 182 | /// - right: The right sequence. 183 | public init(left: LeftSequence, right: RightSequence) { 184 | self.init( 185 | left: left, 186 | identifiedBy: { $0.id }, 187 | right: right, 188 | identifiedBy: { $0.id }) 189 | } 190 | } 191 | 192 | extension SortedDifference where 193 | LeftSequence.Element: Comparable, 194 | RightSequence.Element == LeftSequence.Element, 195 | LeftSequence.Element == ID 196 | { 197 | /// Given two sequences (left and right), this sequence tells whether 198 | /// elements are only found on the left, on the right, or on both sides. 199 | /// 200 | /// For example: 201 | /// 202 | /// // Prints: 203 | /// // - common(1, 1) 204 | /// // - left(2) 205 | /// for change in SortedDifference(left: [1, 2], right: [1]) { 206 | /// print(change) 207 | /// } 208 | /// 209 | /// - precondition: Both input sequences must be sorted. 210 | /// - precondition: Both input sequences must contain unique elements. 211 | /// - parameters: 212 | /// - left: The left sequence. 213 | /// - right: The right sequence. 214 | public init(left: LeftSequence, right: RightSequence) { 215 | self.init( 216 | left: left, 217 | identifiedBy: { $0 }, 218 | right: right, 219 | identifiedBy: { $0 }) 220 | } 221 | } 222 | 223 | /// An element of the SortedDifference sequence. 224 | public enum SortedDifferenceChange { 225 | /// An element only found in the left sequence 226 | case left(Left) 227 | /// An element only found in the right sequence 228 | case right(Right) 229 | /// Left and right elements share a common id 230 | case common(Left, Right) 231 | } 232 | 233 | extension SortedDifferenceChange: CustomStringConvertible { 234 | public var description: String { 235 | switch self { 236 | case let .left(lhs): 237 | return "left(\(String(describing: lhs)))" 238 | case let .right(rhs): 239 | return "right(\(String(describing: rhs)))" 240 | case let .common(lhs, rhs): 241 | return "common(\(String(describing: lhs)), \(String(describing: rhs)))" 242 | } 243 | } 244 | } 245 | 246 | extension SortedDifferenceChange: CustomDebugStringConvertible { 247 | public var debugDescription: String { 248 | switch self { 249 | case let .left(lhs): 250 | return "left(\(String(reflecting: lhs)))" 251 | case let .right(rhs): 252 | return "right(\(String(reflecting: rhs)))" 253 | case let .common(lhs, rhs): 254 | return "common(\(String(reflecting: lhs)), \(String(reflecting: rhs)))" 255 | } 256 | } 257 | } 258 | 259 | extension SortedDifferenceChange: Equatable where 260 | Left: Equatable, 261 | Right: Equatable 262 | { } 263 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SortedDifferenceTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SortedDifferenceTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SortedDifferenceTests/SortedDifferenceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SortedDifference 3 | 4 | private struct ID: Comparable { 5 | var value: String 6 | static func == (lhs: ID, rhs: ID) -> Bool { 7 | return lhs.value == rhs.value 8 | } 9 | static func < (lhs: ID, rhs: ID) -> Bool { 10 | return lhs.value < rhs.value 11 | } 12 | } 13 | 14 | final class SortedDifferenceTests: XCTestCase { 15 | func testSortedDifference() { 16 | XCTAssertEqual( 17 | Array(SortedDifference(left: [Int](), right: [])), 18 | []) 19 | 20 | XCTAssertEqual( 21 | Array(SortedDifference(left: [], right: [0])), 22 | [.right(0)]) 23 | 24 | XCTAssertEqual( 25 | Array(SortedDifference(left: [], right: [0, 1])), 26 | [.right(0), .right(1)]) 27 | 28 | XCTAssertEqual( 29 | Array(SortedDifference(left: [], right: [0, 1, 2])), 30 | [.right(0), .right(1), .right(2)]) 31 | 32 | XCTAssertEqual( 33 | Array(SortedDifference(left: [], right: [0, 1, 2, 3])), 34 | [.right(0), .right(1), .right(2), .right(3)]) 35 | 36 | XCTAssertEqual( 37 | Array(SortedDifference(left: [0], right: [])), 38 | [.left(0)]) 39 | 40 | XCTAssertEqual( 41 | Array(SortedDifference(left: [0], right: [0])), 42 | [.common(0, 0)]) 43 | 44 | XCTAssertEqual( 45 | Array(SortedDifference(left: [0], right: [0, 1])), 46 | [.common(0, 0), .right(1)]) 47 | 48 | XCTAssertEqual( 49 | Array(SortedDifference(left: [0], right: [0, 1, 2])), 50 | [.common(0, 0), .right(1), .right(2)]) 51 | 52 | XCTAssertEqual( 53 | Array(SortedDifference(left: [0], right: [0, 1, 2, 3])), 54 | [.common(0, 0), .right(1), .right(2), .right(3)]) 55 | 56 | XCTAssertEqual( 57 | Array(SortedDifference(left: [0], right: [1])), 58 | [.left(0), .right(1)]) 59 | 60 | XCTAssertEqual( 61 | Array(SortedDifference(left: [0], right: [1, 2])), 62 | [.left(0), .right(1), .right(2)]) 63 | 64 | XCTAssertEqual( 65 | Array(SortedDifference(left: [0], right: [1, 2, 3])), 66 | [.left(0), .right(1), .right(2), .right(3)]) 67 | 68 | XCTAssertEqual( 69 | Array(SortedDifference(left: [0, 1], right: [])), 70 | [.left(0), .left(1)]) 71 | 72 | XCTAssertEqual( 73 | Array(SortedDifference(left: [0, 1], right: [0])), 74 | [.common(0, 0), .left(1)]) 75 | 76 | XCTAssertEqual( 77 | Array(SortedDifference(left: [0, 1], right: [0, 1])), 78 | [.common(0, 0), .common(1, 1)]) 79 | 80 | XCTAssertEqual( 81 | Array(SortedDifference(left: [0, 1], right: [0, 1, 2])), 82 | [.common(0, 0), .common(1, 1), .right(2)]) 83 | 84 | XCTAssertEqual( 85 | Array(SortedDifference(left: [0, 1], right: [0, 1, 2, 3])), 86 | [.common(0, 0), .common(1, 1), .right(2), .right(3)]) 87 | 88 | XCTAssertEqual( 89 | Array(SortedDifference(left: [0, 1], right: [0, 2])), 90 | [.common(0, 0), .left(1), .right(2)]) 91 | 92 | XCTAssertEqual( 93 | Array(SortedDifference(left: [0, 1], right: [0, 2, 3])), 94 | [.common(0, 0), .left(1), .right(2), .right(3)]) 95 | 96 | XCTAssertEqual( 97 | Array(SortedDifference(left: [0, 1], right: [1])), 98 | [.left(0), .common(1, 1)]) 99 | 100 | XCTAssertEqual( 101 | Array(SortedDifference(left: [0, 1], right: [1, 2])), 102 | [.left(0), .common(1, 1), .right(2)]) 103 | 104 | XCTAssertEqual( 105 | Array(SortedDifference(left: [0, 1], right: [1, 2, 3])), 106 | [.left(0), .common(1, 1), .right(2), .right(3)]) 107 | 108 | XCTAssertEqual( 109 | Array(SortedDifference(left: [0, 1], right: [2])), 110 | [.left(0), .left(1), .right(2)]) 111 | 112 | XCTAssertEqual( 113 | Array(SortedDifference(left: [0, 1], right: [2, 3])), 114 | [.left(0), .left(1), .right(2), .right(3)]) 115 | 116 | XCTAssertEqual( 117 | Array(SortedDifference(left: [0, 1, 2], right: [])), 118 | [.left(0), .left(1), .left(2)]) 119 | 120 | XCTAssertEqual( 121 | Array(SortedDifference(left: [0, 1, 2], right: [0])), 122 | [.common(0, 0), .left(1), .left(2)]) 123 | 124 | XCTAssertEqual( 125 | Array(SortedDifference(left: [0, 1, 2], right: [0, 1])), 126 | [.common(0, 0), .common(1, 1), .left(2)]) 127 | 128 | XCTAssertEqual( 129 | Array(SortedDifference(left: [0, 1, 2], right: [0, 1, 2])), 130 | [.common(0, 0), .common(1, 1), .common(2, 2)]) 131 | 132 | XCTAssertEqual( 133 | Array(SortedDifference(left: [0, 1, 2], right: [0, 1, 2, 3])), 134 | [.common(0, 0), .common(1, 1), .common(2, 2), .right(3)]) 135 | 136 | XCTAssertEqual( 137 | Array(SortedDifference(left: [0, 1, 2], right: [0, 1, 3])), 138 | [.common(0, 0), .common(1, 1), .left(2), .right(3)]) 139 | 140 | XCTAssertEqual( 141 | Array(SortedDifference(left: [0, 1, 2], right: [0, 2])), 142 | [.common(0, 0), .left(1), .common(2, 2)]) 143 | 144 | XCTAssertEqual( 145 | Array(SortedDifference(left: [0, 1, 2], right: [0, 2, 3])), 146 | [.common(0, 0), .left(1), .common(2, 2), .right(3)]) 147 | 148 | XCTAssertEqual( 149 | Array(SortedDifference(left: [0, 1, 2], right: [0, 3])), 150 | [.common(0, 0), .left(1), .left(2), .right(3)]) 151 | 152 | XCTAssertEqual( 153 | Array(SortedDifference(left: [0, 1, 2], right: [1])), 154 | [.left(0), .common(1, 1), .left(2)]) 155 | 156 | XCTAssertEqual( 157 | Array(SortedDifference(left: [0, 1, 2], right: [1, 2])), 158 | [.left(0), .common(1, 1), .common(2, 2)]) 159 | 160 | XCTAssertEqual( 161 | Array(SortedDifference(left: [0, 1, 2], right: [1, 2, 3])), 162 | [.left(0), .common(1, 1), .common(2, 2), .right(3)]) 163 | 164 | XCTAssertEqual( 165 | Array(SortedDifference(left: [0, 1, 2], right: [1, 3])), 166 | [.left(0), .common(1, 1), .left(2), .right(3)]) 167 | 168 | XCTAssertEqual( 169 | Array(SortedDifference(left: [0, 1, 2], right: [2])), 170 | [.left(0), .left(1), .common(2, 2)]) 171 | 172 | XCTAssertEqual( 173 | Array(SortedDifference(left: [0, 1, 2], right: [2, 3])), 174 | [.left(0), .left(1), .common(2, 2), .right(3)]) 175 | 176 | XCTAssertEqual( 177 | Array(SortedDifference(left: [0, 1, 2], right: [3])), 178 | [.left(0), .left(1), .left(2), .right(3)]) 179 | 180 | XCTAssertEqual( 181 | Array(SortedDifference(left: [0, 1, 2, 3], right: [])), 182 | [.left(0), .left(1), .left(2), .left(3)]) 183 | 184 | XCTAssertEqual( 185 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0])), 186 | [.common(0, 0), .left(1), .left(2), .left(3)]) 187 | 188 | XCTAssertEqual( 189 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0, 1])), 190 | [.common(0, 0), .common(1, 1), .left(2), .left(3)]) 191 | 192 | XCTAssertEqual( 193 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0, 1, 2])), 194 | [.common(0, 0), .common(1, 1), .common(2, 2), .left(3)]) 195 | 196 | XCTAssertEqual( 197 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0, 1, 2, 3])), 198 | [.common(0, 0), .common(1, 1), .common(2, 2), .common(3, 3)]) 199 | 200 | XCTAssertEqual( 201 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0, 1, 3])), 202 | [.common(0, 0), .common(1, 1), .left(2), .common(3, 3)]) 203 | 204 | XCTAssertEqual( 205 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0, 2])), 206 | [.common(0, 0), .left(1), .common(2, 2), .left(3)]) 207 | 208 | XCTAssertEqual( 209 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0, 2, 3])), 210 | [.common(0, 0), .left(1), .common(2, 2), .common(3, 3)]) 211 | 212 | XCTAssertEqual( 213 | Array(SortedDifference(left: [0, 1, 2, 3], right: [0, 3])), 214 | [.common(0, 0), .left(1), .left(2), .common(3, 3)]) 215 | 216 | XCTAssertEqual( 217 | Array(SortedDifference(left: [0, 1, 2, 3], right: [1])), 218 | [.left(0), .common(1, 1), .left(2), .left(3)]) 219 | 220 | XCTAssertEqual( 221 | Array(SortedDifference(left: [0, 1, 2, 3], right: [1, 2])), 222 | [.left(0), .common(1, 1), .common(2, 2), .left(3)]) 223 | 224 | XCTAssertEqual( 225 | Array(SortedDifference(left: [0, 1, 2, 3], right: [1, 2, 3])), 226 | [.left(0), .common(1, 1), .common(2, 2), .common(3, 3)]) 227 | 228 | XCTAssertEqual( 229 | Array(SortedDifference(left: [0, 1, 2, 3], right: [1, 3])), 230 | [.left(0), .common(1, 1), .left(2), .common(3, 3)]) 231 | 232 | XCTAssertEqual( 233 | Array(SortedDifference(left: [0, 1, 2, 3], right: [2])), 234 | [.left(0), .left(1), .common(2, 2), .left(3)]) 235 | 236 | XCTAssertEqual( 237 | Array(SortedDifference(left: [0, 1, 2, 3], right: [2, 3])), 238 | [.left(0), .left(1), .common(2, 2), .common(3, 3)]) 239 | 240 | XCTAssertEqual( 241 | Array(SortedDifference(left: [0, 1, 2, 3], right: [3])), 242 | [.left(0), .left(1), .left(2), .common(3, 3)]) 243 | 244 | XCTAssertEqual( 245 | Array(SortedDifference(left: [0, 1, 3], right: [0, 1, 2])), 246 | [.common(0, 0), .common(1, 1), .right(2), .left(3)]) 247 | 248 | XCTAssertEqual( 249 | Array(SortedDifference(left: [0, 1, 3], right: [0, 1, 2, 3])), 250 | [.common(0, 0), .common(1, 1), .right(2), .common(3, 3)]) 251 | 252 | XCTAssertEqual( 253 | Array(SortedDifference(left: [0, 1, 3], right: [0, 2])), 254 | [.common(0, 0), .left(1), .right(2), .left(3)]) 255 | 256 | XCTAssertEqual( 257 | Array(SortedDifference(left: [0, 1, 3], right: [0, 2, 3])), 258 | [.common(0, 0), .left(1), .right(2), .common(3, 3)]) 259 | 260 | XCTAssertEqual( 261 | Array(SortedDifference(left: [0, 1, 3], right: [1, 2])), 262 | [.left(0), .common(1, 1), .right(2), .left(3)]) 263 | 264 | XCTAssertEqual( 265 | Array(SortedDifference(left: [0, 1, 3], right: [1, 2, 3])), 266 | [.left(0), .common(1, 1), .right(2), .common(3, 3)]) 267 | 268 | XCTAssertEqual( 269 | Array(SortedDifference(left: [0, 1, 3], right: [2])), 270 | [.left(0), .left(1), .right(2), .left(3)]) 271 | 272 | XCTAssertEqual( 273 | Array(SortedDifference(left: [0, 1, 3], right: [2, 3])), 274 | [.left(0), .left(1), .right(2), .common(3, 3)]) 275 | 276 | XCTAssertEqual( 277 | Array(SortedDifference(left: [0, 2], right: [0, 1])), 278 | [.common(0, 0), .right(1), .left(2)]) 279 | 280 | XCTAssertEqual( 281 | Array(SortedDifference(left: [0, 2], right: [0, 1, 2])), 282 | [.common(0, 0), .right(1), .common(2, 2)]) 283 | 284 | XCTAssertEqual( 285 | Array(SortedDifference(left: [0, 2], right: [0, 1, 2, 3])), 286 | [.common(0, 0), .right(1), .common(2, 2), .right(3)]) 287 | 288 | XCTAssertEqual( 289 | Array(SortedDifference(left: [0, 2], right: [0, 1, 3])), 290 | [.common(0, 0), .right(1), .left(2), .right(3)]) 291 | 292 | XCTAssertEqual( 293 | Array(SortedDifference(left: [0, 2], right: [1])), 294 | [.left(0), .right(1), .left(2)]) 295 | 296 | XCTAssertEqual( 297 | Array(SortedDifference(left: [0, 2], right: [1, 2])), 298 | [.left(0), .right(1), .common(2, 2)]) 299 | 300 | XCTAssertEqual( 301 | Array(SortedDifference(left: [0, 2], right: [1, 2, 3])), 302 | [.left(0), .right(1), .common(2, 2), .right(3)]) 303 | 304 | XCTAssertEqual( 305 | Array(SortedDifference(left: [0, 2], right: [1, 3])), 306 | [.left(0), .right(1), .left(2), .right(3)]) 307 | 308 | XCTAssertEqual( 309 | Array(SortedDifference(left: [0, 2, 3], right: [0, 1])), 310 | [.common(0, 0), .right(1), .left(2), .left(3)]) 311 | 312 | XCTAssertEqual( 313 | Array(SortedDifference(left: [0, 2, 3], right: [0, 1, 2])), 314 | [.common(0, 0), .right(1), .common(2, 2), .left(3)]) 315 | 316 | XCTAssertEqual( 317 | Array(SortedDifference(left: [0, 2, 3], right: [0, 1, 2, 3])), 318 | [.common(0, 0), .right(1), .common(2, 2), .common(3, 3)]) 319 | 320 | XCTAssertEqual( 321 | Array(SortedDifference(left: [0, 2, 3], right: [0, 1, 3])), 322 | [.common(0, 0), .right(1), .left(2), .common(3, 3)]) 323 | 324 | XCTAssertEqual( 325 | Array(SortedDifference(left: [0, 2, 3], right: [1])), 326 | [.left(0), .right(1), .left(2), .left(3)]) 327 | 328 | XCTAssertEqual( 329 | Array(SortedDifference(left: [0, 2, 3], right: [1, 2])), 330 | [.left(0), .right(1), .common(2, 2), .left(3)]) 331 | 332 | XCTAssertEqual( 333 | Array(SortedDifference(left: [0, 2, 3], right: [1, 2, 3])), 334 | [.left(0), .right(1), .common(2, 2), .common(3, 3)]) 335 | 336 | XCTAssertEqual( 337 | Array(SortedDifference(left: [0, 2, 3], right: [1, 3])), 338 | [.left(0), .right(1), .left(2), .common(3, 3)]) 339 | 340 | XCTAssertEqual( 341 | Array(SortedDifference(left: [0, 3], right: [0, 1, 2])), 342 | [.common(0, 0), .right(1), .right(2), .left(3)]) 343 | 344 | XCTAssertEqual( 345 | Array(SortedDifference(left: [0, 3], right: [0, 1, 2, 3])), 346 | [.common(0, 0), .right(1), .right(2), .common(3, 3)]) 347 | 348 | XCTAssertEqual( 349 | Array(SortedDifference(left: [0, 3], right: [1, 2])), 350 | [.left(0), .right(1), .right(2), .left(3)]) 351 | 352 | XCTAssertEqual( 353 | Array(SortedDifference(left: [0, 3], right: [1, 2, 3])), 354 | [.left(0), .right(1), .right(2), .common(3, 3)]) 355 | 356 | XCTAssertEqual( 357 | Array(SortedDifference(left: [1], right: [0])), 358 | [.right(0), .left(1)]) 359 | 360 | XCTAssertEqual( 361 | Array(SortedDifference(left: [1], right: [0, 1])), 362 | [.right(0), .common(1, 1)]) 363 | 364 | XCTAssertEqual( 365 | Array(SortedDifference(left: [1], right: [0, 1, 2])), 366 | [.right(0), .common(1, 1), .right(2)]) 367 | 368 | XCTAssertEqual( 369 | Array(SortedDifference(left: [1], right: [0, 1, 2, 3])), 370 | [.right(0), .common(1, 1), .right(2), .right(3)]) 371 | 372 | XCTAssertEqual( 373 | Array(SortedDifference(left: [1], right: [0, 2])), 374 | [.right(0), .left(1), .right(2)]) 375 | 376 | XCTAssertEqual( 377 | Array(SortedDifference(left: [1], right: [0, 2, 3])), 378 | [.right(0), .left(1), .right(2), .right(3)]) 379 | 380 | XCTAssertEqual( 381 | Array(SortedDifference(left: [1, 2], right: [0])), 382 | [.right(0), .left(1), .left(2)]) 383 | 384 | XCTAssertEqual( 385 | Array(SortedDifference(left: [1, 2], right: [0, 1])), 386 | [.right(0), .common(1, 1), .left(2)]) 387 | 388 | XCTAssertEqual( 389 | Array(SortedDifference(left: [1, 2], right: [0, 1, 2])), 390 | [.right(0), .common(1, 1), .common(2, 2)]) 391 | 392 | XCTAssertEqual( 393 | Array(SortedDifference(left: [1, 2], right: [0, 1, 2, 3])), 394 | [.right(0), .common(1, 1), .common(2, 2), .right(3)]) 395 | 396 | XCTAssertEqual( 397 | Array(SortedDifference(left: [1, 2], right: [0, 1, 3])), 398 | [.right(0), .common(1, 1), .left(2), .right(3)]) 399 | 400 | XCTAssertEqual( 401 | Array(SortedDifference(left: [1, 2], right: [0, 2])), 402 | [.right(0), .left(1), .common(2, 2)]) 403 | 404 | XCTAssertEqual( 405 | Array(SortedDifference(left: [1, 2], right: [0, 2, 3])), 406 | [.right(0), .left(1), .common(2, 2), .right(3)]) 407 | 408 | XCTAssertEqual( 409 | Array(SortedDifference(left: [1, 2], right: [0, 3])), 410 | [.right(0), .left(1), .left(2), .right(3)]) 411 | 412 | XCTAssertEqual( 413 | Array(SortedDifference(left: [1, 2, 3], right: [0])), 414 | [.right(0), .left(1), .left(2), .left(3)]) 415 | 416 | XCTAssertEqual( 417 | Array(SortedDifference(left: [1, 2, 3], right: [0, 1])), 418 | [.right(0), .common(1, 1), .left(2), .left(3)]) 419 | 420 | XCTAssertEqual( 421 | Array(SortedDifference(left: [1, 2, 3], right: [0, 1, 2])), 422 | [.right(0), .common(1, 1), .common(2, 2), .left(3)]) 423 | 424 | XCTAssertEqual( 425 | Array(SortedDifference(left: [1, 2, 3], right: [0, 1, 2, 3])), 426 | [.right(0), .common(1, 1), .common(2, 2), .common(3, 3)]) 427 | 428 | XCTAssertEqual( 429 | Array(SortedDifference(left: [1, 2, 3], right: [0, 1, 3])), 430 | [.right(0), .common(1, 1), .left(2), .common(3, 3)]) 431 | 432 | XCTAssertEqual( 433 | Array(SortedDifference(left: [1, 2, 3], right: [0, 2])), 434 | [.right(0), .left(1), .common(2, 2), .left(3)]) 435 | 436 | XCTAssertEqual( 437 | Array(SortedDifference(left: [1, 2, 3], right: [0, 2, 3])), 438 | [.right(0), .left(1), .common(2, 2), .common(3, 3)]) 439 | 440 | XCTAssertEqual( 441 | Array(SortedDifference(left: [1, 2, 3], right: [0, 3])), 442 | [.right(0), .left(1), .left(2), .common(3, 3)]) 443 | 444 | XCTAssertEqual( 445 | Array(SortedDifference(left: [1, 3], right: [0, 1, 2])), 446 | [.right(0), .common(1, 1), .right(2), .left(3)]) 447 | 448 | XCTAssertEqual( 449 | Array(SortedDifference(left: [1, 3], right: [0, 1, 2, 3])), 450 | [.right(0), .common(1, 1), .right(2), .common(3, 3)]) 451 | 452 | XCTAssertEqual( 453 | Array(SortedDifference(left: [1, 3], right: [0, 2])), 454 | [.right(0), .left(1), .right(2), .left(3)]) 455 | 456 | XCTAssertEqual( 457 | Array(SortedDifference(left: [1, 3], right: [0, 2, 3])), 458 | [.right(0), .left(1), .right(2), .common(3, 3)]) 459 | 460 | XCTAssertEqual( 461 | Array(SortedDifference(left: [2], right: [0, 1])), 462 | [.right(0), .right(1), .left(2)]) 463 | 464 | XCTAssertEqual( 465 | Array(SortedDifference(left: [2], right: [0, 1, 2])), 466 | [.right(0), .right(1), .common(2, 2)]) 467 | 468 | XCTAssertEqual( 469 | Array(SortedDifference(left: [2], right: [0, 1, 2, 3])), 470 | [.right(0), .right(1), .common(2, 2), .right(3)]) 471 | 472 | XCTAssertEqual( 473 | Array(SortedDifference(left: [2], right: [0, 1, 3])), 474 | [.right(0), .right(1), .left(2), .right(3)]) 475 | 476 | XCTAssertEqual( 477 | Array(SortedDifference(left: [2, 3], right: [0, 1])), 478 | [.right(0), .right(1), .left(2), .left(3)]) 479 | 480 | XCTAssertEqual( 481 | Array(SortedDifference(left: [2, 3], right: [0, 1, 2])), 482 | [.right(0), .right(1), .common(2, 2), .left(3)]) 483 | 484 | XCTAssertEqual( 485 | Array(SortedDifference(left: [2, 3], right: [0, 1, 2, 3])), 486 | [.right(0), .right(1), .common(2, 2), .common(3, 3)]) 487 | 488 | XCTAssertEqual( 489 | Array(SortedDifference(left: [2, 3], right: [0, 1, 3])), 490 | [.right(0), .right(1), .left(2), .common(3, 3)]) 491 | 492 | XCTAssertEqual( 493 | Array(SortedDifference(left: [3], right: [0, 1, 2])), 494 | [.right(0), .right(1), .right(2), .left(3)]) 495 | 496 | XCTAssertEqual( 497 | Array(SortedDifference(left: [3], right: [0, 1, 2, 3])), 498 | [.right(0), .right(1), .right(2), .common(3, 3)]) 499 | } 500 | 501 | func testCustomIdentifier() { 502 | struct Left { 503 | var leftID: ID 504 | } 505 | struct Right { 506 | var rightID: ID 507 | } 508 | 509 | let difference = SortedDifference( 510 | left: [Left(leftID: ID(value: "a")), Left(leftID: ID(value: "b"))], 511 | identifiedBy: { $0.leftID }, 512 | right: [Right(rightID: ID(value: "a"))], 513 | identifiedBy: { $0.rightID }) 514 | 515 | // Make changes equatable, without conforming Left and Right to 516 | // Equatable, so that the test runs with minimal requirements 517 | let changes = difference.map { change -> SortedDifferenceChange in 518 | switch change { 519 | case let .left(left): return .left(left.leftID) 520 | case let .right(right): return .right(right.rightID) 521 | case let .common(left, right): return .common(left.leftID, right.rightID) 522 | } 523 | } 524 | 525 | XCTAssertEqual(changes, [ 526 | .common(ID(value: "a"), ID(value: "a")), 527 | .left(ID(value: "b")), 528 | ]) 529 | } 530 | 531 | func testIdentifiableConvenienceInitializer() { 532 | if #available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) { 533 | struct Left: Identifiable { 534 | var id: Int 535 | } 536 | struct Right: Identifiable { 537 | var id: Int 538 | } 539 | 540 | let difference = SortedDifference( 541 | left: [Left(id: 0), Left(id: 1)], 542 | right: [Right(id: 0)]) 543 | 544 | // Make changes equatable, without conforming Left and Right to 545 | // Equatable, so that the test runs with minimal requirements 546 | let changes = difference.map { change -> SortedDifferenceChange in 547 | switch change { 548 | case let .left(left): return .left(left.id) 549 | case let .right(right): return .right(right.id) 550 | case let .common(left, right): return .common(left.id, right.id) 551 | } 552 | } 553 | 554 | XCTAssertEqual(changes, [ 555 | .common(0, 0), 556 | .left(1), 557 | ]) 558 | } 559 | } 560 | 561 | func testComparableConvenienceInitializer() { 562 | let difference = SortedDifference( 563 | left: [ID(value: "a"), ID(value: "b")], 564 | right: [ID(value: "a")]) 565 | 566 | XCTAssertEqual(Array(difference), [ 567 | .common(ID(value: "a"), ID(value: "a")), 568 | .left(ID(value: "b")), 569 | ]) 570 | } 571 | 572 | static var allTests = [ 573 | ("testSortedDifference", testSortedDifference), 574 | ("testCustomIdentifier", testCustomIdentifier), 575 | ("testIdentifiableConvenienceInitializer", testIdentifiableConvenienceInitializer), 576 | ("testComparableConvenienceInitializer", testComparableConvenienceInitializer), 577 | ] 578 | } 579 | -------------------------------------------------------------------------------- /Tests/SortedDifferenceTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SortedDifferenceTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------