From 61a30e62cea5e12de56117c7f24e0e55c40f0f9d Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 11 Jan 2022 10:30:28 -0800 Subject: [PATCH 1/4] Add async rethrowing initializers for RangeReplaceableCollection, SetAlgebra, and Dictionary for use with AsyncSequence types --- Sources/AsyncAlgorithms/Dictionary.swift | 35 +++++++++++++++ .../RangeReplaceableCollection.swift | 19 ++++++++ Sources/AsyncAlgorithms/SetAlgebra.swift | 19 ++++++++ .../AsyncAlgorithmsTests/TestDictionary.swift | 36 ++++++++++++++++ .../TestRangeReplacableCollection.swift | 43 +++++++++++++++++++ .../AsyncAlgorithmsTests/TestSetAlgebra.swift | 36 ++++++++++++++++ 6 files changed, 188 insertions(+) create mode 100644 Sources/AsyncAlgorithms/Dictionary.swift create mode 100644 Sources/AsyncAlgorithms/RangeReplaceableCollection.swift create mode 100644 Sources/AsyncAlgorithms/SetAlgebra.swift create mode 100644 Tests/AsyncAlgorithmsTests/TestDictionary.swift create mode 100644 Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift create mode 100644 Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift diff --git a/Sources/AsyncAlgorithms/Dictionary.swift b/Sources/AsyncAlgorithms/Dictionary.swift new file mode 100644 index 00000000..b842a85e --- /dev/null +++ b/Sources/AsyncAlgorithms/Dictionary.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +extension Dictionary { + public init(uniqueKeysWithValues keysAndValues: S) async rethrows where S.Element == (Key, Value) { + self.init(uniqueKeysWithValues: try await Array(keysAndValues)) + } + + public init(_ keysAndValues: S, uniquingKeysWith combine: (Value, Value) async throws -> Value) async rethrows where S.Element == (Key, Value) { + self.init() + for try await (key, value) in keysAndValues { + if let existing = self[key] { + self[key] = try await combine(existing, value) + } else { + self[key] = value + } + } + } + + public init(grouping values: S, by keyForValue: (S.Element) async throws -> Key) async rethrows where Value == [S.Element] { + self.init() + for try await value in values { + let key = try await keyForValue(value) + self[key, default: []].append(value) + } + } +} diff --git a/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift b/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift new file mode 100644 index 00000000..d2d3f2fa --- /dev/null +++ b/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +extension RangeReplaceableCollection { + public init(_ source: Source) async rethrows where Source.Element == Element { + self.init() + for try await item in source { + append(item) + } + } +} diff --git a/Sources/AsyncAlgorithms/SetAlgebra.swift b/Sources/AsyncAlgorithms/SetAlgebra.swift new file mode 100644 index 00000000..9403de62 --- /dev/null +++ b/Sources/AsyncAlgorithms/SetAlgebra.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +extension SetAlgebra { + public init(_ source: Source) async rethrows where Source.Element == Element { + self.init() + for try await item in source { + insert(item) + } + } +} diff --git a/Tests/AsyncAlgorithmsTests/TestDictionary.swift b/Tests/AsyncAlgorithmsTests/TestDictionary.swift new file mode 100644 index 00000000..0c4576d8 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestDictionary.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import AsyncAlgorithms + +final class TestDictionary: XCTestCase { + func test_uniqueKeysAndValues() async { + let source = [(1, "a"), (2, "b"), (3, "c")] + let expected = Dictionary(uniqueKeysWithValues: source) + let actual = await Dictionary(uniqueKeysWithValues: source.async) + XCTAssertEqual(expected, actual) + } + + func test_uniqingWith() async { + let source = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] + let expected = Dictionary(source) { first, _ in first } + let actual = await Dictionary(source.async) { first, _ in first } + XCTAssertEqual(expected, actual) + } + + func test_grouping() async { + let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] + let expected = Dictionary(grouping: source, by: { $0.first! }) + let actual = await Dictionary(grouping: source.async, by: { $0.first! }) + XCTAssertEqual(expected, actual) + } +} diff --git a/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift b/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift new file mode 100644 index 00000000..ad0838bc --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import AsyncAlgorithms + +final class TestRangeReplacableCollection: XCTestCase { + func test_String() async { + let source = "abc" + let expected = source + let actual = await String(source.async) + XCTAssertEqual(expected, actual) + } + + func test_Data() async { + let source = Data([1, 2, 3]) + let expected = source + let actual = await Data(source.async) + XCTAssertEqual(expected, actual) + } + + func test_ContiguousArray() async { + let source = ContiguousArray([1, 2, 3]) + let expected = source + let actual = await ContiguousArray(source.async) + XCTAssertEqual(expected, actual) + } + + func test_Array() async { + let source = Array([1, 2, 3]) + let expected = source + let actual = await Array(source.async) + XCTAssertEqual(expected, actual) + } +} diff --git a/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift b/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift new file mode 100644 index 00000000..e4aa27da --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import AsyncAlgorithms + +final class TestSetAlgebra: XCTestCase { + func test_Set() async { + let source = [1, 2, 3] + let expected = Set(source) + let actual = await Set(source.async) + XCTAssertEqual(expected, actual) + } + + func test_Set_duplicate() async { + let source = [1, 2, 3, 3] + let expected = Set(source) + let actual = await Set(source.async) + XCTAssertEqual(expected, actual) + } + + func test_IndexSet() async { + let source = [1, 2, 3] + let expected = IndexSet(source) + let actual = await IndexSet(source.async) + XCTAssertEqual(expected, actual) + } +} From 5ee2e94fae4aa6dc573655b26ed5874202963c7e Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Tue, 11 Jan 2022 17:18:45 -0800 Subject: [PATCH 2/4] Add some additional tests for throwing coverage and documentation on collection initializers --- Sources/AsyncAlgorithms/Dictionary.swift | 45 +++++++++++++++++++ .../RangeReplaceableCollection.swift | 4 ++ Sources/AsyncAlgorithms/SetAlgebra.swift | 6 +++ .../AsyncAlgorithmsTests/TestDictionary.swift | 42 +++++++++++++++++ .../TestRangeReplacableCollection.swift | 14 ++++++ .../AsyncAlgorithmsTests/TestSetAlgebra.swift | 14 ++++++ 6 files changed, 125 insertions(+) diff --git a/Sources/AsyncAlgorithms/Dictionary.swift b/Sources/AsyncAlgorithms/Dictionary.swift index b842a85e..f2086d92 100644 --- a/Sources/AsyncAlgorithms/Dictionary.swift +++ b/Sources/AsyncAlgorithms/Dictionary.swift @@ -10,10 +10,42 @@ //===----------------------------------------------------------------------===// extension Dictionary { + /// Creates a new dictionary from the key-value pairs in the given asynchronous sequence. + /// + /// You use this initializer to create a dictionary when you have an asynchronous sequence + /// of key-value tuples with unique keys. Passing an asynchronous sequence with duplicate + /// keys to this initializer results in a runtime error. If your + /// asynchronous sequence might have duplicate keys, use the + /// `Dictionary(_:uniquingKeysWith:)` initializer instead. + /// + /// - Parameter keysAndValues: An asynchronous sequence of key-value pairs to use for + /// the new dictionary. Every key in `keysAndValues` must be unique. + /// - Returns: A new dictionary initialized with the elements of + /// `keysAndValues`. + /// - Precondition: The sequence must not have duplicate keys. + @inlinable public init(uniqueKeysWithValues keysAndValues: S) async rethrows where S.Element == (Key, Value) { self.init(uniqueKeysWithValues: try await Array(keysAndValues)) } + /// Creates a new dictionary from the key-value pairs in the given asynchronous sequence, + /// using a combining closure to determine the value for any duplicate keys. + /// + /// You use this initializer to create a dictionary when you have a sequence + /// of key-value tuples that might have duplicate keys. As the dictionary is + /// built, the initializer calls the `combine` closure with the current and + /// new values for any duplicate keys. Pass a closure as `combine` that + /// returns the value to use in the resulting dictionary: The closure can + /// choose between the two values, combine them to produce a new value, or + /// even throw an error. + /// + /// - Parameters: + /// - keysAndValues: An asynchronous sequence of key-value pairs to use for the new + /// dictionary. + /// - combine: A closure that is called with the values for any duplicate + /// keys that are encountered. The closure returns the desired value for + /// the final dictionary. + @inlinable public init(_ keysAndValues: S, uniquingKeysWith combine: (Value, Value) async throws -> Value) async rethrows where S.Element == (Key, Value) { self.init() for try await (key, value) in keysAndValues { @@ -25,6 +57,19 @@ extension Dictionary { } } + /// Creates a new dictionary whose keys are the groupings returned by the + /// given closure and whose values are arrays of the elements that returned + /// each key. + /// + /// The arrays in the "values" position of the new dictionary each contain at + /// least one element, with the elements in the same order as the source + /// asynchronous sequence. + /// + /// - Parameters: + /// - values: An asynchronous sequence of values to group into a dictionary. + /// - keyForValue: A closure that returns a key for each element in + /// `values`. + @inlinable public init(grouping values: S, by keyForValue: (S.Element) async throws -> Key) async rethrows where Value == [S.Element] { self.init() for try await value in values { diff --git a/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift b/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift index d2d3f2fa..58f879e1 100644 --- a/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift +++ b/Sources/AsyncAlgorithms/RangeReplaceableCollection.swift @@ -10,6 +10,10 @@ //===----------------------------------------------------------------------===// extension RangeReplaceableCollection { + /// Creates a new instance of a collection containing the elements of an asynchronous sequence. + /// + /// - Parameter surce: The asynchronous sequence of elements for the new collection. + @inlinable public init(_ source: Source) async rethrows where Source.Element == Element { self.init() for try await item in source { diff --git a/Sources/AsyncAlgorithms/SetAlgebra.swift b/Sources/AsyncAlgorithms/SetAlgebra.swift index 9403de62..884fc02b 100644 --- a/Sources/AsyncAlgorithms/SetAlgebra.swift +++ b/Sources/AsyncAlgorithms/SetAlgebra.swift @@ -10,6 +10,12 @@ //===----------------------------------------------------------------------===// extension SetAlgebra { + /// Creates a new set from an asynchronous sequence of items. + /// + /// Use this initializer to create a new set from an asynchronous sequence + /// + /// - Parameter source: The elements to use as members of the new set. + @inlinable public init(_ source: Source) async rethrows where Source.Element == Element { self.init() for try await item in source { diff --git a/Tests/AsyncAlgorithmsTests/TestDictionary.swift b/Tests/AsyncAlgorithmsTests/TestDictionary.swift index 0c4576d8..d8fe329c 100644 --- a/Tests/AsyncAlgorithmsTests/TestDictionary.swift +++ b/Tests/AsyncAlgorithmsTests/TestDictionary.swift @@ -20,6 +20,20 @@ final class TestDictionary: XCTestCase { XCTAssertEqual(expected, actual) } + func test_throwing_uniqueKeysAndValues() async { + let source = Array([1, 2, 3, 4, 5, 6]) + let input = source.async.map { (value: Int) async throws -> (Int, Int) in + if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return (value, value) + } + do { + _ = try await Dictionary(uniqueKeysWithValues: input) + XCTFail() + } catch { + XCTAssertEqual((error as NSError).code, -1) + } + } + func test_uniqingWith() async { let source = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] let expected = Dictionary(source) { first, _ in first } @@ -27,10 +41,38 @@ final class TestDictionary: XCTestCase { XCTAssertEqual(expected, actual) } + func test_throwing_uniqingWith() async { + let source = Array([1, 2, 3, 4, 5, 6]) + let input = source.async.map { (value: Int) async throws -> (Int, Int) in + if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return (value, value) + } + do { + _ = try await Dictionary(input) { first, _ in first } + XCTFail() + } catch { + XCTAssertEqual((error as NSError).code, -1) + } + } + func test_grouping() async { let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] let expected = Dictionary(grouping: source, by: { $0.first! }) let actual = await Dictionary(grouping: source.async, by: { $0.first! }) XCTAssertEqual(expected, actual) } + + func test_throwing_grouping() async { + let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] + let input = source.async.map { (value: String) async throws -> String in + if value == "Kweku" { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return value + } + do { + _ = try await Dictionary(grouping: input, by: { $0.first! }) + XCTFail() + } catch { + XCTAssertEqual((error as NSError).code, -1) + } + } } diff --git a/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift b/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift index ad0838bc..ae5616e4 100644 --- a/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift +++ b/Tests/AsyncAlgorithmsTests/TestRangeReplacableCollection.swift @@ -40,4 +40,18 @@ final class TestRangeReplacableCollection: XCTestCase { let actual = await Array(source.async) XCTAssertEqual(expected, actual) } + + func test_throwing() async { + let source = Array([1, 2, 3, 4, 5, 6]) + let input = source.async.map { (value: Int) async throws -> Int in + if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return value + } + do { + _ = try await Array(input) + XCTFail() + } catch { + XCTAssertEqual((error as NSError).code, -1) + } + } } diff --git a/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift b/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift index e4aa27da..94acfff3 100644 --- a/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift +++ b/Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift @@ -33,4 +33,18 @@ final class TestSetAlgebra: XCTestCase { let actual = await IndexSet(source.async) XCTAssertEqual(expected, actual) } + + func test_throwing() async { + let source = Array([1, 2, 3, 4, 5, 6]) + let input = source.async.map { (value: Int) async throws -> Int in + if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } + return value + } + do { + _ = try await Set(input) + XCTFail() + } catch { + XCTAssertEqual((error as NSError).code, -1) + } + } } From 36d9c60dad96022150663af1e8556d4e95f2c96d Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 12 Jan 2022 14:47:11 -0800 Subject: [PATCH 3/4] Spelling fix of unique Co-authored-by: Nate Cook --- Tests/AsyncAlgorithmsTests/TestDictionary.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AsyncAlgorithmsTests/TestDictionary.swift b/Tests/AsyncAlgorithmsTests/TestDictionary.swift index d8fe329c..83600347 100644 --- a/Tests/AsyncAlgorithmsTests/TestDictionary.swift +++ b/Tests/AsyncAlgorithmsTests/TestDictionary.swift @@ -34,7 +34,7 @@ final class TestDictionary: XCTestCase { } } - func test_uniqingWith() async { + func test_uniquingWith() async { let source = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] let expected = Dictionary(source) { first, _ in first } let actual = await Dictionary(source.async) { first, _ in first } From 81c2c9536479c2ebd488ff3cc38fa184cba24418 Mon Sep 17 00:00:00 2001 From: Philippe Hausler Date: Wed, 12 Jan 2022 14:47:30 -0800 Subject: [PATCH 4/4] Spelling fix for unique (throwing version) Co-authored-by: Nate Cook --- Tests/AsyncAlgorithmsTests/TestDictionary.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AsyncAlgorithmsTests/TestDictionary.swift b/Tests/AsyncAlgorithmsTests/TestDictionary.swift index 83600347..bebfc974 100644 --- a/Tests/AsyncAlgorithmsTests/TestDictionary.swift +++ b/Tests/AsyncAlgorithmsTests/TestDictionary.swift @@ -41,7 +41,7 @@ final class TestDictionary: XCTestCase { XCTAssertEqual(expected, actual) } - func test_throwing_uniqingWith() async { + func test_throwing_uniquingWith() async { let source = Array([1, 2, 3, 4, 5, 6]) let input = source.async.map { (value: Int) async throws -> (Int, Int) in if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) }