Skip to content

Commit fc8498f

Browse files
committed
Split keyed(by:) into two overloads
1 parent b3c2fad commit fc8498f

File tree

3 files changed

+67
-42
lines changed

3 files changed

+67
-42
lines changed

Guides/Keyed.md

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

88
```swift
99
let fruits = ["Apple", "Banana", "Cherry"]
10-
let fruitByLetter = fruits.keyed(by: { $0.first! })
10+
let fruitByLetter = try! fruits.keyed(by: { $0.first! })
1111
// Results in:
1212
// [
1313
// "A": "Apple",
@@ -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 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: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,45 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12+
public struct KeysAreNotUnique<Key, Element>: Error {
13+
public let key: Key
14+
public let previousElement: Element
15+
public let conflictingElement: Element
16+
17+
@inlinable
18+
public init(key: Key, previousElement: Element, conflictingElement: Element) {
19+
self.key = key
20+
self.previousElement = previousElement
21+
self.conflictingElement = conflictingElement
22+
}
23+
}
24+
1225
extension Sequence {
26+
/// Creates a new Dictionary from the elements of `self`, keyed by the
27+
/// results returned by the given `keyForValue` closure. Deriving the
28+
/// same duplicate key for more than one element of `self` will cause
29+
/// an error to be thrown.
30+
///
31+
/// - Parameters:
32+
/// - keyForValue: A closure that returns a key for each element in `self`.
33+
/// - Throws: `KeysAreNotUnique ` if two values map to the same key (via `keyForValue`).
34+
@inlinable
35+
public func keyed<Key>(
36+
by keyForValue: (Element) throws -> Key
37+
) throws -> [Key: Element] {
38+
var result = [Key: Element]()
39+
40+
for element in self {
41+
let key = try keyForValue(element)
42+
43+
if let previousElement = result.updateValue(element, forKey: key) {
44+
throw KeysAreNotUnique(key: key, previousElement: previousElement, conflictingElement: element)
45+
}
46+
}
47+
48+
return result
49+
}
50+
1351
/// Creates a new Dictionary from the elements of `self`, keyed by the
1452
/// results returned by the given `keyForValue` closure. As the dictionary is
1553
/// built, the initializer calls the `combine` closure with the current and
@@ -18,50 +56,30 @@ extension Sequence {
1856
/// choose between the two values, combine them to produce a new value, or
1957
/// even throw an error.
2058
///
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-
///
2459
/// - Parameters:
25-
/// - keyForValue: A closure that returns a key for each element in
26-
/// `self`.
60+
/// - keyForValue: A closure that returns a key for each element in `self`.
2761
/// - combine: A closure that is called with the values for any duplicate
2862
/// keys that are encountered. The closure returns the desired value for
2963
/// the final dictionary.
3064
@inlinable
3165
public func keyed<Key>(
3266
by keyForValue: (Element) throws -> Key,
33-
uniquingKeysWith combine: ((Key, Element, Element) throws -> Element)? = nil
67+
uniquingKeysWith combine: (Key, Element, Element) throws -> Element
3468
) rethrows -> [Key: Element] {
3569
var result = [Key: Element]()
3670

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.
71+
for element in self {
72+
let key = try keyForValue(element)
5873

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

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

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.conflictingElement, "Avocado")
40+
}
3841
}
3942

4043
func testNonUniqueKeysWithMergeFunction() {

0 commit comments

Comments
 (0)