Skip to content

Commit 817c97a

Browse files
committed
Implement initial SortedDictionary API and implementations for SortedSet.
Key Changes: - SortedSet - Implement BidirectionalCollection - SortedDictionary - Implement Codable - Implement Partial RangeReplaceableCollection - Includes remove*() operations - init(grouping:by:) - var keys: Keys (aka SortedSet<Key>) - func (compact)mapValues(_:) Minor Changes: - Correct time documented complexities of various methods - Implement more variations of removal methods.
1 parent 3d5d5ac commit 817c97a

17 files changed

+903
-84
lines changed

Sources/SortedCollections/BTree/_BTree.UnsafePath.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ extension _BTree {
111111
node: Node,
112112
slot: Int,
113113
childSlots: Array<Int>,
114-
offset: Int) {
114+
offset: Int
115+
) {
115116
self.init(node: node.storage, slot: slot, childSlots: childSlots, offset: offset)
116117
}
117118

Sources/SortedCollections/BTree/_BTree.swift

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,22 @@ extension _BTree {
164164
return nil
165165
}
166166

167+
/// Verifies if the tree is balanced post-removal
168+
/// - Warning: This does not invalidate indices.
169+
@inlinable
170+
@inline(__always)
171+
internal mutating func _balanceRoot() {
172+
if self.root.read({ $0.numElements == 0 && !$0.isLeaf }) {
173+
let newRoot: Node = self.root.update { handle in
174+
let newRoot = handle.moveChild(at: 0)
175+
handle.drop()
176+
return newRoot
177+
}
178+
179+
self.root = newRoot
180+
}
181+
}
182+
167183
/// Removes the key-value pair corresponding to the first found instance of the key.
168184
///
169185
/// This may not be the first instance of the key. This is marginally more efficient for trees
@@ -179,22 +195,82 @@ extension _BTree {
179195
invalidateIndices()
180196

181197
// TODO: Don't create CoW copy until needed.
182-
// TODO: Handle root deletion
183198
let removedElement = self.root.update { $0.removeAny(key: key) }
184199

185200
// Check if the tree height needs to be reduced
186-
if self.root.read({ $0.numElements == 0 && !$0.isLeaf }) {
187-
let newRoot: Node = self.root.update { handle in
188-
let newRoot = handle.moveChild(at: 0)
189-
handle.drop()
190-
return newRoot
191-
}
192-
193-
self.root = newRoot
194-
}
201+
self._balanceRoot()
202+
203+
return removedElement
204+
}
205+
206+
/// Removes the first element of a tree, if it exists.
207+
///
208+
/// - Returns: The moved first element of the tree.
209+
@inlinable
210+
@discardableResult
211+
internal mutating func popLast() -> Element? {
212+
invalidateIndices()
195213

214+
if self.count == 0 { return nil }
215+
216+
let removedElement = self.root.update { $0.popLastElement() }
217+
self._balanceRoot()
218+
return removedElement
219+
}
220+
221+
/// Removes the first element of a tree, if it exists.
222+
///
223+
/// - Returns: The moved first element of the tree.
224+
@inlinable
225+
@discardableResult
226+
internal mutating func popFirst() -> Element? {
227+
invalidateIndices()
228+
229+
if self.count == 0 { return nil }
230+
231+
let removedElement = self.root.update { $0.popFirstElement() }
232+
self._balanceRoot()
233+
return removedElement
234+
}
235+
236+
/// Removes the element of a tree at a given offset.
237+
///
238+
/// - Parameter offset: the offset which must be in-bounds.
239+
/// - Returns: The moved element of the tree
240+
@inlinable
241+
@discardableResult
242+
internal mutating func remove(at offset: Int) -> Element {
243+
invalidateIndices()
244+
let removedElement = self.root.update { $0.remove(at: offset) }
245+
self._balanceRoot()
196246
return removedElement
197247
}
248+
249+
/// Removes the element of a tree at a given index.
250+
///
251+
/// - Parameter index: a valid index of the tree, not `endIndex`
252+
/// - Returns: The moved element of the tree
253+
@inlinable
254+
@discardableResult
255+
internal mutating func remove(at index: Index) -> Element {
256+
invalidateIndices()
257+
guard let path = index.path else { preconditionFailure("Index out of bounds.") }
258+
return self.remove(at: path.offset)
259+
}
260+
261+
/// Removes the elements in the specified subrange from the collection.
262+
@inlinable
263+
internal mutating func removeSubrange(_ bounds: Range<Index>) {
264+
guard let startPath = bounds.lowerBound.path else { preconditionFailure("Index out of bounds.") }
265+
guard let _ = bounds.upperBound.path else { preconditionFailure("Index out of bounds.") }
266+
267+
let rangeSize = self.distance(from: bounds.lowerBound, to: bounds.upperBound)
268+
let startOffset = startPath.offset
269+
270+
for _ in 0..<rangeSize {
271+
self.remove(at: startOffset)
272+
}
273+
}
198274
}
199275

