blob: 76f12ec8ee733a17d7918843a7da2d753f5517f7 [file] [log] [blame]
//===----------------------------------------------------------------------===//
//
// 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(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
public struct CollectionDifference<ChangeElement> {
/// 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.
@_frozen
public enum Change {
case insert(offset: Int, element: ChangeElement, associatedWith: Int?)
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
}
}
}
}
/// The `.insert` changes contained by this difference, from lowest offset to highest
public let insertions: [Change]
/// The `.remove` changes contained by this difference, from lowest offset to highest
public let removals: [Change]
/// 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.count == 0 { 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
}
/// 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 associations between insertions and removals are symmetric
///
/// - Parameter c: 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 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.count == 0 {
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])
}
}
/// 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(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference: Collection {
public typealias Element = Change
@_fixed_layout
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(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference.Index: Equatable {
@inlinable
public static func == (
lhs: CollectionDifference.Index,
rhs: CollectionDifference.Index
) -> Bool {
return lhs._offset == rhs._offset
}
}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference.Index: Comparable {
@inlinable
public static func < (
lhs: CollectionDifference.Index,
rhs: CollectionDifference.Index
) -> Bool {
return lhs._offset < rhs._offset
}
}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference.Index: Hashable {
@inlinable
public func hash(into hasher: inout Hasher) {
hasher.combine(_offset)
}
}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference.Change: Equatable where ChangeElement: Equatable {}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference: Equatable where ChangeElement: Equatable {}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference.Change: Hashable where ChangeElement: Hashable {}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference: Hashable where ChangeElement: Hashable {}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference 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<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
}))
}
}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-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)
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)
}
}
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1)
extension CollectionDifference: Codable where ChangeElement: Codable {}