From 4190dec46fc7db40d3d6e19e82bfb7d279897026 Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatyev <> Date: Wed, 10 Jan 2024 05:36:05 +0300 Subject: [PATCH 1/6] added partitionMap --- Sources/Algorithms/PartitionMap.swift | 203 ++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 Sources/Algorithms/PartitionMap.swift diff --git a/Sources/Algorithms/PartitionMap.swift b/Sources/Algorithms/PartitionMap.swift new file mode 100644 index 00000000..a7b40677 --- /dev/null +++ b/Sources/Algorithms/PartitionMap.swift @@ -0,0 +1,203 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020-2023 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// PartitionMapResult2 +//===----------------------------------------------------------------------===// + +// `PartitionMapResult` Types are needed because of current generic limitations. +// It is separated into public struct and internal enum. Such design has benefits +// in comparison to plain enum: +// - prevent its usage as a general purpose Either / OneOf Type – there are no +// public properties which makes it useless outside +// the library anywhere except with `partitionMap()` function. +// - allows to rename `first`, `second` and `third` without source breakage . +// If something more suitable will be found then old static initializers can be +// deprecated with introducing new ones. + +public struct PartitionMapResult2 { + @usableFromInline + internal let oneOf: _PartitionMapResult2 + + @usableFromInline + internal init(oneOf: _PartitionMapResult2) { self.oneOf = oneOf } + + @inlinable + public static func first(_ value: A) -> Self { + Self(oneOf: .first(value)) + } + + @inlinable + public static func second(_ value: B) -> Self { + Self(oneOf: .second(value)) + } +} + +@usableFromInline +internal enum _PartitionMapResult2 { + case first(A) + case second(B) +} + +//===----------------------------------------------------------------------===// +// PartitionMapResult3 +//===----------------------------------------------------------------------===// + +public struct PartitionMapResult3 { + @usableFromInline + internal let oneOf: _PartitionMapResult3 + + @usableFromInline + internal init(oneOf: _PartitionMapResult3) { + self.oneOf = oneOf + } + + @inlinable + public static func first(_ value: A) -> Self { + Self(oneOf: .first(value)) + } + + @inlinable + public static func second(_ value: B) -> Self { + Self(oneOf: .second(value)) + } + + @inlinable + public static func third(_ value: C) -> Self { + Self(oneOf: .third(value)) + } +} + +@usableFromInline +internal enum _PartitionMapResult3 { + case first(A) + case second(B) + case third(C) +} + +//===----------------------------------------------------------------------===// +// partitionMap() +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Allows to separate elements into distinct groups while applying a transformation to each element + /// + /// This method do the same as `partitioned(by:)` but with an added map step baked in for + /// ergonomic reasons. + /// + /// The `partitionMap` applies the given closure to each element of the collection and divides the + /// results into two groups based on the transformation's output. + /// The closure returns a `PartitionMapResult`, which indicates whether the result should be + /// included in the first group or in the second. + /// + /// In this example, `partitionMap(_:)` is used to separate an array of `any Error` elements into + /// two arrays while also transforming the type from + /// `any Error` to `URLSessionError` for the first group. + /// ``` + /// func handle(errors: [any Error]) { + /// let (recoverableErrors, unrecoverableErrors) = errors + /// .partitionMap { error -> PartitionMapResult2 in + /// switch error { + /// case let urlError as URLSessionError: return .first(urlError) + /// default: return .second(error) + /// } + /// } + /// // recoverableErrors Type is Array + /// // unrecoverableErrors Type is Array + /// } + /// ``` + /// + /// - Parameters: + /// - transform: A mapping closure. `transform` accepts an element of this sequence as its + /// parameter and returns a `PartitionMapResult` with a transformed value, representing + /// membership to either the first or second group with elements of the original or of a different type. + /// + /// - Returns: Two arrays, with elements from the first or second group appropriately. + /// + /// - Throws: Rethrows any errors produced by the `transform` closure. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + @inlinable + public func partitionMap( + _ transform: (Element) throws -> PartitionMapResult2 + ) rethrows -> ([A], [B]) { + var groupA: [A] = [] + var groupB: [B] = [] + + for element in self { + switch try transform(element).oneOf { + case .first(let a): groupA.append(a) + case .second(let b): groupB.append(b) + } + } + + return (groupA, groupB) + } + + /// Allows to separate elements into distinct groups while applying a transformation to each element + /// + /// This method do the same as `partitioned(by:)` but with an added map step baked in for + /// ergonomic reasons. + /// + /// The `partitionMap` applies the given closure to each element of the collection and divides the + /// results into distinct groups based on the transformation's output. + /// The closure returns a `PartitionMapResult`, which indicates whether the result should be + /// included in the first , second or third group. + /// + /// In this example, `partitionMap(_:)` is used to separate an array of `any Error` elements into + /// three arrays while also transforming the type from + /// `any Error` to `URLSessionError` for the first and second groups. + /// ``` + /// func handle(errors: [any Error]) { + /// let (recoverableErrors, unrecoverableErrors, unknownErrors) = errors + /// .partitionMap { error -> PartitionMapResult3 in + /// switch error { + /// case let urlError as URLSessionError: + /// return recoverableURLErrorCodes.contains(urlError.code) ? .first(urlError) : .second(urlError) + /// default: + /// return .third(error) + /// } + /// } + /// // recoverableErrors Type is Array + /// // unrecoverableErrors Type is Array + /// // unknownErrors Type is Array + /// } + /// ``` + /// + /// - Parameters: + /// - transform: A mapping closure. `transform` accepts an element of this sequence as its + /// parameter and returns a `PartitionMapResult` with a transformed value, representing + /// membership to either first, second or third group with elements of the original or of a different type. + /// + /// - Returns: Three arrays, with elements from the first, second or third group appropriately. + /// + /// - Throws: Rethrows any errors produced by the `transform` closure. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + @inlinable + public func partitionMap( + _ transform: (Element) throws -> PartitionMapResult3 + ) rethrows -> ([A], [B], [C]) { + var groupA: [A] = [] + var groupB: [B] = [] + var groupC: [C] = [] + + for element in self { + switch try transform(element).oneOf { + case .first(let a): groupA.append(a) + case .second(let b): groupB.append(b) + case .third(let c): groupC.append(c) + } + } + + return (groupA, groupB, groupC) + } +} From b49f41f383b6a752d67f4d1784b5f4bfbba236e3 Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatyev <> Date: Wed, 17 Jan 2024 00:49:30 +0300 Subject: [PATCH 2/6] add `partitionMap(_:)` tests (+1 squashed commit) Squashed commits: [6dcee60] add `partitionMap(_:)` tests --- Sources/Algorithms/PartitionMap.swift | 8 +- .../PartitionMapTests.swift | 148 ++++++++++++++++++ 2 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 Tests/SwiftAlgorithmsTests/PartitionMapTests.swift diff --git a/Sources/Algorithms/PartitionMap.swift b/Sources/Algorithms/PartitionMap.swift index a7b40677..7da0706e 100644 --- a/Sources/Algorithms/PartitionMap.swift +++ b/Sources/Algorithms/PartitionMap.swift @@ -27,8 +27,10 @@ public struct PartitionMapResult2 { @usableFromInline internal let oneOf: _PartitionMapResult2 - @usableFromInline - internal init(oneOf: _PartitionMapResult2) { self.oneOf = oneOf } + @inlinable + internal init(oneOf: _PartitionMapResult2) { + self.oneOf = oneOf + } @inlinable public static func first(_ value: A) -> Self { @@ -55,7 +57,7 @@ public struct PartitionMapResult3 { @usableFromInline internal let oneOf: _PartitionMapResult3 - @usableFromInline + @inlinable internal init(oneOf: _PartitionMapResult3) { self.oneOf = oneOf } diff --git a/Tests/SwiftAlgorithmsTests/PartitionMapTests.swift b/Tests/SwiftAlgorithmsTests/PartitionMapTests.swift new file mode 100644 index 00000000..811d80e9 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/PartitionMapTests.swift @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2023 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 Algorithms + +final class PartitionMapTests: XCTestCase { + func testPartitionMap2WithEmptyInput() { + let input: [Int] = [] + + let (first, second) = input.partitionMap { _ -> PartitionMapResult2 in + .first(0) + } + + XCTAssertTrue(first.isEmpty) + XCTAssertTrue(second.isEmpty) + } + + func testPartitionMap3WithEmptyInput() { + let input: [Int] = [] + + let (first, second, third) = input.partitionMap { _ -> PartitionMapResult3 in + .first(0) + } + + XCTAssertTrue(first.isEmpty) + XCTAssertTrue(second.isEmpty) + XCTAssertTrue(third.isEmpty) + } + + func testPartitionMap2Example() throws { + let nanString = String(describing: Double.nan) + let numericStrings = ["", "^", "-1", "0", "1", "-1.5", "1.5", nanString] + + let (doubles, unrepresentable) = numericStrings + .partitionMap { string -> PartitionMapResult2 in + if let double = Double(string) { + return .first(double) + } else { + return .second(string) + } + } + + XCTAssertEqual(doubles.map(String.init(describing:)), ["-1.0", "0.0", "1.0", "-1.5", "1.5", nanString]) + XCTAssertEqual(unrepresentable, ["", "^"]) + } + + func testPartitionMap3Example() throws { + let nanString = String(describing: Double.nan) + let numericStrings = ["", "^", "-1", "0", "1", "-1.5", "1.5", nanString] + + let (integers, doubles, unrepresentable) = numericStrings + .partitionMap { string -> PartitionMapResult3 in + if let integer = Int(string) { + return .first(integer) + } else if let double = Double(string) { + return .second(double) + } else { + return .third(string) + } + } + + XCTAssertEqual(integers, [-1, 0, 1]) + XCTAssertEqual(doubles.map(String.init(describing:)), ["-1.5", "1.5", nanString]) + XCTAssertEqual(unrepresentable, ["", "^"]) + } + + + func testPartitionMap2WithPredicate() throws { + let predicate: (Int) throws -> PartitionMapResult2 = { number -> PartitionMapResult2 in + if let uint = UInt8(exactly: number) { + return .second(uint) + } else if let int = Int8(exactly: number) { + return .first(int) + } else { + throw TestError() + } + } + + let s0 = try [1, 2, 3, 4].partitionMap(predicate) + let s1 = try [-1, 2, 3, 4].partitionMap(predicate) + let s2 = try [-1, 2, -3, 4].partitionMap(predicate) + let s3 = try [-1, 2, -3, -4].partitionMap(predicate) + + XCTAssertThrowsError(try [256].partitionMap(predicate)) + XCTAssertThrowsError(try [-129].partitionMap(predicate)) + + XCTAssertEqual(s0.0, []) + XCTAssertEqual(s0.1, [1, 2, 3, 4]) + + XCTAssertEqual(s1.0, [-1]) + XCTAssertEqual(s1.1, [2, 3, 4]) + + XCTAssertEqual(s2.0, [-1, -3]) + XCTAssertEqual(s2.1, [2, 4]) + + XCTAssertEqual(s3.0, [-1, -3, -4]) + XCTAssertEqual(s3.1, [2]) + } + + func testPartitionMap3WithPredicate() throws { + let predicate: (Int) throws -> PartitionMapResult3 = { number -> PartitionMapResult3 in + if number == 0 { + return .third(Void()) + } else if let uint = UInt8(exactly: number) { + return .second(uint) + } else if let int = Int8(exactly: number) { + return .first(int) + } else { + throw TestError() + } + } + + let s0 = try [0, 1, 2, 3, 4].partitionMap(predicate) + let s1 = try [0, 0, -1, 2, 3, 4].partitionMap(predicate) + let s2 = try [0, 0, -1, 2, -3, 4].partitionMap(predicate) + let s3 = try [0, -1, 2, -3, -4].partitionMap(predicate) + + XCTAssertThrowsError(try [256].partitionMap(predicate)) + XCTAssertThrowsError(try [-129].partitionMap(predicate)) + + XCTAssertEqual(s0.0, []) + XCTAssertEqual(s0.1, [1, 2, 3, 4]) + XCTAssertEqual(s0.2.count, 1) + + XCTAssertEqual(s1.0, [-1]) + XCTAssertEqual(s1.1, [2, 3, 4]) + XCTAssertEqual(s1.2.count, 2) + + XCTAssertEqual(s2.0, [-1, -3]) + XCTAssertEqual(s2.1, [2, 4]) + XCTAssertEqual(s2.2.count, 2) + + XCTAssertEqual(s3.0, [-1, -3, -4]) + XCTAssertEqual(s3.1, [2]) + XCTAssertEqual(s3.2.count, 1) + } + + private struct TestError: Error {} +} From 9cf2276b1eca82de11ee3bca46aff270a70e3acd Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatyev Date: Wed, 16 Apr 2025 11:57:33 +0300 Subject: [PATCH 3/6] prepare for merge request --- Guides/PartitionMap.md | 50 ++++++++++++++++ Sources/Algorithms/PartitionMap.swift | 82 ++++++++++++++++----------- 2 files changed, 99 insertions(+), 33 deletions(-) create mode 100644 Guides/PartitionMap.md diff --git a/Guides/PartitionMap.md b/Guides/PartitionMap.md new file mode 100644 index 00000000..d814ef20 --- /dev/null +++ b/Guides/PartitionMap.md @@ -0,0 +1,50 @@ +# Grouped + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/PartitionMap.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/PartitionMapTests.swift)] + +Groups up elements of a sequence into two Arrays while applying a transform closure for each element. + +```swift +func process(results: [Result]) { + let (successes, failures) = results + .partitionMap { result -> PartitionMapResult2 in + switch result { + case .success(let value): .first(value) + case .failure(let error): .second(error) + } + } +} +``` + +It is similar to some other grouping functions, but achives another goals. +- in comparison to `partitioned(by:)` it allows to make to make a transform for each element of the source sequence +independently for groups. Also it is possible to make more then 2 groups. +- in comparison to `grouped(by:)` & `split(whereSeparator:)` it has exact number of groups defined at compile time. +For `grouped(by:)` & `split(whereSeparator:)` number of groups is dynamicaly defined while program executiin. + +## Detailed Design + +The `partitionMap(_:)` method is declared as a `Sequence` extension returning a tuple with 2 or 3 arrays. +`([NewTypeA], [NewTypeB])`. + +```swift +extension Sequence { + public func partitionMap( + _ transform: (Element) throws(Error) -> PartitionMapResult3 + ) throws(Error) -> ([A], [B], [C]) +} +``` + +`PartitionMapResult` Types are needed because of current generic limitations. +It is separated into public struct and internal enum. Such design has benefits +in comparison to plain enum: +- prevent its usage as a general purpose Either / OneOf Type – there are no +public properties which makes it usable outside the library. +- allows to rename `first`, `second` and `third` without source breakage. +If something more suitable will be found in future then old static initializers can be +deprecated with introducing new ones. + +### Complexity + +Calling `partitionMap(_:)` is an O(_n_) operation. diff --git a/Sources/Algorithms/PartitionMap.swift b/Sources/Algorithms/PartitionMap.swift index 7da0706e..52f42f9a 100644 --- a/Sources/Algorithms/PartitionMap.swift +++ b/Sources/Algorithms/PartitionMap.swift @@ -13,16 +13,6 @@ // PartitionMapResult2 //===----------------------------------------------------------------------===// -// `PartitionMapResult` Types are needed because of current generic limitations. -// It is separated into public struct and internal enum. Such design has benefits -// in comparison to plain enum: -// - prevent its usage as a general purpose Either / OneOf Type – there are no -// public properties which makes it useless outside -// the library anywhere except with `partitionMap()` function. -// - allows to rename `first`, `second` and `third` without source breakage . -// If something more suitable will be found then old static initializers can be -// deprecated with introducing new ones. - public struct PartitionMapResult2 { @usableFromInline internal let oneOf: _PartitionMapResult2 @@ -100,20 +90,32 @@ extension Sequence { /// The closure returns a `PartitionMapResult`, which indicates whether the result should be /// included in the first group or in the second. /// - /// In this example, `partitionMap(_:)` is used to separate an array of `any Error` elements into - /// two arrays while also transforming the type from - /// `any Error` to `URLSessionError` for the first group. + /// Example 1: + /// ``` + /// func process(results: [Result]) { + /// let (successes, failures) = results + /// .partitionMap { result -> PartitionMapResult2 in + /// switch result { + /// case .success(let value): .first(value) + /// case .failure(let error): .second(error) + /// } + /// } + /// } + /// ``` + /// Example 2: + /// `partitionMap(_:)` is used to separate an array of `any Error` elements into two arrays while + /// also transforming the type from `any Error` to `URLSessionError` for the first group. /// ``` /// func handle(errors: [any Error]) { - /// let (recoverableErrors, unrecoverableErrors) = errors + /// let (urlSessionErrors, unknownErrors) = errors /// .partitionMap { error -> PartitionMapResult2 in /// switch error { - /// case let urlError as URLSessionError: return .first(urlError) - /// default: return .second(error) + /// case let urlError as URLSessionError: .first(urlError) + /// default: .second(error) /// } /// } - /// // recoverableErrors Type is Array - /// // unrecoverableErrors Type is Array + /// // `urlSessionErrors` Type is `Array` + /// // `unknownErrors` Type is `Array` /// } /// ``` /// @@ -128,9 +130,9 @@ extension Sequence { /// /// - Complexity: O(*n*), where *n* is the length of the collection. @inlinable - public func partitionMap( - _ transform: (Element) throws -> PartitionMapResult2 - ) rethrows -> ([A], [B]) { + public func partitionMap( + _ transform: (Element) throws(Error) -> PartitionMapResult2 + ) throws(Error) -> ([A], [B]) { var groupA: [A] = [] var groupB: [B] = [] @@ -153,24 +155,38 @@ extension Sequence { /// results into distinct groups based on the transformation's output. /// The closure returns a `PartitionMapResult`, which indicates whether the result should be /// included in the first , second or third group. - /// - /// In this example, `partitionMap(_:)` is used to separate an array of `any Error` elements into - /// three arrays while also transforming the type from + /// - Example 1: + /// ``` + /// func process(results: [Result]) { + /// let (successes, failures) = results + /// .partitionMap { result -> PartitionMapResult2 in + /// switch result { + /// case .success(let value): .first(value) + /// case .failure(let error): .second(error) + /// } + /// } + /// } + /// ``` + /// - Example 2: + /// `partitionMap(_:)` is used to separate an array of `any Error` elements into three arrays + /// while also transforming the type from /// `any Error` to `URLSessionError` for the first and second groups. /// ``` /// func handle(errors: [any Error]) { - /// let (recoverableErrors, unrecoverableErrors, unknownErrors) = errors + /// let (urlSessionErrors, httpErrors, unknownErrors) = errors /// .partitionMap { error -> PartitionMapResult3 in /// switch error { /// case let urlError as URLSessionError: - /// return recoverableURLErrorCodes.contains(urlError.code) ? .first(urlError) : .second(urlError) + /// .first(urlError) + /// case let httpError as HTTPError: + /// .second(urlError) /// default: - /// return .third(error) + /// .third(error) /// } /// } - /// // recoverableErrors Type is Array - /// // unrecoverableErrors Type is Array - /// // unknownErrors Type is Array + /// // `urlSessionErrors` Type `is Array` + /// // `httpErrors` Type is `Array` + /// // `unknownErrors` Type is `Array` /// } /// ``` /// @@ -185,9 +201,9 @@ extension Sequence { /// /// - Complexity: O(*n*), where *n* is the length of the collection. @inlinable - public func partitionMap( - _ transform: (Element) throws -> PartitionMapResult3 - ) rethrows -> ([A], [B], [C]) { + public func partitionMap( + _ transform: (Element) throws(Error) -> PartitionMapResult3 + ) throws(Error) -> ([A], [B], [C]) { var groupA: [A] = [] var groupB: [B] = [] var groupC: [C] = [] From 0aac37fbe7221d5ed1bb793bbf6b9a78c0f9bf57 Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatyev Date: Wed, 16 Apr 2025 12:02:40 +0300 Subject: [PATCH 4/6] fix typo --- Guides/PartitionMap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guides/PartitionMap.md b/Guides/PartitionMap.md index d814ef20..db9fcc54 100644 --- a/Guides/PartitionMap.md +++ b/Guides/PartitionMap.md @@ -1,4 +1,4 @@ -# Grouped +# PartitionMap [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/PartitionMap.swift) | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/PartitionMapTests.swift)] From 991d22a642df4d804f40a68341037029011b8a39 Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatyev Date: Wed, 16 Apr 2025 12:05:52 +0300 Subject: [PATCH 5/6] fix type --- Guides/PartitionMap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guides/PartitionMap.md b/Guides/PartitionMap.md index db9fcc54..3ee4fbf3 100644 --- a/Guides/PartitionMap.md +++ b/Guides/PartitionMap.md @@ -21,7 +21,7 @@ It is similar to some other grouping functions, but achives another goals. - in comparison to `partitioned(by:)` it allows to make to make a transform for each element of the source sequence independently for groups. Also it is possible to make more then 2 groups. - in comparison to `grouped(by:)` & `split(whereSeparator:)` it has exact number of groups defined at compile time. -For `grouped(by:)` & `split(whereSeparator:)` number of groups is dynamicaly defined while program executiin. +For `grouped(by:)` & `split(whereSeparator:)` number of groups is dynamicaly defined while program execution. ## Detailed Design From e51779ec9fc54516427467198cad3ad1982e35de Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatyev Date: Wed, 16 Apr 2025 16:23:15 +0300 Subject: [PATCH 6/6] comment added --- Guides/PartitionMap.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Guides/PartitionMap.md b/Guides/PartitionMap.md index 3ee4fbf3..6a781154 100644 --- a/Guides/PartitionMap.md +++ b/Guides/PartitionMap.md @@ -4,6 +4,7 @@ [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/PartitionMapTests.swift)] Groups up elements of a sequence into two Arrays while applying a transform closure for each element. +This method is a partition with an added map step baked in for ergonomic reasons. ```swift func process(results: [Result]) {