File: CollectionChanges.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (214 lines) | stat: -rw-r--r-- 9,831 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/*
 This source file is part of the Swift.org open source project

 Copyright (c) 2022 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 Swift project authors
*/

import Foundation

/// A collection of sparse segments that describe the subsequences that are common or different between two collections.
struct CollectionChanges {
    /// The segments of common elements, removed elements, and inserted elements.
    let segments: [Segment]
    
    /// A single segment that describe a number of elements that are either common between both collections, or that are removed or inserted in the second collection.
    struct Segment: Equatable {
        var kind: Kind
        var count: Int
        
        enum Kind: Equatable {
            /// These elements are common between both collections.
            case common
            /// These elements are removed from the first collection to produce the second collection.
            case remove
            /// These elements are inserted in the first collection to produce the second collection.
            case insert
        }
    }
    
    /// Creates a new collection changes value from the differences between to collections.
    ///
    /// - Parameters:
    ///   - from: The collection that the base is compared to.
    ///   - to: The base collection.
    ///   - areEquivalent: A closure that returns a Boolean value indicating whether two elements are equivalent.
    init<C>(from: C, to: C, by areEquivalent: (C.Element, C.Element) -> Bool = (==)) where C: BidirectionalCollection, C.Element: Hashable {
        guard !from.isEmpty else {
            segments = [.init(kind: .insert, count: to.count)]
            return
        }
        guard !to.isEmpty else {
            segments = [.init(kind: .remove, count: from.count)]
            return
        }
        
        var changes = ChangeSegmentBuilder(originalCount: from.count)
        // The `CollectionDifference` enumeration order is documented; first removals in descending order then insertions in ascending order.
        // https://github.com/apple/swift/blob/main/stdlib/public/core/CollectionDifference.swift#L216-L235
        for change in to.difference(from: from, by: areEquivalent) {
            switch change {
            case .remove(let offset, _, _):
                changes.remove(at: offset)
            case .insert(let offset, _, _):
                changes.insert(at: offset)
            }
        }
        segments = changes.segments
    }
}

/// A builder that applies collection differences to construct an array of ``Segment`` values.
///
/// - Important:
/// Removals need to be applied in reverse order. All removals need to be applied before applying any insertions. Insertions need to be applied in order.
private struct ChangeSegmentBuilder {
    typealias Segment = CollectionChanges.Segment
    
    private(set) var segments: [Segment]
    
    private var insertStartIndex = 0
    private var insertStartOffset = 0
    
    init(originalCount: Int) {
        self.segments = [ Segment(kind: .common, count: originalCount) ]
    }
    
    mutating func remove(at removalIndex: Int) {
        // Removals are applied in reverse order. When the first removal is applied, the only segment is the 'common' count.
        //
        // Each removal can be either be at the start of the segment, middle of the segment, or end of the segment.
        // - After removing from the start of the segment there can be no more removals (since those indices would be in ascending order).
        // - After removing from the middle, the 'common' segment is split in two with a 'remove' segment in between.
        //   Since the removal has to be at a lower index, it can only be applied to the split 'original' segment.
        // - After removing from the end, the 'common' segment is made shorter and a new 'remove' segment is added after it.
        //   Since the removal has to be at a lower index, it can only be applied to the shortened 'common' segment.
        //
        // This process repeats, meaning that every removal is always applied to the first segment.
        let segment = segments[0]
        assert(segment.kind == .common && removalIndex < segment.count, """
            The first segment should always be a 'common' segment (was \(segment.kind)) and (0 ..< \(segment.count)) should always contain the removal index (\(removalIndex)).
            If it's not, then that's means that the remove operations wasn't performed in reverse order.
            """)
        
        if removalIndex == 0 {
            // Removing at the start of the segment
            if segment.count == 1 {
                segments.remove(at: 0)
            } else {
                segments[0].count -= 1
            }
            
            if segments.first?.kind == .remove {
                segments[0].count += 1
            } else {
                segments.insert(Segment(kind: .remove, count: 1), at: 0)
            }
        }
        else if removalIndex == segment.count - 1 {
            // Removing at end of segment
            segments[0].count -= 1

            if segments.count > 1, segments[1].kind == .remove {
                segments[1].count += 1
            } else {
                // Insert at `endIndex` is equivalent to `append()`
                segments.insert(Segment(kind: .remove, count: 1), at: 1)
            }
        } else {
            // Removal within segment
            let lowerSegmentCount  = removalIndex
            let higherSegmentCount = segment.count - lowerSegmentCount - 1 // the 1 is for the removed element
            
            // Split the segment in two with a new removal segment in-between.
            segments[0...0] = [
                Segment(kind: .common, count: lowerSegmentCount),
                Segment(kind: .remove, count: 1),
                Segment(kind: .common, count: higherSegmentCount),
            ]
        }
    }
    
