mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
461 lines
15 KiB
Swift
461 lines
15 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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 collection of insertions and removals that describe the difference
|
|
/// between two ordered collection states.
|
|
@available(SwiftStdlib 5.1, *)
|
|
public struct CollectionDifference<ChangeElement> {
|
|
/// A single change to a collection.
|
|
@frozen
|
|
public enum Change {
|
|
/// An insertion.
|
|
///
|
|
/// The `offset` value is the offset of the inserted element in the final
|
|
/// state of the collection after the difference is fully applied.
|
|
/// A non-`nil` `associatedWith` value is the offset of the complementary
|
|
/// change.
|
|
case insert(offset: Int, element: ChangeElement, associatedWith: Int?)
|
|
|
|
/// A removal.
|
|
///
|
|
/// The `offset` value is the offset of the element to be removed in the
|
|
/// original state of the collection. A non-`nil` `associatedWith` value is
|
|
/// the offset of the complementary change.
|
|
case remove(offset: Int, element: ChangeElement, associatedWith: Int?)
|
|
|
|
// Internal common field accessors
|
|
internal var _offset: Int {
|
|
get {
|
|
switch self {
|
|
case .insert(offset: let o, element: _, associatedWith: _):
|
|
return o
|
|
case .remove(offset: let o, element: _, associatedWith: _):
|
|
return o
|
|
}
|
|
}
|
|
}
|
|
internal var _element: ChangeElement {
|
|
get {
|
|
switch self {
|
|
case .insert(offset: _, element: let e, associatedWith: _):
|
|
return e
|
|
case .remove(offset: _, element: let e, associatedWith: _):
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
internal var _associatedOffset: Int? {
|
|
get {
|
|
switch self {
|
|
case .insert(offset: _, element: _, associatedWith: let o):
|
|
return o
|
|
case .remove(offset: _, element: _, associatedWith: let o):
|
|
return o
|
|
}
|
|
}
|
|
}
|
|
internal var _isRemoval: Bool {
|
|
switch self {
|
|
case .insert: false
|
|
case .remove: true
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The insertions contained by this difference, from lowest offset to
|
|
/// highest.
|
|
public let insertions: [Change]
|
|
|
|
/// The removals contained by this difference, from lowest offset to highest.
|
|
public let removals: [Change]
|
|
|
|
/// Creates a new collection difference from a collection of changes.
|
|
///
|
|
/// To find the difference between two collections, use the
|
|
/// `difference(from:)` method declared on the `BidirectionalCollection`
|
|
/// protocol.
|
|
///
|
|
/// The collection of changes passed as `changes` must meet these
|
|
/// requirements:
|
|
///
|
|
/// - All insertion offsets are unique
|
|
/// - All removal offsets are unique
|
|
/// - All 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?<Changes: Collection>(
|
|
_ changes: Changes
|
|
) where Changes.Element == Change {
|
|
guard CollectionDifference<ChangeElement>._validateChanges(changes) else {
|
|
return nil
|
|
}
|
|
|
|
self.init(_validatedChanges: changes)
|
|
}
|
|
|
|
/// Internal initializer for use by algorithms that cannot produce invalid
|
|
/// collections of changes. These include the Myers' diff algorithm,
|
|
/// self.inverse(), and the move inferencer.
|
|
///
|
|
/// If parameter validity cannot be guaranteed by the caller then
|
|
/// `CollectionDifference.init?(_:)` should be used instead.
|
|
///
|
|
/// - Parameter c: A valid collection of changes that represent a transition
|
|
/// between two states.
|
|
///
|
|
/// - Complexity: O(*n* * log(*n*)), where *n* is the length of the
|
|
/// parameter.
|
|
internal init<Changes: Collection>(
|
|
_validatedChanges changes: Changes
|
|
) where Changes.Element == Change {
|
|
let sortedChanges = changes.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 sortedChanges.isEmpty {
|
|
firstInsertIndex = 0
|
|
} else {
|
|
var range = 0...sortedChanges.count
|
|
while range.lowerBound != range.upperBound {
|
|
let i = (range.lowerBound + range.upperBound) / 2
|
|
switch sortedChanges[i] {
|
|
case .insert(_, _, _):
|
|
range = range.lowerBound...i
|
|
case .remove(_, _, _):
|
|
range = (i + 1)...range.upperBound
|
|
}
|
|
}
|
|
firstInsertIndex = range.lowerBound
|
|
}
|
|
|
|
removals = Array(sortedChanges[0..<firstInsertIndex])
|
|
insertions = Array(sortedChanges[firstInsertIndex..<sortedChanges.count])
|
|
}
|
|
|
|
/// The public initializer calls this function to ensure that its parameter
|
|
/// meets the conditions set in its documentation.
|
|
///
|
|
/// - Parameter changes: a collection of `CollectionDifference.Change`
|
|
/// instances intended to represent a valid state transition for
|
|
/// `CollectionDifference`.
|
|
///
|
|
/// - Returns: whether the parameter meets the following criteria:
|
|
///
|
|
/// 1. All insertion offsets are unique
|
|
/// 2. All removal offsets are unique
|
|
/// 3. All associations between insertions and removals are symmetric
|
|
///
|
|
/// Complexity: O(`changes.count`)
|
|
private static func _validateChanges<Changes: Collection>(
|
|
_ changes : Changes
|
|
) -> Bool where Changes.Element == Change {
|
|
if changes.isEmpty { return true }
|
|
|
|
var insertAssocToOffset = Dictionary<Int,Int>()
|
|
var removeOffsetToAssoc = Dictionary<Int,Int>()
|
|
var insertOffset = Set<Int>()
|
|
var removeOffset = Set<Int>()
|
|
|
|
for change in changes {
|
|
let offset = change._offset
|
|
if offset < 0 { return false }
|
|
|
|
switch change {
|
|
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 = change._associatedOffset {
|
|
if assoc < 0 { return false }
|
|
switch change {
|
|
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
|
|
}
|
|
|
|
public func inverse() -> Self {
|
|
return CollectionDifference(_validatedChanges: self.map { c in
|
|
switch c {
|
|
case .remove(let o, let e, let a):
|
|
return .insert(offset: o, element: e, associatedWith: a)
|
|
case .insert(let o, let e, let a):
|
|
return .remove(offset: o, element: e, associatedWith: a)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A CollectionDifference is itself a Collection.
|
|
///
|
|
/// The enumeration order of `Change` elements is:
|
|
///
|
|
/// 1. `.remove`s, from highest `offset` to lowest
|
|
/// 2. `.insert`s, from lowest `offset` to highest
|
|
///
|
|
/// This guarantees that applicators on compatible base states are safe when
|
|
/// written in the form:
|
|
///
|
|
/// ```
|
|
/// for c in diff {
|
|
/// switch c {
|
|
/// case .remove(offset: let o, element: _, associatedWith: _):
|
|
/// arr.remove(at: o)
|
|
/// case .insert(offset: let o, element: let e, associatedWith: _):
|
|
/// arr.insert(e, at: o)
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference: Collection {
|
|
public typealias Element = Change
|
|
|
|
/// The position of a collection difference.
|
|
@frozen
|
|
public struct Index {
|
|
// Opaque index type is isomorphic to Int
|
|
@usableFromInline
|
|
internal let _offset: Int
|
|
|
|
internal init(_offset offset: Int) {
|
|
_offset = offset
|
|
}
|
|
}
|
|
|
|
public var startIndex: Index {
|
|
return Index(_offset: 0)
|
|
}
|
|
|
|
public var endIndex: Index {
|
|
return Index(_offset: removals.count + insertions.count)
|
|
}
|
|
|
|
public func index(after index: Index) -> Index {
|
|
return Index(_offset: index._offset + 1)
|
|
}
|
|
|
|
public subscript(position: Index) -> Element {
|
|
if position._offset < removals.count {
|
|
return removals[removals.count - (position._offset + 1)]
|
|
}
|
|
return insertions[position._offset - removals.count]
|
|
}
|
|
|
|
public func index(before index: Index) -> Index {
|
|
return Index(_offset: index._offset - 1)
|
|
}
|
|
|
|
public func formIndex(_ index: inout Index, offsetBy distance: Int) {
|
|
index = Index(_offset: index._offset + distance)
|
|
}
|
|
|
|
public func distance(from start: Index, to end: Index) -> Int {
|
|
return end._offset - start._offset
|
|
}
|
|
}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference.Index: Equatable {
|
|
@inlinable
|
|
public static func == (
|
|
lhs: CollectionDifference.Index,
|
|
rhs: CollectionDifference.Index
|
|
) -> Bool {
|
|
return lhs._offset == rhs._offset
|
|
}
|
|
}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference.Index: Comparable {
|
|
@inlinable
|
|
public static func < (
|
|
lhs: CollectionDifference.Index,
|
|
rhs: CollectionDifference.Index
|
|
) -> Bool {
|
|
return lhs._offset < rhs._offset
|
|
}
|
|
}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference.Index: Hashable {
|
|
@inlinable
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(_offset)
|
|
}
|
|
}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference.Change: Equatable where ChangeElement: Equatable {}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference: Equatable where ChangeElement: Equatable {}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference.Change: Hashable where ChangeElement: Hashable {}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference: Hashable where ChangeElement: Hashable {}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference where ChangeElement: Hashable {
|
|
/// Returns a new collection difference with associations between individual
|
|
/// elements that have been removed and inserted only once.
|
|
///
|
|
/// - Returns: A collection difference with all possible moves inferred.
|
|
///
|
|
/// - Complexity: O(*n*) where *n* is the number of collection differences.
|
|
public func inferringMoves() -> CollectionDifference<ChangeElement> {
|
|
let uniqueRemovals: [ChangeElement:Int?] = {
|
|
var result = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count))
|
|
for removal in removals {
|
|
let element = removal._element
|
|
if result[element] != .none {
|
|
result[element] = .some(.none)
|
|
} else {
|
|
result[element] = .some(removal._offset)
|
|
}
|
|
}
|
|
return result.filter { (_, v) -> Bool in v != .none }
|
|
}()
|
|
|
|
let uniqueInsertions: [ChangeElement:Int?] = {
|
|
var result = [ChangeElement:Int?](minimumCapacity: Swift.min(removals.count, insertions.count))
|
|
for insertion in insertions {
|
|
let element = insertion._element
|
|
if result[element] != .none {
|
|
result[element] = .some(.none)
|
|
} else {
|
|
result[element] = .some(insertion._offset)
|
|
}
|
|
}
|
|
return result.filter { (_, v) -> Bool in v != .none }
|
|
}()
|
|
|
|
return CollectionDifference(_validatedChanges: map({ (change: Change) -> Change in
|
|
switch change {
|
|
case .remove(offset: let offset, element: let element, associatedWith: _):
|
|
if uniqueRemovals[element] == nil {
|
|
return change
|
|
}
|
|
if let assoc = uniqueInsertions[element] {
|
|
return .remove(offset: offset, element: element, associatedWith: assoc)
|
|
}
|
|
case .insert(offset: let offset, element: let element, associatedWith: _):
|
|
if uniqueInsertions[element] == nil {
|
|
return change
|
|
}
|
|
if let assoc = uniqueRemovals[element] {
|
|
return .insert(offset: offset, element: element, associatedWith: assoc)
|
|
}
|
|
}
|
|
return change
|
|
}))
|
|
}
|
|
}
|
|
|
|
#if !$Embedded
|
|
@available(SwiftStdlib 5.1, *)
|
|
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)
|
|
try container.encode(_isRemoval, forKey: .isRemove)
|
|
try container.encode(_offset, forKey: .offset)
|
|
try container.encode(_element, forKey: .element)
|
|
try container.encode(_associatedOffset, forKey: .associatedOffset)
|
|
}
|
|
}
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference: Codable where ChangeElement: Codable {
|
|
private enum _CodingKeys: String, CodingKey {
|
|
case insertions
|
|
case removals
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: _CodingKeys.self)
|
|
var changes = try container.decode([Change].self, forKey: .removals)
|
|
let removalCount = changes.count
|
|
try changes.append(contentsOf: container.decode([Change].self, forKey: .insertions))
|
|
|
|
guard changes[..<removalCount].allSatisfy({ $0._isRemoval }),
|
|
changes[removalCount...].allSatisfy({ !$0._isRemoval }),
|
|
Self._validateChanges(changes)
|
|
else {
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(
|
|
codingPath: decoder.codingPath,
|
|
debugDescription: "Cannot decode an invalid collection difference"))
|
|
}
|
|
|
|
self.init(_validatedChanges: changes)
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: _CodingKeys.self)
|
|
try container.encode(insertions, forKey: .insertions)
|
|
try container.encode(removals, forKey: .removals)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference: Sendable where ChangeElement: Sendable { }
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference.Change: Sendable where ChangeElement: Sendable { }
|
|
@available(SwiftStdlib 5.1, *)
|
|
extension CollectionDifference.Index: Sendable where ChangeElement: Sendable { }
|