200276
// MARK: Read Operations
@@ -234,7 +310,6 @@ extension _BTree {
234310
return false
235311
}
236312

237-
238313
/// Returns the value corresponding to the first found instance of the key.
239314
///
240315
/// This may not be the first instance of the key. This is marginally more efficient

Sources/SortedCollections/BTree/_Node.UnsafeHandle+Deletion.swift

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ extension _Node.UnsafeHandle {
3636
} else {
3737
// Deletion within an internal node
3838

39-
// TODO: potentially be smarter about using the predecessor or successor.0
40-
let predecessor = self[childAt: slot].update { $0.popElement() }
39+
// TODO: potentially be smarter about using the predecessor or successor.
40+
let predecessor = self[childAt: slot].update { $0.popLastElement() }
4141

4242
// Reduce the element count.
4343
self.numTotalElements -= 1
@@ -84,6 +84,77 @@ extension _Node.UnsafeHandle {
8484
}
8585
}
8686

87+
/// Removes the element of the tree rooted at this node, at a given offset.
88+
///
89+
/// This may leave the node it is called upon unbalanced so it is important to
90+
/// ensure the tree above this is balanced. This does adjust child counts
91+
///
92+
/// - Parameter offset: the offset which must be in-bounds.
93+
/// - Returns: The moved element of the tree
94+
@inlinable
95+
@inline(__always)
96+
internal func remove(at offset: Int) -> _Node.Element {
97+
assertMutable()
98+
assert(0 <= offset && offset < self.numTotalElements, "Cannot remove with out-of-bounds offset.")
99+
100+
if self.isLeaf {
101+
return self.removeElement(at: offset)
102+
} else {
103+
var startIndex = 0
104+
for childSlot in 0..<self.numChildren {
105+
let endIndex = startIndex + self[childAt: childSlot].read { $0.numTotalElements }
106+
107+
if offset < endIndex {
108+
return self[childAt: childSlot].update { $0.remove(at: offset - startIndex) }
109+
} else if offset == endIndex {
110+
let predecessor = self[childAt: childSlot].update { $0.popLastElement() }
111+
112+
self.numTotalElements -= 1
113+
114+
// Replace the current element with the predecessor.
115+
let element = self.moveElement(at: childSlot)
116+
self.setElement(predecessor, at: childSlot)
117+
118+
// Balance the predecessor child slot, as the pop operation may have
119+
// brought it out of balance.
120+
self.balance(at: childSlot)
121+
122+
return element
123+
} else {
124+
startIndex = endIndex + 1
125+
}
126+
}
127+
128+
preconditionFailure("B-Tree in invalid state.")
129+
}
130+
}
131+
132+
/// Removes the first element of a tree, balancing the tree.
133+
///
134+
/// This may leave the node it is called upon unbalanced so it is important to
135+
/// ensure the tree above this is balanced. This does adjust child counts
136+
///
137+
/// - Returns: The moved first element of the tree.
138+
@inlinable
139+
@inline(__always)
140+
internal func popFirstElement() -> _Node.Element {
141+
assertMutable()
142+
143+
if self.isLeaf {
144+
// At a leaf, it is trivial to pop the last element
145+
// removeElement(at:) automatically updates the counts.
146+
return self.removeElement(at: 0)
147+
} else {
148+
// Remove the subtree's element
149+
let poppedElement = self[childAt: 0].update { $0.popFirstElement() }
150+
151+
self.numTotalElements -= 1
152+
153+
self.balance(at: self.numChildren - 1)
154+
return poppedElement
155+
}
156+
}
157+
87158
/// Removes the last element of a tree, balancing the tree.
88159
///
89160
/// This may leave the node it is called upon unbalanced so it is important to
@@ -92,16 +163,16 @@ extension _Node.UnsafeHandle {
92163
/// - Returns: The moved last element of the tree.
93164
@inlinable
94165
@inline(__always)
95-
internal func popElement() -> _Node.Element {
166+
internal func popLastElement() -> _Node.Element {
96167
assertMutable()
97168

98169
if self.isLeaf {
99170
// At a leaf, it is trivial to pop the last element
100-
// removeElement(at:) automatically updates the counts.
171+
// popLastElement(at:) automatically updates the counts.
101172
return self.removeElement(at: self.numElements - 1)
102173
} else {
103174
// Remove the subtree's element
104-
let poppedElement = self[childAt: self.numChildren - 1].update { $0.popElement() }
175+
let poppedElement = self[childAt: self.numChildren - 1].update { $0.popLastElement() }
105176

106177
self.numTotalElements -= 1
107178

Sources/SortedCollections/SortedDictionary/SortedDictionary+BidirectionalCollection.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,4 @@ extension SortedDictionary: BidirectionalCollection {
8080
i._index.ensureValid(for: self._root)
8181
return Index(self._root.index(i._index, offsetBy: distance))
8282
}
83-
84-
@inlinable
85-
public subscript(position: Index) -> Element {
86-
position._index.ensureValid(for: self._root)
87-
return self._root[position._index]
88-
}
8983
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Collections open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
extension SortedDictionary: Encodable where Key: Codable, Value: Codable {
13+
/// Encodes the contents of this dictionary into the given encoder.
14+
///
15+
/// The dictionary's contents are encoded as alternating key-value pairs in
16+
/// an unkeyed container.
17+
///
18+
/// This function throws an error if any values are invalid for the given
19+
/// encoder's format.
20+
///
21+
/// - Note: Unlike the standard `Dictionary` type, sorted dictionaries
22+
/// always encode themselves into an unkeyed container, because
23+
/// `Codable`'s keyed containers do not guarantee that they preserve the
24+
/// ordering of the items they contain. (And in popular encoding formats,
25+
/// keyed containers tend to map to unordered data structures -- e.g.,
26+
/// JSON's "object" construct is explicitly unordered.)
27+
///
28+
/// - Parameter encoder: The encoder to write data to.
29+
@inlinable
30+
public func encode(to encoder: Encoder) throws {
31+
// Encode contents as an array of alternating key-value pairs.
32+
var container = encoder.unkeyedContainer()
33+
for (key, value) in self {
34+
try container.encode(key)
35+
try container.encode(value)
36+
}
37+
}
38+
}
39+
40+
extension SortedDictionary: Decodable where Key: Decodable, Value: Decodable {
41+
@inlinable
42+
public init(from decoder: Decoder) throws {
43+
// We expect to be encoded as an array of alternating key-value pairs.
44+
var container = try decoder.unkeyedContainer()
45+
46+
self.init()
47+
while !container.isAtEnd {
48+
let key = try container.decode(Key.self)
49+
50+
guard !container.isAtEnd else {
51+
throw DecodingError.dataCorrupted(
52+
DecodingError.Context(
53+
codingPath: container.codingPath,
54+
debugDescription: "Unkeyed container reached end before value in key-value pair"
55+
)
56+
)
57+
}
58+
59+
let value = try container.decode(Value.self)
60+
let oldValue = self.updateValue(value, forKey: key)
61+
if oldValue != nil {
62+
let context = DecodingError.Context(
63+
codingPath: container.codingPath,
64+
debugDescription: "Duplicate key at offset \(container.currentIndex - 1)")
65+
throw DecodingError.dataCorrupted(context)
66+
}
67+
}
68+
}
69+
}

Sources/SortedCollections/SortedDictionary/SortedDictionary+Equatable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ extension SortedDictionary: Equatable where Value: Equatable {
2020
/// - Parameters:
2121
/// - lhs: A value to compare.
2222
/// - rhs: Another value to compare.
23-
/// - Complexity: O(`n log n`)
23+
/// - Complexity: O(`n`)
2424
@inlinable
2525
public static func ==(lhs: SortedDictionary<Key, Value>, rhs: SortedDictionary<Key, Value>) -> Bool {
2626
if lhs.count != rhs.count { return false }

Sources/SortedCollections/SortedDictionary/SortedDictionary+Hashable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ extension SortedDictionary: Hashable where Key: Hashable, Value: Hashable {
1414
/// into the givenhasher.
1515
/// - Parameter hasher: The hasher to use when combining
1616
/// the components of this instance.
17-
/// - Complexity: O(`n log n`)
17+
/// - Complexity: O(`n`)
1818
@inlinable
1919
public func hash(into hasher: inout Hasher) {
2020
hasher.combine(self.count)

0 commit comments

Comments
 (0)