    private func findSegment(toInsertAt index: Int) -> (segment: Segment, startOffset: Int, segmentIndex: Int)? {
        // Insertions are applied in order. This means that we can start with the previous offset and index.
        var offset = insertStartOffset
        for segmentIndex in insertStartIndex ..< segments.count {
            let segment = segments[segmentIndex]
            if segment.kind == .remove {
                continue
            }
            
            if index <= offset + segment.count {
                return (segment, offset, segmentIndex)
            }
            offset += segment.count
        }
        return nil
    }
    
    mutating func insert(at insertIndex: Int) {
        guard let (segment, startOffset, segmentIndex) = findSegment(toInsertAt: insertIndex) else {
            assert(segments.count == 1 && segments[0].kind == .remove, """
                The only case when a segment can't be found in the loop is if the only segment is a 'remove' segment.
                This happens when all the 'common' elements are removed (meaning that the 'from' and 'to' values have nothing in common.
                """)
            
            segments.append(Segment(kind: .insert, count: 1))
            return
        }
        assert(segment.kind != .remove)
        
        insertStartOffset = startOffset
        insertStartIndex  = segmentIndex
        
        guard segment.kind != .insert else {
            segments[segmentIndex].count += 1
            return
        }
        assert(segment.kind == .common)
        
        if insertIndex == startOffset {
            // Insert at start of segment
            segments.insert(Segment(kind: .insert, count: 1), at: segmentIndex)
        } else if insertIndex == startOffset + segment.count {
            // Insert at end of segment
            let insertSegmentIndex = segmentIndex + 1
            
            // If this is the last segment, append a new 'insert' segment
            guard insertSegmentIndex < segments.count else {
                segments.append(Segment(kind: .insert, count: 1))
                return
            }
            
            switch segments[insertSegmentIndex].kind {
            case .insert:
                assertionFailure("Inserts are processed from low to high. There shouldn't be another 'insert' segment after 'segmentIndex'.")
                
            case .common:
                // If the next segment is a 'common' segment, insert a new 'insert' segment before it
                segments.insert(Segment(kind: .insert, count: 1), at: insertSegmentIndex)
                
            case .remove:
                // If the next segment is a 'remove' segment, skip over it so that insertions are always after removals.
                segments.insert(Segment(kind: .insert, count: 1), at: insertSegmentIndex + 1)
                
                assert(insertSegmentIndex + 2 == segments.count || segments[insertSegmentIndex + 2].kind == .common,
                       "If there's a segment after the remove segment, that is a segment of 'common' characters.")
            }
        } else {
            // Insert within segment
            let lowerSegmentCount  = insertIndex - startOffset
            let higherSegmentCount = segment.count - lowerSegmentCount // nothing to add
            
            // Split the segment in two with a new insertion segment in-between.
            segments[segmentIndex...segmentIndex] = [
                Segment(kind: .common, count: lowerSegmentCount),
                Segment(kind: .insert, count: 1),
                Segment(kind: .common, count: higherSegmentCount),
            ]
        }
    }
}