Skip to content

Commit dd2753a

Browse files
committed
Split keyed(by:) into two overloads
1 parent 8f664a1 commit dd2753a

File tree

3 files changed

+65
-42
lines changed

3 files changed

+65
-42
lines changed

Guides/Keyed.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Stores the elements of a sequence as the values of a Dictionary, keyed by the result of the given closure.
77

88
```swift
9-
let fruits = ["Apple", "Banana", "Cherry"]
9+
let fruits = try! ["Apple", "Banana", "Cherry"]
1010
let fruitByLetter = fruits.keyed(by: { $0.first! })
1111
// Results in:
1212
// [
@@ -16,7 +16,7 @@ let fruitByLetter = fruits.keyed(by: { $0.first! })
1616
// ]
1717
```
1818

19-
Duplicate keys will trigger a runtime error by default. To handle this, you can provide a closure which specifies which value to keep:
19+
Duplicate keys throw an runtime error by default. Alternatively, you can provide a closure which specifies which value to keep:
2020

2121
```swift
2222
let fruits = ["Apricot", "Banana", "Apple", "Cherry", "Blackberry", "Avocado", "Coconut"]
@@ -34,10 +34,14 @@ let fruitsByLetter = fruits.keyed(
3434

3535
## Detailed Design
3636

37-
The `keyed(by:)` method is declared as a `Sequence` extension returning `[Key: Element]`.
37+
The `keyed(by:)` and `keyed(by:uniquingKeysWith:)` methods are declared in an `Sequence` extension, both returning `[Key: Element]`.
3838

3939
```swift
4040
extension Sequence {
41+
public func keyed<Key>(
42+
by keyForValue: (Element) throws -> Key
43+
) throws -> [Key: Element]
44+
4145
public func keyed<Key>(
4246
by keyForValue: (Element) throws -> Key,
4347
uniquingKeysWith combine: ((Key, Element, Element) throws -> Element)? = nil

Sources/Algorithms/Keyed.swift

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,43 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12+
public struct KeysAreNotUnique<Key, Element>: Error {
13+
public let key: Key
14+
public let previousElement: Element
15+
public let conflictedElement: Element
16+
17+
public init(key: Key, previousElement: Element, conflictedElement: Element) {
18+
self.key = key
19+
self.previousElement = previousElement
20+
self.conflictedElement = conflictedElement
21+
}
22+
}
23+
1224
extension Sequence {
25+
/// Creates a new Dictionary from the elements of `self`, keyed by the
26+
/// results returned by the given `keyForValue` closure. Deriving the
27+
/// same duplicate key for more than one element of `self` will cause
28+
/// an error to be thrown.
29+
///
30+
/// - Parameters:
31+
/// - keyForValue: A closure that returns a key for each element in `self`.
32+
@inlinable
33+
public func keyed<Key>(
34+
by keyForValue: (Element) throws -> Key
35+
) throws -> [Key: Element] {
36+
var result = [Key: Element]()
37+
38+
for element in self {
39+
let key = try keyForValue(element)
40+
41+
if let previousElement = result.updateValue(element, forKey: key) {
42+
throw KeysAreNotUnique(key: key, previousElement: previousElement, conflictedElement: element)
43+
}
44+
}
45+
46+
return result
47+
}
48+
1349
/// Creates a new Dictionary from the elements of `self`, keyed by the
1450
/// results returned by the given `keyForValue` closure. As the dictionary is
1551
/// built, the initializer calls the `combine` closure with the current and
@@ -18,50 +54,30 @@ extension Sequence {
1854
/// choose between the two values, combine them to produce a new value, or
1955
/// even throw an error.
2056
///
21-
/// If no `combine` closure is provided, deriving the same duplicate key for
22-
/// more than one element of self results in a runtime error.
23-
///
2457
/// - Parameters:
25-
/// - keyForValue: A closure that returns a key for each element in
26-
/// `self`.
58+
/// - keyForValue: A closure that returns a key for each element in `self`.
2759
/// - combine: A closure that is called with the values for any duplicate
2860
/// keys that are encountered. The closure returns the desired value for
2961
/// the final dictionary.
3062
@inlinable
3163
public func keyed<Key>(
3264
by keyForValue: (Element) throws -> Key,
33-
uniquingKeysWith combine: ((Key, Element, Element) throws -> Element)? = nil
65+
uniquingKeysWith combine: (Key, Element, Element) throws -> Element
3466
) rethrows -> [Key: Element] {
3567
var result = [Key: Element]()
3668

37-
if combine != nil {
38-
// We have a `combine` closure. Use it to resolve duplicate keys.
39-
40-
for element in self {
41-
let key = try keyForValue(element)
42-
43-
if let oldValue = result.updateValue(element, forKey: key) {
44-
// Can't use a conditional binding to unwrap this, because the newly bound variable
45-
// doesn't play nice with the `rethrows` system.
46-
let valueToKeep = try combine!(key, oldValue, element)
47-
48-
// This causes a second look-up for the same key. The standard library can avoid that
49-
// by calling `mutatingFind` to get access to the bucket where the value will end up,
50-
// and updating in place.
51-
// Swift Algorithms doesn't have access to that API, so we make due.
52-
// When this gets merged into the standard library, we should optimize this.
53-
result[key] = valueToKeep
54-
}
55-
}
56-
} else {
57-
// There's no `combine` closure. Duplicate keys are disallowed.
69+
for element in self {
70+
let key = try keyForValue(element)
5871

59-
for element in self {
60-
let key = try keyForValue(element)
72+
if let oldValue = result.updateValue(element, forKey: key) {
73+
let valueToKeep = try combine(key, oldValue, element)
6174

62-
guard result.updateValue(element, forKey: key) == nil else {
63-
fatalError("Duplicate values for key: '\(key)'")
64-
}
75+
// This causes a second look-up for the same key. The standard library can avoid that
76+
// by calling `mutatingFind` to get access to the bucket where the value will end up,
77+
// and updating in place.
78+
// Swift Algorithms doesn't have access to that API, so we make due.
79+
// When this gets merged into the standard library, we should optimize this.
80+
result[key] = valueToKeep
6581
}
6682
}
6783

Tests/SwiftAlgorithmsTests/KeyedTests.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class KeyedTests: XCTestCase {
1616
private class SampleError: Error {}
1717

1818
func testUniqueKeys() {
19-
let d = ["Apple", "Banana", "Cherry"].keyed(by: { $0.first! })
19+
let d = try! ["Apple", "Banana", "Cherry"].keyed(by: { $0.first! })
2020
XCTAssertEqual(d.count, 3)
2121
XCTAssertEqual(d["A"]!, "Apple")
2222
XCTAssertEqual(d["B"]!, "Banana")
@@ -25,16 +25,19 @@ final class KeyedTests: XCTestCase {
2525
}
2626

2727
func testEmpty() {
28-
let d = EmptyCollection<String>().keyed(by: { $0.first! })
28+
let d = try! EmptyCollection<String>().keyed(by: { $0.first! })
2929
XCTAssertEqual(d.count, 0)
3030
}
3131

3232
func testNonUniqueKeys() throws {
33-
throw XCTSkip("""
34-
TODO: What's the XCTest equivalent to `expectCrashLater()`?
35-
36-
https://github.com/apple/swift/blob/4d1d8a9de5ebc132a17aee9fc267461facf89bf8/validation-test/stdlib/Dictionary.swift#L1914
37-
""")
33+
XCTAssertThrowsError(
34+
try ["Apple", "Avocado", "Banana", "Cherry"].keyed(by: { $0.first! })
35+
) { thrownError in
36+
let e = thrownError as! KeysAreNotUnique<Character, String>
37+
XCTAssertEqual(e.key, "A")
38+
XCTAssertEqual(e.previousElement, "Apple")
39+
XCTAssertEqual(e.conflictedElement, "Avocado")
40+
}
3841
}
3942

4043
func testNonUniqueKeysWithMergeFunction() {

0 commit comments

Comments
 (0)