//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2015 - 2019 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// /// A type that represents the difference between two ordered collection states. @available(swift, introduced: 5.1) public struct CollectionDifference { /// A type that represents a single change to a collection. /// /// The `offset` of each `insert` refers to the offset of its `element` in /// the final state after the difference is fully applied. The `offset` of /// each `remove` refers to the offset of its `element` in the original /// state. Non-`nil` values of `associatedWith` refer to the offset of the /// complementary change. public enum Change { case insert(offset: Int, element: ChangeElement, associatedWith: Int?) case remove(offset: Int, element: ChangeElement, associatedWith: Int?) // Internal common field accessors var offset: Int { get { switch self { case .insert(offset: let o, element: _, associatedWith: _): return o case .remove(offset: let o, element: _, associatedWith: _): return o } } } var element: ChangeElement { get { switch self { case .insert(offset: _, element: let e, associatedWith: _): return e case .remove(offset: _, element: let e, associatedWith: _): return e } } } var associatedOffset: Int? { get { switch self { case .insert(offset: _, element: _, associatedWith: let o): return o case .remove(offset: _, element: _, associatedWith: let o): return o } } } } // The public initializer calls this function to ensure that its parameter // meets the conditions set in its documentation. private static func validateChanges(_ changes : C) -> Bool where C:Collection, C.Element == Change { if changes.count == 0 { return true } var insertAssocToOffset = Dictionary() var removeOffsetToAssoc = Dictionary() var insertOffset = Set() var removeOffset = Set() for c in changes { let offset = c.offset if offset < 0 { return false } switch c { case .remove(_, _, _): if removeOffset.contains(offset) { return false } removeOffset.insert(offset) case .insert(_, _, _): if insertOffset.contains(offset) { return false } insertOffset.insert(offset) } if let assoc = c.associatedOffset { if assoc < 0 { return false } switch c { case .remove(_, _, _): if removeOffsetToAssoc[offset] != nil { return false } removeOffsetToAssoc[offset] = assoc case .insert(_, _, _): if insertAssocToOffset[assoc] != nil { return false } insertAssocToOffset[assoc] = offset } } } return removeOffsetToAssoc == insertAssocToOffset } /// Creates an instance from a collection of changes. /// /// For clients interested in the difference between two collections, see /// `BidirectionalCollection.difference(from:)`. /// /// To guarantee that instances are unambiguous and safe for compatible base /// states, this initializer will fail unless its parameter meets to the /// following requirements: /// /// 1) All insertion offsets are unique /// 2) All removal offsets are unique /// 3) All offset associations between insertions and removals are symmetric /// /// - Parameter changes: A collection of changes that represent a transition /// between two states. /// /// - Complexity: O(*n* * log(*n*)), where *n* is the length of the /// parameter. public init?(_ c: C) where C:Collection, C.Element == Change { if !CollectionDifference.validateChanges(c) { return nil } self.init(validatedChanges: c) } // Internal initializer for use by algorithms that cannot produce invalid // collections of changes. These include the Myers' diff algorithm and // the move inferencer. init(validatedChanges c: C) where C:Collection, C.Element == Change { let changes = c.sorted { (a, b) -> Bool in switch (a, b) { case (.remove(_, _, _), .insert(_, _, _)): return true case (.insert(_, _, _), .remove(_, _, _)): return false default: return a.offset < b.offset } } // Find first insertion via binary search let firstInsertIndex: Int if changes.count == 0 { firstInsertIndex = 0 } else { var range = 0...changes.count while range.lowerBound != range.upperBound { let i = (range.lowerBound + range.upperBound) / 2 switch changes[i] { case .insert(_, _, _): range = range.lowerBound...i case .remove(_, _, _): range = (i + 1)...range.upperBound } } firstInsertIndex = range.lowerBound } removals = Array(changes[0...Change // Opaque index type is isomorphic to Int public struct Index: Comparable, Hashable { public static func < (lhs: CollectionDifference.Index, rhs: CollectionDifference.Index) -> Bool { return lhs.i < rhs.i } let i: Int init(_ index: Int) { i = index } } public var startIndex: CollectionDifference.Index { return Index(0) } public var endIndex: CollectionDifference.Index { return Index(removals.count + insertions.count) } public func index(after index: CollectionDifference.Index) -> CollectionDifference.Index { return Index(index.i + 1) } public subscript(position: CollectionDifference.Index) -> Element { return position.i < removals.count ? removals[removals.count - (position.i + 1)] : insertions[position.i - removals.count] } public func index(before index: CollectionDifference.Index) -> CollectionDifference.Index { return Index(index.i - 1) } public func formIndex(_ index: inout CollectionDifference.Index, offsetBy distance: Int) { index = Index(index.i + distance) } public func distance(from start: CollectionDifference.Index, to end: CollectionDifference.Index) -> Int { return end.i - start.i } } extension CollectionDifference.Change: Equatable where ChangeElement: Equatable {} extension CollectionDifference: Equatable where ChangeElement: Equatable {} extension CollectionDifference.Change: Hashable where ChangeElement: Hashable {} extension CollectionDifference: Hashable where ChangeElement: Hashable { /// Infers which `ChangeElement`s have been both inserted and removed only /// once and returns a new difference with those associations. /// /// - Returns: an instance with all possible moves inferred. /// /// - Complexity: O(*n*) where *n* is `self.count` public func inferringMoves() -> CollectionDifference { let removeDict: [ChangeElement:Int?] = { var res = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count)) for r in removals { let element = r.element if res[element] != .none { res[element] = .some(.none) } else { res[element] = .some(r.offset) } } return res.filter { (_, v) -> Bool in v != .none } }() let insertDict: [ChangeElement:Int?] = { var res = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count)) for i in insertions { let element = i.element if res[element] != .none { res[element] = .some(.none) } else { res[element] = .some(i.offset) } } return res.filter { (_, v) -> Bool in v != .none } }() return CollectionDifference.init(validatedChanges:map({ (c: CollectionDifference.Change) -> CollectionDifference.Change in switch c { case .remove(offset: let o, element: let e, associatedWith: _): if removeDict[e] == nil { return c } if let assoc = insertDict[e] { return .remove(offset: o, element: e, associatedWith: assoc) } case .insert(offset: let o, element: let e, associatedWith: _): if insertDict[e] == nil { return c } if let assoc = removeDict[e] { return .insert(offset: o, element: e, associatedWith: assoc) } } return c })) } } extension CollectionDifference.Change: Codable where ChangeElement: Codable { private enum CodingKeys: String, CodingKey { case offset case element case associatedOffset case isRemove } public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) let offset = try values.decode(Int.self, forKey: .offset) let element = try values.decode(ChangeElement.self, forKey: .element) let associatedOffset = try values.decode(Int?.self, forKey: .associatedOffset) let isRemove = try values.decode(Bool.self, forKey: .isRemove) if isRemove { self = .remove(offset: offset, element: element, associatedWith: associatedOffset) } else { self = .insert(offset: offset, element: element, associatedWith: associatedOffset) } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .remove(_, _, _): try container.encode(true, forKey: .isRemove) case .insert(_, _, _): try container.encode(false, forKey: .isRemove) } try container.encode(offset, forKey: .offset) try container.encode(element, forKey: .element) try container.encode(associatedOffset, forKey: .associatedOffset) } } extension CollectionDifference: Codable where ChangeElement: Codable {}