├── .spi.yml ├── .gitignore ├── Guides ├── Resources │ └── SortedPrefix │ │ ├── FewElements.png │ │ └── ManyElements.png ├── Indexed.md ├── Suffix.md ├── EndsWith.md ├── Compacted.md ├── Intersperse.md ├── AdjacentPairs.md ├── Chain.md ├── Windows.md ├── Unique.md ├── Cycle.md ├── Rotate.md ├── Stride.md ├── FirstNonNil.md ├── Product.md ├── Split.md ├── Keyed.md ├── Grouped.md ├── Combinations.md ├── Joined.md ├── MinMax.md ├── Partition.md ├── Permutations.md ├── Reductions.md └── Chunked.md ├── Xcode ├── Algorithms.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Algorithms.xcscheme ├── README.md ├── Algorithms.xctestplan ├── AlgorithmsTests.xcconfig ├── Algorithms.xcconfig └── Shared.xcconfig ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── FEATURE_REQUEST.md │ └── BUG_REPORT.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── pull_request.yml └── PULL_REQUEST_TEMPLATE │ └── NEW.md ├── Sources └── Algorithms │ ├── Documentation.docc │ ├── Keying.md │ ├── DeprecatedScan.md │ ├── Sampling.md │ ├── Selecting.md │ ├── MinAndMax.md │ ├── Extending.md │ ├── CombinationsPermutations.md │ ├── SlicingSplitting.md │ ├── Reductions.md │ ├── Joining.md │ ├── Trimming.md │ ├── Filtering.md │ ├── Chunking.md │ ├── Algorithms.md │ └── Partitioning.md │ ├── Grouped.swift │ ├── FirstNonNil.swift │ ├── Keyed.swift │ ├── EndsWith.swift │ ├── Indexed.swift │ ├── Suffix.swift │ ├── Unique.swift │ └── Compacted.swift ├── .license_header_template ├── .licenseignore ├── CONTRIBUTING.md ├── Scripts └── format.sh ├── Tests └── SwiftAlgorithmsTests │ ├── FirstNonNilTests.swift │ ├── IndexedTests.swift │ ├── CompactedTests.swift │ ├── GroupedTests.swift │ ├── UniqueTests.swift │ ├── ProductTests.swift │ ├── SuffixTests.swift │ ├── ChainTests.swift │ ├── EndsWithTests.swift │ ├── CycleTests.swift │ ├── AdjacentPairsTests.swift │ ├── KeyedTests.swift │ ├── IntersperseTests.swift │ ├── RotateTests.swift │ ├── TrimTests.swift │ ├── WindowsTests.swift │ ├── ReductionsTests.swift │ ├── UniquePermutationsTests.swift │ ├── RandomSampleTests.swift │ ├── JoinedTests.swift │ ├── StrideTests.swift │ └── CombinationsTests.swift ├── Package.swift ├── .swift-format ├── README.md └── CODE_OF_CONDUCT.md /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Algorithms] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | /.swiftpm 7 | Package.resolved 8 | -------------------------------------------------------------------------------- /Guides/Resources/SortedPrefix/FewElements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-algorithms/HEAD/Guides/Resources/SortedPrefix/FewElements.png -------------------------------------------------------------------------------- /Guides/Resources/SortedPrefix/ManyElements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-algorithms/HEAD/Guides/Resources/SortedPrefix/ManyElements.png -------------------------------------------------------------------------------- /Xcode/Algorithms.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ Discussion Forum 4 | url: https://forums.swift.org/c/related-projects/algorithms/ 5 | about: Questions about using Swift Algorithms? Ask here! 6 | -------------------------------------------------------------------------------- /Xcode/Algorithms.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Keying.md: -------------------------------------------------------------------------------- 1 | # Keying and Grouping 2 | 3 | Convert a sequence to a dictionary, providing keys to individual elements or to use as grouping values. 4 | 5 | ## Topics 6 | 7 | ### Creating a Keyed Dictionary 8 | 9 | - ``Swift/Sequence/keyed(by:)`` 10 | - ``Swift/Sequence/keyed(by:resolvingConflictsWith:)`` 11 | 12 | ### Grouping Elements by Key 13 | 14 | - ``Swift/Sequence/grouped(by:)`` 15 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/DeprecatedScan.md: -------------------------------------------------------------------------------- 1 | # DeprecatedScan 2 | 3 | These methods are deprecated; use the `reductions` family of methods instead. 4 | 5 | See: 6 | 7 | ## Topics 8 | 9 | - ``Swift/Sequence/scan(_:)`` 10 | - ``Swift/Sequence/scan(_:_:)`` 11 | - ``Swift/Sequence/scan(into:_:)`` 12 | - ``Swift/LazySequenceProtocol/scan(_:)`` 13 | - ``Swift/LazySequenceProtocol/scan(_:_:)`` 14 | - ``Swift/LazySequenceProtocol/scan(into:_:)`` 15 | -------------------------------------------------------------------------------- /.license_header_template: -------------------------------------------------------------------------------- 1 | @@===----------------------------------------------------------------------===@@ 2 | @@ 3 | @@ This source file is part of the Swift Algorithms open source project 4 | @@ 5 | @@ Copyright (c) YEARS 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 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | **/.gitignore 3 | .licenseignore 4 | .gitattributes 5 | .git-blame-ignore-revs 6 | .mailfilter 7 | .mailmap 8 | .spi.yml 9 | .swift-format 10 | .editorconfig 11 | .github/* 12 | *.md 13 | *.mdoc 14 | *.txt 15 | *.yml 16 | *.yaml 17 | *.json 18 | *.png 19 | *.bash 20 | *.cmake 21 | *.cmake.in 22 | Package.swift 23 | **/Package.swift 24 | Package@*.swift 25 | **/Package@*.swift 26 | Package.resolved 27 | **/Package.resolved 28 | .unacceptablelanguageignore 29 | **/Snapshots/* 30 | Xcode/* 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By submitting a pull request, you represent that you have the right to license 2 | your contribution to Apple and the community, and agree by submitting the patch 3 | that your contributions are licensed under the [Swift 4 | license](https://swift.org/LICENSE.txt). 5 | 6 | --- 7 | 8 | Before submitting the pull request, please make sure you have tested your 9 | changes and that they follow the Swift project [guidelines for contributing 10 | code](https://swift.org/contributing/#contributing-code). 11 | -------------------------------------------------------------------------------- /Xcode/README.md: -------------------------------------------------------------------------------- 1 | # Xcode build files 2 | 3 | The project file here can be used to build a variant of this package with Xcode, producing a single framework bundle. Build settings are entirely configured via the provided xcconfig files. 4 | 5 | Beware! The contents of this directory are not source stable. They are provided as is, with no compatibility promises across package releases. Future versions of this package can arbitrarily change these files or remove them, without any advance notice. (This can include patch releases.) 6 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Sampling.md: -------------------------------------------------------------------------------- 1 | # Random Sampling 2 | 3 | Choose a specified number of random elements from a sequence or collection. 4 | 5 | ## Topics 6 | 7 | ### Random Sampling 8 | 9 | - ``Swift/Sequence/randomSample(count:)`` 10 | - ``Swift/Collection/randomSample(count:)`` 11 | - ``Swift/Collection/randomStableSample(count:)`` 12 | 13 | ### Random Sampling with a Generator 14 | 15 | - ``Swift/Sequence/randomSample(count:using:)`` 16 | - ``Swift/Collection/randomSample(count:using:)`` 17 | - ``Swift/Collection/randomStableSample(count:using:)`` 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: A suggestion for a new feature 4 | --- 5 | 6 | 12 | 13 | Replace this paragraph with a description of your proposed feature. Code samples that show what's missing, or what new capabilities will be possible, are very helpful! Provide links to existing issues or external references/discussions, if appropriate. -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Selecting.md: -------------------------------------------------------------------------------- 1 | # Selecting Elements 2 | 3 | Select elements at a particular interval, the first mapped value, 4 | or iterate of elements with their indices. 5 | 6 | ## Topics 7 | 8 | ### Selecting Elements at an Interval 9 | 10 | - ``Swift/Sequence/striding(by:)`` 11 | - ``Swift/Collection/striding(by:)`` 12 | 13 | ### Conditionally Finding the First Mapped Value 14 | 15 | - ``Swift/Sequence/firstNonNil(_:)`` 16 | 17 | ### Iterating Over Elements with Their Indices 18 | 19 | - ``Swift/Collection/indexed()`` 20 | 21 | ### Supporting Types 22 | 23 | - ``IndexedCollection`` 24 | - ``StridingSequence`` 25 | - ``StridingCollection`` 26 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/MinAndMax.md: -------------------------------------------------------------------------------- 1 | # Finding the Minimum and Maximum 2 | 3 | Find the minimum and maximum elements simultaneously, 4 | or a specific number of elements at the minimum and maximum. 5 | 6 | ## Topics 7 | 8 | ### Finding Minimum or Maximum Elements 9 | 10 | - ``Swift/Sequence/min(count:)`` 11 | - ``Swift/Collection/min(count:)`` 12 | - ``Swift/Sequence/min(count:sortedBy:)`` 13 | - ``Swift/Collection/min(count:sortedBy:)`` 14 | - ``Swift/Sequence/max(count:)`` 15 | - ``Swift/Collection/max(count:)`` 16 | - ``Swift/Sequence/max(count:sortedBy:)`` 17 | - ``Swift/Collection/max(count:sortedBy:)`` 18 | 19 | ### Finding the Minimum and Maximum Elements Simultaneously 20 | 21 | - ``Swift/Sequence/minAndMax()`` 22 | - ``Swift/Sequence/minAndMax(by:)`` 23 | 24 | -------------------------------------------------------------------------------- /Xcode/Algorithms.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "CF37E70B-D810-4CF2-AB41-571BAFF72572", 5 | "name" : "Default", 6 | "options" : { 7 | 8 | } 9 | }, 10 | { 11 | "id" : "33436295-E24E-439B-9B8E-4602E0A6C8BB", 12 | "name" : "TSan", 13 | "options" : { 14 | "threadSanitizerEnabled" : true 15 | } 16 | } 17 | ], 18 | "defaultOptions" : { 19 | "codeCoverage" : false 20 | }, 21 | "testTargets" : [ 22 | { 23 | "parallelizable" : true, 24 | "target" : { 25 | "containerPath" : "container:Algorithms.xcodeproj", 26 | "identifier" : "7D489E4629CE969D00499B21", 27 | "name" : "AlgorithmsTests" 28 | } 29 | } 30 | ], 31 | "version" : 1 32 | } 33 | -------------------------------------------------------------------------------- /Scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the Swift Algorithms open source project 5 | ## 6 | ## Copyright (c) 2025 Apple Inc. and the Swift project authors 7 | ## Licensed under Apache License v2.0 with Runtime Library Exception 8 | ## 9 | ## See https://swift.org/LICENSE.txt for license information 10 | ## 11 | ##===----------------------------------------------------------------------===## 12 | 13 | # Move to the project root 14 | cd "$(dirname "$0")" || exit 15 | cd .. 16 | echo "Formatting Swift sources in $(pwd)" 17 | 18 | # Run the format / lint commands 19 | git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place 20 | git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel 21 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Extending.md: -------------------------------------------------------------------------------- 1 | # Extending 2 | 3 | Chain two collections end-to-end, 4 | or repeat a collection forever or a specific number of times. 5 | 6 | ## Overview 7 | 8 | _Chaining_ two collections 9 | 10 | ```swift 11 | let letters = chain("abcd", "EFGH") 12 | // String(letters) == "abcdEFGH" 13 | 14 | for (num, letter) in zip((1...3).cycled(), letters) { 15 | print(num, letter) 16 | } 17 | // 1 a 18 | // 2 b 19 | // 3 c 20 | // 1 d 21 | // 2 E 22 | // 3 F 23 | // 1 G 24 | // 2 H 25 | ``` 26 | 27 | ## Topics 28 | 29 | ### Chaining Two Collections 30 | 31 | - ``chain(_:_:)`` 32 | 33 | ### Cycling a Collection 34 | 35 | - ``Swift/Collection/cycled()`` 36 | - ``Swift/Collection/cycled(times:)`` 37 | 38 | ### Supporting Types 39 | 40 | - ``Chain2Sequence`` 41 | - ``CycledSequence`` 42 | - ``CycledTimesCollection`` 43 | -------------------------------------------------------------------------------- /Xcode/AlgorithmsTests.xcconfig: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2023 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 | PRODUCT_NAME = AlgorithmsTests 13 | PRODUCT_BUNDLE_IDENTIFIER = org.swift.AlgorithmsTests 14 | 15 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator appletvos appletvsimulator 16 | ARCHS = $(ARCHS_STANDARD) 17 | 18 | CURRENT_PROJECT_VERSION = 1 19 | MARKETING_VERSION = 1.0 20 | 21 | GENERATE_INFOPLIST_FILE = YES 22 | 23 | ENABLE_TESTABILITY = NO 24 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/CombinationsPermutations.md: -------------------------------------------------------------------------------- 1 | # Combinations and Permutations 2 | 3 | Find the combinations and permutations of any collection's elements, 4 | or the product of two different collections. 5 | 6 | ## Topics 7 | 8 | ### Combinations 9 | 10 | - ``Swift/Collection/combinations(ofCount:)-26o4x`` 11 | - ``Swift/Collection/combinations(ofCount:)-53jql`` 12 | 13 | ### Permutations 14 | 15 | - ``Swift/Collection/permutations(ofCount:)-7rc99`` 16 | - ``Swift/Collection/permutations(ofCount:)-5zvhn`` 17 | 18 | ### Unique Permutations 19 | 20 | - ``Swift/Collection/uniquePermutations(ofCount:)-2extq`` 21 | - ``Swift/Collection/uniquePermutations(ofCount:)-48r1k`` 22 | 23 | ### Product 24 | 25 | - ``product(_:_:)`` 26 | 27 | ### Supporting Types 28 | 29 | - ``CombinationsSequence`` 30 | - ``PermutationsSequence`` 31 | - ``UniquePermutationsSequence`` 32 | - ``Product2Sequence`` 33 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/FirstNonNilTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class FirstNonNilTests: XCTestCase { 16 | func testFirstNonNil() { 17 | XCTAssertNil([].firstNonNil { $0 }) 18 | XCTAssertNil(["A", "B", "C"].firstNonNil { Int($0) }) 19 | XCTAssertNil(["A", "B", "C"].firstNonNil { _ in nil }) 20 | XCTAssertEqual(["A", "B", "10"].firstNonNil { Int($0) }, 10) 21 | XCTAssertEqual(["20", "B", "10"].firstNonNil { Int($0) }, 20) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | Replace this paragraph with a description of your changes and rationale. Provide links to an existing issue or external references/discussions, if appropriate. 16 | 17 | ### Checklist 18 | - [ ] I've added at least one test that validates that my change is working, if appropriate 19 | - [ ] I've followed the code style of the rest of the project 20 | - [ ] I've read the [Contribution Guidelines](../blob/main/CONTRIBUTING.md) 21 | - [ ] I've updated the documentation if necessary 22 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/SlicingSplitting.md: -------------------------------------------------------------------------------- 1 | # Slicing and Splitting 2 | 3 | Iterate over tuple pairs of adjacent elements, overlapping windows of a specified size, or lazily-calculated splits. 4 | 5 | ## Topics 6 | 7 | ### Adjacent Pairs 8 | 9 | - ``Swift/Sequence/adjacentPairs()`` 10 | - ``Swift/Collection/adjacentPairs()`` 11 | 12 | ### Windows 13 | 14 | - ``Swift/Collection/windows(ofCount:)`` 15 | 16 | ### Lazily Splitting a Collection 17 | 18 | - ``Swift/LazySequenceProtocol/split(separator:maxSplits:omittingEmptySubsequences:)-4q4x8`` 19 | - ``Swift/LazySequenceProtocol/split(maxSplits:omittingEmptySubsequences:whereSeparator:)-68oqf`` 20 | - ``Swift/LazySequenceProtocol/split(separator:maxSplits:omittingEmptySubsequences:)-a46s`` 21 | - ``Swift/LazySequenceProtocol/split(maxSplits:omittingEmptySubsequences:whereSeparator:)-3rwee`` 22 | 23 | ### Supporting Types 24 | 25 | - ``AdjacentPairsSequence`` 26 | - ``AdjacentPairsCollection`` 27 | - ``WindowsOfCountCollection`` 28 | - ``SplitSequence`` 29 | - ``SplitCollection`` 30 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | validate_format_config: 12 | name: Validate Format Config 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | - name: Install apt dependencies 18 | run: sudo apt-get -qq update && sudo apt-get -qq -y install curl 19 | - name: Compare against swift-mmio swift-format config 20 | run: | 21 | curl -sL https://raw.githubusercontent.com/apple/swift-mmio/refs/heads/main/.swift-format -o .swift-format-mmio 22 | diff .swift-format .swift-format-mmio 23 | 24 | tests: 25 | name: Test 26 | uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main 27 | with: 28 | enable_wasm_sdk_build: true 29 | 30 | soundness: 31 | name: Soundness 32 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 33 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Reductions.md: -------------------------------------------------------------------------------- 1 | # Reductions 2 | 3 | Find the incremental values of a sequence "reduce" operation. 4 | 5 | ## Overview 6 | 7 | Call one of the `reductions` methods when you want the result of a `reduce` operation along with all of its intermediate values: 8 | 9 | ```swift 10 | let exclusiveRunningTotal = (1...5).reductions(0, +) 11 | print(exclusiveRunningTotal) 12 | // prints [0, 1, 3, 6, 10, 15] 13 | 14 | let inclusiveRunningTotal = (1...5).reductions(+) 15 | print(inclusiveRunningTotal) 16 | // prints [1, 3, 6, 10, 15] 17 | ``` 18 | 19 | ## Topics 20 | 21 | - ``Swift/Sequence/reductions(_:)`` 22 | - ``Swift/Sequence/reductions(_:_:)`` 23 | - ``Swift/Sequence/reductions(into:_:)`` 24 | - ``Swift/LazySequenceProtocol/reductions(_:)`` 25 | - ``Swift/LazySequenceProtocol/reductions(_:_:)`` 26 | - ``Swift/LazySequenceProtocol/reductions(into:_:)`` 27 | 28 | ### Supporting Types 29 | 30 | - ``InclusiveReductionsSequence`` 31 | - ``ExclusiveReductionsSequence`` 32 | 33 | ### Deprecated Methods 34 | 35 | - 36 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Joining.md: -------------------------------------------------------------------------------- 1 | # Joining 2 | 3 | Join the parts of a collection of collections, 4 | providing a connecting element or collection, 5 | or a closure that produces the connector. 6 | 7 | ## Topics 8 | 9 | ### Joining by an Element 10 | 11 | - ``Swift/Sequence/joined(by:)-6mrf9`` 12 | - ``Swift/Sequence/joined(by:)-9hyaf`` 13 | - ``Swift/Collection/joined(by:)-430ue`` 14 | - ``Swift/LazySequenceProtocol/joined(by:)-3yjw0`` 15 | - ``Swift/LazySequenceProtocol/joined(by:)-47xvy`` 16 | 17 | ### Joining by a Collection 18 | 19 | - ``Swift/Sequence/joined(by:)-62j1h`` 20 | - ``Swift/Sequence/joined(by:)-9b108`` 21 | - ``Swift/Collection/joined(by:)-28n3b`` 22 | - ``Swift/LazySequenceProtocol/joined(by:)-4neii`` 23 | - ``Swift/LazySequenceProtocol/joined(by:)-49xws`` 24 | 25 | ### Interspersing Elements 26 | 27 | - ``Swift/Sequence/interspersed(with:)`` 28 | 29 | ### Supporting Types 30 | 31 | - ``JoinedBySequence`` 32 | - ``JoinedByCollection`` 33 | - ``JoinedByClosureSequence`` 34 | - ``JoinedByClosureCollection`` 35 | - ``InterspersedSequence`` 36 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Trimming.md: -------------------------------------------------------------------------------- 1 | # Trimming 2 | 3 | Remove unwanted elements from the start, the end, or both ends of a collection. 4 | 5 | ## Topics 6 | 7 | ### Trimming Both Ends of a Collection 8 | 9 | - ``Swift/BidirectionalCollection/trimming(while:)`` 10 | - ``Swift/BidirectionalCollection/trim(while:)-781az`` 11 | - ``Swift/BidirectionalCollection/trim(while:)-6cbz3`` 12 | 13 | ### Trimming from the Start 14 | 15 | - ``Swift/Collection/trimmingPrefix(while:)`` 16 | - ``Swift/Collection/trimPrefix(while:)-2r8n8`` 17 | - ``Swift/Collection/trimPrefix(while:)-93obt`` 18 | 19 | ### Trimming from the End 20 | 21 | - ``Swift/BidirectionalCollection/trimmingSuffix(while:)`` 22 | - ``Swift/BidirectionalCollection/trimSuffix(while:)-33ubj`` 23 | - ``Swift/BidirectionalCollection/trimSuffix(while:)-3o6x9`` 24 | 25 | ### Finding the Suffix of a Collection 26 | 27 | - ``Swift/BidirectionalCollection/suffix(while:)`` 28 | 29 | ### Finding Boundaries within a Collection 30 | 31 | - ``Swift/Collection/endOfPrefix(while:)`` 32 | - ``Swift/BidirectionalCollection/startOfSuffix(while:)`` 33 | -------------------------------------------------------------------------------- /Guides/Indexed.md: -------------------------------------------------------------------------------- 1 | # Indexed 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Indexed.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/IndexedTests.swift)] 5 | 6 | The `enumerated` method, but pairing each element with its index instead of an 7 | incrementing integer counter. 8 | 9 | This is essentially equivalent to `zip(x.indices, x)`: 10 | 11 | ```swift 12 | let numbers = [10, 20, 30, 40, 50] 13 | var matchingIndices: Set = [] 14 | for (i, n) in numbers.indexed() { 15 | if n.isMultiple(of: 20) { 16 | matchingIndices.insert(i) 17 | } 18 | } 19 | // matchingIndices == [1, 3] 20 | ``` 21 | 22 | ## Detailed Design 23 | 24 | The `indexed` method returns an `IndexedCollection` type: 25 | 26 | ```swift 27 | extension Collection { 28 | func indexed() -> IndexedCollection 29 | } 30 | ``` 31 | 32 | `IndexedCollection` scales from a collection up to a random-access collection, 33 | depending on its base type. `Indexed` also conforms to `LazySequenceProtocol` 34 | when the base type conforms. 35 | 36 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the Swift Algorithms open source project 5 | // 6 | // Copyright (c) 2020 Apple Inc. and the Swift project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See https://swift.org/LICENSE.txt for license information 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | import PackageDescription 14 | 15 | let package = Package( 16 | name: "swift-algorithms", 17 | products: [ 18 | .library( 19 | name: "Algorithms", 20 | targets: ["Algorithms"]) 21 | ], 22 | dependencies: [ 23 | .package(url: "https://github.com/apple/swift-numerics.git", from: "1.0.0") 24 | ], 25 | targets: [ 26 | .target( 27 | name: "Algorithms", 28 | dependencies: [ 29 | .product(name: "RealModule", package: "swift-numerics") 30 | ]), 31 | .testTarget( 32 | name: "SwiftAlgorithmsTests", 33 | dependencies: ["Algorithms"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | Remove duplicated elements or strip the `nil` values from a sequence or collection. 4 | 5 | ## Overview 6 | 7 | Use the _uniquing_ methods to remove duplicates from a sequence or collection, or to remove elements that have a duplicated property. 8 | 9 | ```swift 10 | let numbers = [1, 2, 3, 3, 2, 3, 3, 2, 2, 2, 1] 11 | 12 | let unique = numbers.uniqued() 13 | // Array(unique) == [1, 2, 3] 14 | ``` 15 | 16 | The `compacted()` method removes all `nil` values from a sequence or collection of optionals: 17 | 18 | ```swift 19 | let array: [Int?] = [10, nil, 30, nil, 2, 3, nil, 5] 20 | let withNoNils = array.compacted() 21 | // Array(withNoNils) == [10, 30, 2, 3, 5] 22 | ``` 23 | 24 | ## Topics 25 | 26 | ### Uniquing Elements 27 | 28 | - ``Swift/Sequence/uniqued()`` 29 | - ``Swift/Sequence/uniqued(on:)`` 30 | - ``Swift/LazySequenceProtocol/uniqued(on:)`` 31 | 32 | ### Filtering out `nil` Elements 33 | 34 | - ``Swift/Collection/compacted()`` 35 | - ``Swift/Sequence/compacted()`` 36 | 37 | ### Supporting Types 38 | 39 | - ``UniquedSequence`` 40 | - ``CompactedSequence`` 41 | - ``CompactedCollection`` 42 | -------------------------------------------------------------------------------- /Sources/Algorithms/Grouped.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 Sequence { 13 | /// Groups elements of this sequence into a new dictionary, 14 | /// whose values are arrays of grouped elements, 15 | /// each keyed by the key returned by the given closure. 16 | /// 17 | /// - Parameter keyForValue: A closure that returns a key for each element 18 | /// in the sequence. 19 | /// - Returns: A dictionary containing grouped elements of self, keyed by 20 | /// the keys derived by the `keyForValue` closure. 21 | @inlinable 22 | public func grouped( 23 | by keyForValue: (Element) throws -> GroupKey 24 | ) rethrows -> [GroupKey: [Element]] { 25 | try Dictionary(grouping: self, by: keyForValue) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/IndexedTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class IndexedTests: XCTestCase { 16 | func testIndexed() { 17 | let s = "ABCDEFGHIJKLMNOP" 18 | let si = s.indexed() 19 | 20 | XCTAssertEqual(s.startIndex, si.first!.index) 21 | XCTAssertEqual("A", si.first!.element) 22 | XCTAssertEqual(s.index(before: s.endIndex), si.last!.index) 23 | XCTAssertEqual("P", si.last!.element) 24 | 25 | let indexOfG = si.first(where: { $0.element == "G" })!.index 26 | XCTAssertEqual("G", s[indexOfG]) 27 | let indexOfI = si.last(where: { $0.element == "I" })!.index 28 | XCTAssertEqual("I", s[indexOfI]) 29 | } 30 | 31 | func testIndexedLazy() { 32 | requireLazyCollection("ABCD".lazy.indexed()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Something isn't working as expected 4 | --- 5 | 6 | 16 | 17 | Replace this paragraph with a short description of the incorrect incorrect behavior. If this is a regression, please note the last version that the behavior was correct in addition to your current version. 18 | 19 | **Swift Algorithms version:** `1.0.0` or the `main` branch, for example. 20 | **Swift version:** Paste the output of `swift --version` here. 21 | 22 | ### Checklist 23 | - [ ] If possible, I've reproduced the issue using the `main` branch of this package 24 | - [ ] I've searched for [existing GitHub issues](https://github.com/apple/swift-algorithms/issues) 25 | 26 | ### Steps to Reproduce 27 | Replace this paragraph with an explanation of how to reproduce the incorrect behavior. Include a simple code example, if possible. 28 | 29 | ### Expected behavior 30 | Describe what you expect to happen. 31 | 32 | ### Actual behavior 33 | Describe or copy/paste the behavior you observe. 34 | -------------------------------------------------------------------------------- /Xcode/Algorithms.xcconfig: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2023 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 | PRODUCT_NAME = Algorithms 13 | PRODUCT_BUNDLE_IDENTIFIER = org.swift.Algorithms 14 | 15 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator appletvos appletvsimulator 16 | ARCHS = $(ARCHS_STANDARD) 17 | 18 | MACOSX_DEPLOYMENT_TARGET = 12.0 19 | IPHONEOS_DEPLOYMENT_TARGET = 15.0 20 | WATCHOS_DEPLOYMENT_TARGET = 8.0 21 | TVOS_DEPLOYMENT_TARGET = 15.0 22 | 23 | MARKETING_VERSION = 1.2 24 | 25 | CURRENT_PROJECT_VERSION = 1 26 | VERSIONING_SYSTEM = apple-generic 27 | VERSION_INFO_PREFIX = 28 | DYLIB_COMPATIBILITY_VERSION = $(CURRENT_PROJECT_VERSION) 29 | DYLIB_CURRENT_VERSION = $(CURRENT_PROJECT_VERSION) 30 | 31 | INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks 32 | SKIP_INSTALL = YES 33 | DYLIB_INSTALL_NAME_BASE = @rpath 34 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks 35 | 36 | ENABLE_TESTABILITY = NO 37 | ENABLE_TESTABILITY[config=Debug] = YES 38 | 39 | GENERATE_INFOPLIST_FILE = YES 40 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Chunking.md: -------------------------------------------------------------------------------- 1 | # Chunking 2 | 3 | Break collections into consecutive chunks by length, count, or based on closure-based logic. 4 | 5 | ## Overview 6 | 7 | _Chunking_ is the process of breaking a collection into consecutive subsequences, without dropping or duplicating any of the collection's elements. After chunking a collection, joining the resulting subsequences produces the original collection of elements, unlike _splitting_, which consumes the separator element(s). 8 | 9 | ```swift 10 | let names = ["Ji-sun", "Jin-su", "Min-jae", "Young-ho"] 11 | let evenlyChunked = names.chunks(ofCount: 2) 12 | // ~ [["Ji-sun", "Jin-su"], ["Min-jae", "Young-ho"]] 13 | 14 | let chunkedByFirstLetter = names.chunked(on: \.first) 15 | // equivalent to [("J", ["Ji-sun", "Jin-su"]), ("M", ["Min-jae"]), ("Y", ["Young-ho"])] 16 | ``` 17 | 18 | ## Topics 19 | 20 | ### Chunking a Collection by Count 21 | 22 | - ``Swift/Collection/chunks(ofCount:)`` 23 | - ``Swift/Collection/evenlyChunked(in:)`` 24 | 25 | ### Chunking a Collection by Predicate 26 | 27 | - ``Swift/Collection/chunked(by:)`` 28 | - ``Swift/LazySequenceProtocol/chunked(by:)`` 29 | 30 | ### Chunking a Collection by Subject 31 | 32 | - ``Swift/Collection/chunked(on:)`` 33 | - ``Swift/LazySequenceProtocol/chunked(on:)`` 34 | 35 | ### Supporting Types 36 | 37 | - ``ChunkedByCollection`` 38 | - ``ChunkedOnCollection`` 39 | - ``ChunksOfCountCollection`` 40 | - ``EvenlyChunkedCollection`` 41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/NEW.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ### Description 13 | Replace this paragraph with a description of your changes and rationale. Provide links to an existing issue or external references/discussions, if appropriate. 14 | 15 | ### Detailed Design 16 | Include any additional information about the design here. At minimum, show any new API: 17 | 18 | ```swift 19 | extension Collection { 20 | /// The new feature implemented by this pull request. 21 | func newFeature() 22 | } 23 | ``` 24 | 25 | ### Documentation Plan 26 | How has the new feature been documented? Have the relevant portions of the guides been updated in addition to symbol-level documentation? 27 | 28 | ### Test Plan 29 | How is the new feature tested? 30 | 31 | ### Source Impact 32 | What is the impact of this change on existing users? Does it deprecate or remove any existing API? 33 | 34 | ### Checklist 35 | - [ ] I've added at least one test that validates that my change is working, if appropriate 36 | - [ ] I've followed the code style of the rest of the project 37 | - [ ] I've read the [Contribution Guidelines](../../blob/main/CONTRIBUTING.md) 38 | - [ ] I've updated the documentation if necessary 39 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/CompactedTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | import Algorithms 13 | import XCTest 14 | 15 | final class CompactedTests: XCTestCase { 16 | 17 | let tests: [[Int?]] = 18 | [nil, nil, nil, 0, 1, 2] 19 | .uniquePermutations(ofCount: 0...) 20 | .map(Array.init) 21 | 22 | func testCompactedCompacted() { 23 | for collection in self.tests { 24 | let seq = AnySequence(collection) 25 | expectEqualSequences( 26 | seq.compactMap({ $0 }), seq.compacted()) 27 | expectEqualSequences( 28 | collection.compactMap({ $0 }), collection.compacted()) 29 | } 30 | } 31 | 32 | func testCompactedBidirectionalCollection() { 33 | for array in self.tests { 34 | expectEqualSequences( 35 | array.compactMap({ $0 }).reversed(), 36 | array.compacted().reversed()) 37 | } 38 | } 39 | 40 | func testCollectionTraversals() { 41 | let validator = IndexValidator>() 42 | for array in self.tests { 43 | validator.validate(array.compacted()) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/GroupedTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class GroupedTests: XCTestCase { 16 | private final class SampleError: Error {} 17 | 18 | // Based on https://github.com/apple/swift/blob/4d1d8a9de5ebc132a17aee9fc267461facf89bf8/validation-test/stdlib/Dictionary.swift#L1974-L1988 19 | 20 | func testGroupedBy() { 21 | let r = 0..<10 22 | 23 | let d1 = r.grouped(by: { $0 % 3 }) 24 | XCTAssertEqual(3, d1.count) 25 | XCTAssertEqual(d1[0]!, [0, 3, 6, 9]) 26 | XCTAssertEqual(d1[1]!, [1, 4, 7]) 27 | XCTAssertEqual(d1[2]!, [2, 5, 8]) 28 | 29 | let d2 = r.grouped(by: { $0 }) 30 | XCTAssertEqual(10, d2.count) 31 | 32 | let d3 = (0..<0).grouped(by: { $0 }) 33 | XCTAssertEqual(0, d3.count) 34 | } 35 | 36 | func testThrowingFromKeyFunction() { 37 | let input = ["Apple", "Banana", "Cherry"] 38 | let error = SampleError() 39 | 40 | XCTAssertThrowsError( 41 | try input.grouped(by: { (_: String) -> Character in throw error }) 42 | ) { thrownError in 43 | XCTAssertIdentical(error, thrownError as? SampleError) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Guides/Suffix.md: -------------------------------------------------------------------------------- 1 | # Suffix 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Suffix.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/SuffixTests.swift)] 5 | 6 | This function returns a subsequence containing the elements from the end of the 7 | collection until predicate returns `false` and skipping the remaining elements. 8 | 9 | This example uses `suffix(while: )` to iterate through collection of integers 10 | from the end until the predicate returns false, in this case when `$0 <= 5` 11 | ```swift 12 | (0...10).suffix(while: { $0 > 5 }) // == [6,7,8,9,10] 13 | ``` 14 | 15 | ## Detailed Design 16 | 17 | The `suffix(while:)` function is added as a method on an extension of 18 | `BidirectionalCollection`. 19 | 20 | ```swift 21 | extension BidirectionalCollection { 22 | public func suffix(while predicate: (Element) throws -> Bool) rethrows -> SubSequence 23 | } 24 | ``` 25 | 26 | This method requires `BidirectionalCollection` for an efficient implementation 27 | which visits as few elements as possible. Swift's protocol allows for backward 28 | traversal of a collection as well as access to *last* property of a collection. 29 | 30 | ### Complexity 31 | 32 | Calling this method is O(*n*), where *n* is the length of the collection. 33 | 34 | ### Naming 35 | 36 | The function's name resembles that of an existing Swift function 37 | `prefix(while:)`, which performs same operation however in the forward direction 38 | of the collection. Hence, as this function traverses from the end of the 39 | collection, `suffix(while:)` is an appropriate name. 40 | -------------------------------------------------------------------------------- /Guides/EndsWith.md: -------------------------------------------------------------------------------- 1 | # EndsWith 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/EndsWith.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/EndsWithTests.swift)] 5 | 6 | This function checks whether the final elements of the one collection are the same as the elements in another collection. 7 | ``` 8 | 9 | ## Detailed Design 10 | 11 | The `ends(with:)` and `ends(with:by:)` functions are added as methods on an extension of 12 | `BidirectionalCollection`. 13 | 14 | ```swift 15 | extension BidirectionalCollection { 16 | public func ends( 17 | with possibleSuffix: PossibleSuffix 18 | ) -> Bool where PossibleSuffix.Element == Element 19 | 20 | public func ends( 21 | with possibleSuffix: PossibleSuffix, 22 | by areEquivalent: (Element, PossibleSuffix.Element) throws -> Bool 23 | ) rethrows -> Bool 24 | } 25 | ``` 26 | 27 | This method requires `BidirectionalCollection` for being able to traverse back from the end of the collection. It also requires the `possibleSuffix` to be `BidirectionalCollection`, because it too needs to be traverse backwards, to compare its elements against `self` from back to front. 28 | 29 | ### Complexity 30 | 31 | O(*m*), where *m* is the lesser of the length of the collection and the length of `possibleSuffix`. 32 | 33 | ### Naming 34 | 35 | The function's name resembles that of an existing Swift function 36 | `starts(with:)`, which performs same operation however in the forward direction 37 | of the collection. 38 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Algorithms.md: -------------------------------------------------------------------------------- 1 | # ``Algorithms`` 2 | 3 | **Swift Algorithms** is an open-source package of sequence and collection algorithms, 4 | along with their related types. 5 | 6 | ## Overview 7 | 8 | The Algorithms package provides a variety of sequence and collection operations, letting you cycle over a collection's elements, find combinations and permutations, create a random sample, and more. 9 | 10 | For example, the package includes a group of "chunking" methods, each of which breaks a collection into consecutive subsequences. One version tests adjacent elements to find the breaking point between chunks — you can use it to quickly separate an array into ascending runs: 11 | 12 | ```swift 13 | let numbers = [10, 20, 30, 10, 40, 40, 10, 20] 14 | let chunks = numbers.chunked(by: { $0 <= $1 }) 15 | // [[10, 20, 30], [10, 40, 40], [10, 20]] 16 | ``` 17 | 18 | Another version looks for a change in the transformation of each successive value. You can use that to separate a list of names into groups by the first character: 19 | 20 | ```swift 21 | let names = ["Cassie", "Chloe", "Jasmine", "Jordan", "Taylor"] 22 | let chunks = names.chunked(on: \.first) 23 | // [["Cassie", "Chloe"], ["Jasmine", "Jordan"], ["Taylor"]] 24 | ``` 25 | 26 | Explore more chunking methods and the remainder of the Algorithms package, grouped in the following topics. 27 | 28 | ## Topics 29 | 30 | - 31 | - 32 | - 33 | - 34 | - 35 | - 36 | - 37 | - 38 | - 39 | - 40 | - 41 | - 42 | - 43 | -------------------------------------------------------------------------------- /Sources/Algorithms/FirstNonNil.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | //===----------------------------------------------------------------------===// 13 | // firstNonNil(_:) 14 | //===----------------------------------------------------------------------===// 15 | 16 | extension Sequence { 17 | /// Returns the first non-`nil` result obtained from applying the given 18 | /// transformation to the elements of the sequence. 19 | /// 20 | /// let strings = ["three", "3.14", "-5", "2"] 21 | /// if let firstInt = strings.firstNonNil({ Int($0) }) { 22 | /// print(firstInt) 23 | /// // -5 24 | /// } 25 | /// 26 | /// - Parameter transform: A closure that takes an element of the sequence as 27 | /// its argument and returns an optional transformed value. 28 | /// - Returns: The first non-`nil` return value of the transformation, or 29 | /// `nil` if no transformation is successful. 30 | /// 31 | /// - Complexity: O(*n*), where *n* is the number of elements at the start of 32 | /// the sequence that result in `nil` when applying the transformation. 33 | @inlinable 34 | public func firstNonNil( 35 | _ transform: (Element) throws -> Result? 36 | ) rethrows -> Result? { 37 | for value in self { 38 | if let value = try transform(value) { 39 | return value 40 | } 41 | } 42 | return nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Guides/Compacted.md: -------------------------------------------------------------------------------- 1 | # Compacted 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Compacted.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/CompactedTests.swift)] 5 | 6 | A convenience method that lazily flattens the `nil`s out of a sequence 7 | or collection. 8 | 9 | The new method matches one of the most common uses of `compactMap`, 10 | which is to only remove `nil`s without transforming the elements 11 | (e.g. `collection.lazy.compactMap { $0 }`). 12 | 13 | ```swift 14 | let array: [Int?] = [10, nil, 30, nil, 2, 3, nil, 5] 15 | let withNoNils = array.compacted() 16 | // Array(withNoNils) == [10, 30, 2, 3, 5] 17 | ``` 18 | 19 | The type returned by `compacted()` lazily computes the non-`nil` elements 20 | without requiring a user to provide a closure or use the `.lazy` property. 21 | 22 | ## Detailed Design 23 | 24 | The `compacted()` methods has two overloads: 25 | 26 | ```swift 27 | extension Sequence { 28 | public func compacted() -> CompactedSequence 29 | where Element == Unwrapped? 30 | } 31 | 32 | extension Collection { 33 | public func compacted() -> CompactedCollection 34 | where Element == Unwrapped? 35 | } 36 | ``` 37 | 38 | The `Sequence` version of `compacted()` returns a `CompactedSequence` type, 39 | while the `Collection` version returns `CompactedCollection`. The collection 40 | has conditional conformance to `BidirectionalCollection` when the base 41 | collection conforms, and both have conditional conformance to 42 | `LazySequenceProtocol` when the base collection conforms. 43 | 44 | ### Naming 45 | 46 | The name `compacted()` matches the existing method `compactMap`, 47 | which is commonly used to extract only the non-`nil` values 48 | without mapping them to a different type. 49 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/UniqueTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class UniqueTests: XCTestCase { 16 | func testUnique() { 17 | let a = repeatElement(1...10, count: 15).joined().shuffled() 18 | let b = a.uniqued() 19 | XCTAssertEqual(b.sorted(), Set(a).sorted()) 20 | XCTAssertEqual(10, Array(b).count) 21 | 22 | let c: [Int] = [] 23 | expectEqualSequences(c.uniqued(), []) 24 | 25 | let d = Array(repeating: 1, count: 10) 26 | expectEqualSequences(d.uniqued(), [1]) 27 | } 28 | 29 | func testUniqueOn() { 30 | let a = [ 31 | "Albemarle", "Abeforth", "Astrology", "Brandywine", "Beatrice", "Axiom", 32 | ] 33 | let b = a.uniqued(on: { $0.first }) 34 | XCTAssertEqual(["Albemarle", "Brandywine"], b) 35 | 36 | let c: [Int] = [] 37 | XCTAssertEqual(c.uniqued(on: { $0.bitWidth }), []) 38 | 39 | let d = Array(repeating: "Andromeda", count: 10) 40 | expectEqualSequences(d.uniqued(on: { $0.first }), ["Andromeda"]) 41 | } 42 | 43 | func testLazyUniqueOn() { 44 | let a = [ 45 | "Albemarle", "Abeforth", "Astrology", "Brandywine", "Beatrice", "Axiom", 46 | ] 47 | let b = a.lazy.uniqued(on: { $0.first }) 48 | expectEqualSequences(b, ["Albemarle", "Brandywine"]) 49 | requireLazySequence(b) 50 | 51 | let c: [Int] = [] 52 | expectEqualSequences(c.lazy.uniqued(on: { $0.bitWidth }), []) 53 | 54 | let d = Array(repeating: "Andromeda", count: 10) 55 | expectEqualSequences(d.lazy.uniqued(on: { $0.first }), ["Andromeda"]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Guides/Intersperse.md: -------------------------------------------------------------------------------- 1 | # Intersperse 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Intersperse.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/IntersperseTests.swift)] 5 | 6 | Place a given value in between each element of the sequence. 7 | 8 | ```swift 9 | let numbers = [1, 2, 3].interspersed(with: 0) 10 | // Array(numbers) == [1, 0, 2, 0, 3] 11 | 12 | let letters = "ABCDE".interspersed(with: "-") 13 | // String(letters) == "A-B-C-D-E" 14 | 15 | let empty = [].interspersed(with: 0) 16 | // Array(empty) == [] 17 | ``` 18 | 19 | `interspersed(with:)` takes a separator value and inserts it in between every 20 | element in the sequence. 21 | 22 | ## Detailed Design 23 | 24 | A new method is added to sequence: 25 | 26 | ```swift 27 | extension Sequence { 28 | func interspersed(with separator: Element) -> InterspersedSequence 29 | } 30 | ``` 31 | 32 | The new `InterspersedSequence` type represents the sequence when the separator 33 | is inserted between each element. `InterspersedSequence` conforms to 34 | `Collection`, `BidirectionalCollection`, `RandomAccessCollection` and 35 | `LazySequenceProtocol` when the base sequence conforms to those respective 36 | protocols. 37 | 38 | ### Complexity 39 | 40 | Calling these methods is O(_1_). 41 | 42 | ### Naming 43 | 44 | This method’s and type’s name match the term of art used in other languages 45 | and libraries. 46 | 47 | ### Comparison with other languages 48 | 49 | **[Haskell][Haskell]:** Has an `intersperse` function which takes an element 50 | and a list and 'intersperses' that element between the elements of the list. 51 | 52 | **[Rust][Rust]:** Has a function called `intersperse` to insert a particular 53 | value between each element. 54 | 55 | 56 | 57 | [Haskell]: https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-List.html#v:intersperse 58 | [Rust]: https://docs.rs/itertools/0.9.0/itertools/trait.Itertools.html#method.intersperse 59 | -------------------------------------------------------------------------------- /Guides/AdjacentPairs.md: -------------------------------------------------------------------------------- 1 | # AdjacentPairs 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/AdjacentPairs.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/AdjacentPairsTests.swift)] 5 | 6 | Lazily iterates over tuples of adjacent elements. 7 | 8 | ```swift 9 | let numbers = (1...5) 10 | let pairs = numbers.adjacentPairs() 11 | // Array(pairs) == [(1, 2), (2, 3), (3, 4), (4, 5)] 12 | ``` 13 | 14 | ## Detailed Design 15 | 16 | The `adjacentPairs()` method is declared as a `Sequence` extension returning 17 | `AdjacentPairsSequence` and as a `Collection` extension returning 18 | `AdjacentPairsCollection`. 19 | 20 | ```swift 21 | extension Sequence { 22 | public func adjacentPairs() -> AdjacentPairsSequence 23 | } 24 | ``` 25 | 26 | ```swift 27 | extension Collection { 28 | public func adjacentPairs() -> AdjacentPairsCollection 29 | } 30 | ``` 31 | 32 | The `AdjacentPairsSequence` type is a sequence, and the 33 | `AdjacentPairsCollection` type is a collection with conditional conformance to 34 | `BidirectionalCollection` and `RandomAccessCollection` when the underlying 35 | collection conforms. 36 | 37 | ### Complexity 38 | 39 | Calling `adjacentPairs` is an O(1) operation. 40 | 41 | ### Naming 42 | 43 | This method is named for clarity while remaining agnostic to any particular 44 | domain of programming. In natural language processing, this operation is akin to 45 | computing a list of bigrams; however, this algorithm is not specific to this use 46 | case. 47 | 48 | ### Comparison with other languages 49 | 50 | This function is often written as a `zip` of a sequence together with itself, 51 | minus its first element. 52 | 53 | **Haskell:** This operation is spelled ``s `zip` tail s``. 54 | 55 | **Python:** Python users may write `zip(s, s[1:])` for a list with at least one 56 | element. For natural language processing, the `nltk` package offers a `bigrams` 57 | function akin to this method. 58 | 59 | Note that in Swift, the spelling `zip(s, s.dropFirst())` is undefined behavior 60 | for a single-pass sequence `s`. 61 | -------------------------------------------------------------------------------- /Guides/Chain.md: -------------------------------------------------------------------------------- 1 | # Chain 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Chain.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ChainTests.swift)] 5 | 6 | Concatenates two collections with the same element type, one after another. 7 | 8 | This operation is available for any two sequences by calling the `chain(_:_:)` 9 | function. 10 | 11 | ```swift 12 | let numbers = chain([10, 20, 30], 1...5) 13 | // Array(numbers) == [10, 20, 30, 1, 2, 3, 4, 5] 14 | 15 | let letters = chain("abcde", "FGHIJ") 16 | // String(letters) == "abcdeFGHIJ" 17 | ``` 18 | 19 | Unlike placing two collections in an array and calling `joined()`, chaining 20 | permits different collection types, performs no allocations, and can preserve 21 | the shared conformances of the two underlying types. 22 | 23 | ## Detailed Design 24 | 25 | The `chain(_:_:)` function takes two sequences as arguments: 26 | 27 | ```swift 28 | public func chain(_ s1: S1, _ s2: S2) -> Chain2Sequence 29 | where S1.Element == S2.Element 30 | ``` 31 | 32 | The resulting `Chain2Sequence` type is a sequence, with conditional conformance 33 | to `Collection`, `BidirectionalCollection`, and `RandomAccessCollection` when 34 | both the first and second arguments conform. 35 | 36 | ### Naming 37 | 38 | This function's and type's name match the term of art used in other languages 39 | and libraries. 40 | 41 | This operation was previously implemented as a `Sequence` method named 42 | `chained(with:)`, and was switched to a free function to align with APIs like 43 | `zip` and `product` after [a lengthy forum discussion][naming]. Alternative 44 | suggestions for method names include `appending(contentsOf:)`, `followed(by:)`, 45 | and `concatenated(to:)`. 46 | 47 | [naming]: https://forums.swift.org/t/naming-of-chained-with/40999/ 48 | 49 | ### Comparison with other languages 50 | 51 | **Rust:** Rust provides a `chain` function that concatenates two iterators. 52 | 53 | **Ruby/Python:** Ruby and Python’s `itertools` both define a `chain` function 54 | for concatenating collections of different kinds. 55 | -------------------------------------------------------------------------------- /Guides/Windows.md: -------------------------------------------------------------------------------- 1 | # Windows 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Windows.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/WindowsTests.swift)] 5 | 6 | Break a collection into overlapping subsequences where 7 | elements are slices from the original collection. 8 | 9 | The `windows(ofCount:)` method takes a size as a parameter and returns a 10 | collection of subsequences. Each element of the returned collection is a 11 | successive overlapping slice of the given size. 12 | 13 | ```swift 14 | let swift = "swift" 15 | 16 | let windowed = swift.windows(ofCount: 2) 17 | // Array(windowed) == [ "sw", "wi", "if", "ft" ] 18 | ``` 19 | 20 | ## Detailed Design 21 | 22 | The `windows(ofCount:)` method is added as an extension `Collection` method: 23 | 24 | ```swift 25 | extension Collection { 26 | public func windows(ofCount count: Int) -> WindowsOfCountCollection 27 | } 28 | ``` 29 | 30 | If a size larger than the collection's length is specified, the returned 31 | collection is empty. 32 | 33 | ```swift 34 | [1, 2, 3].windows(ofCount: 5).isEmpty // true 35 | ``` 36 | 37 | The resulting `WindowsOfCountCollection` type is a collection, with conditional 38 | conformance to the `BidirectionalCollection`, `RandomAccessCollection`, and 39 | `LazySequenceProtocol` protocols when the base collection conforms. 40 | 41 | ### Complexity 42 | 43 | The call to `windows(ofCount: k)` is O(1) if the collection conforms to 44 | `RandomAccessCollection`, otherwise O(_k_). Access to each successive window is 45 | O(1). 46 | 47 | ### Naming 48 | 49 | The method and type name take their names from the sliding windows algorithm. 50 | 51 | The `ofCount` parameter label was chosen to create a consistent feel with other 52 | APIs in the `Algorithms` package, specifically with `combinations(ofCount:)` 53 | and `permutations(ofCount:)`. 54 | 55 | ### Comparison with other languages 56 | 57 | [rust](https://doc.rust-lang.org/std/slice/struct.Windows.html) has 58 | `std::slice::Windows` which is a method available on slices. It has the same 59 | semantics as described here. 60 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/ProductTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import XCTest 13 | 14 | @testable import Algorithms 15 | 16 | final class ProductTests: XCTestCase { 17 | func testProduct() { 18 | expectEqualSequences( 19 | [(1, "A" as Character), (1, "B"), (2, "A"), (2, "B")], 20 | product(1...2, "AB"), 21 | by: ==) 22 | 23 | expectEqualSequences(product(1...10, ""), [], by: ==) 24 | expectEqualSequences(product("", 1...10), [], by: ==) 25 | } 26 | 27 | func testProductReversed() { 28 | expectEqualSequences( 29 | [(2, "B" as Character), (2, "A"), (1, "B"), (1, "A")], 30 | product(1...2, "AB").reversed(), 31 | by: ==) 32 | 33 | expectEqualSequences(product(1...10, "").reversed(), [], by: ==) 34 | expectEqualSequences(product("", 1...10).reversed(), [], by: ==) 35 | } 36 | 37 | func testProductDistanceFromTo() { 38 | let p = product([1, 2], "abc") 39 | XCTAssertEqual(p.distance(from: p.startIndex, to: p.endIndex), 6) 40 | } 41 | 42 | func testProductIndexTraversals() { 43 | let validator = IndexValidator>( 44 | indicesIncludingEnd: { product in 45 | product.base1.indices.flatMap { i1 in 46 | product.base2.indices.map { i2 in 47 | .init(i1: i1, i2: i2) 48 | } 49 | } + [.init(i1: product.base1.endIndex, i2: product.base2.startIndex)] 50 | }) 51 | 52 | validator.validate(product([1, 2, 3, 4], "abc"), expectedCount: 4 * 3) 53 | validator.validate(product([1, 2, 3, 4], ""), expectedCount: 4 * 0) 54 | validator.validate(product([], "abc"), expectedCount: 0 * 3) 55 | validator.validate(product([], ""), expectedCount: 0 * 0) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/SuffixTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | import Algorithms 13 | import XCTest 14 | 15 | final class SuffixTests: XCTestCase { 16 | func testSuffix() { 17 | let a = 0...10 18 | expectEqualSequences(a.suffix(while: { $0 > 5 }), (6...10)) 19 | expectEqualSequences(a.suffix(while: { $0 > 10 }), []) 20 | expectEqualSequences(a.suffix(while: { $0 > 9 }), [10]) 21 | expectEqualSequences(a.suffix(while: { $0 > -1 }), (0...10)) 22 | 23 | let empty: [Int] = [] 24 | expectEqualSequences(empty.suffix(while: { $0 > 10 }), []) 25 | } 26 | 27 | func testEndOfPrefix() { 28 | let array = Array(0..<10) 29 | XCTAssertEqual(array.endOfPrefix(while: { $0 < 3 }), 3) 30 | XCTAssertEqual(array.endOfPrefix(while: { _ in false }), array.startIndex) 31 | XCTAssertEqual(array.endOfPrefix(while: { _ in true }), array.endIndex) 32 | 33 | let empty: [Int] = [] 34 | XCTAssertEqual(empty.endOfPrefix(while: { $0 < 3 }), 0) 35 | XCTAssertEqual(empty.endOfPrefix(while: { _ in false }), empty.startIndex) 36 | XCTAssertEqual(empty.endOfPrefix(while: { _ in true }), empty.endIndex) 37 | } 38 | 39 | func testStartOfSuffix() { 40 | let array = Array(0..<10) 41 | XCTAssertEqual(array.startOfSuffix(while: { $0 >= 3 }), 3) 42 | XCTAssertEqual(array.startOfSuffix(while: { _ in false }), array.endIndex) 43 | XCTAssertEqual(array.startOfSuffix(while: { _ in true }), array.startIndex) 44 | 45 | let empty: [Int] = [] 46 | XCTAssertEqual(empty.startOfSuffix(while: { $0 < 3 }), 0) 47 | XCTAssertEqual(empty.startOfSuffix(while: { _ in false }), empty.endIndex) 48 | XCTAssertEqual(empty.startOfSuffix(while: { _ in true }), empty.startIndex) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Guides/Unique.md: -------------------------------------------------------------------------------- 1 | # Unique 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Unique.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/UniqueTests.swift)] 5 | 6 | Methods to strip repeated elements from a sequence or collection. 7 | 8 | The `uniqued()` method returns a sequence, dropping duplicate elements from a 9 | sequence. The `uniqued(on:)` method does the same, using the result of the given 10 | closure to determine the "uniqueness" of each element. 11 | 12 | ```swift 13 | let numbers = [1, 2, 3, 3, 2, 3, 3, 2, 2, 2, 1] 14 | 15 | let unique = numbers.uniqued() 16 | // Array(unique) == [1, 2, 3] 17 | ``` 18 | 19 | ## Detailed Design 20 | 21 | Both methods are available for sequences, with the simplest limited to when the 22 | element type conforms to `Hashable`. Both methods preserve the relative order of 23 | the elements. `uniqued(on:)` has a matching lazy version that is added to 24 | `LazySequenceProtocol`. 25 | 26 | ```swift 27 | extension Sequence where Element: Hashable { 28 | func uniqued() -> UniquedSequence 29 | } 30 | 31 | extension Sequence { 32 | func uniqued(on projection: (Element) throws -> Subject) rethrows -> [Element] 33 | where Subject: Hashable 34 | } 35 | 36 | extension LazySequenceProtocol { 37 | func uniqued(on projection: @escaping (Element) -> Subject) -> UniquedSequence 38 | where Subject: Hashable 39 | } 40 | ``` 41 | 42 | ### Complexity 43 | 44 | The eager `uniqued(on:)` method is O(_n_) in both time and space complexity. The 45 | lazy versions are O(_1_). 46 | 47 | ### Comparison with other languages 48 | 49 | **C+\+:** The `` library defines a `unique` function that removes 50 | consecutive equal elements. 51 | 52 | **Ruby:** Ruby defines a `uniq()` method that acts like `uniqued()`, as well as 53 | a variant that takes a mapping block to a property that should be unique, 54 | matching `uniqued(on:)`. 55 | 56 | **Rust:** Rust includes `unique()` and `unique_by()` methods that lazily 57 | compute the unique elements of a collection, storing the set of already seen 58 | elements in the iterator. 59 | 60 | -------------------------------------------------------------------------------- /Sources/Algorithms/Documentation.docc/Partitioning.md: -------------------------------------------------------------------------------- 1 | # Partitioning and Rotating 2 | 3 | Partition a collection according to a unary predicate, 4 | rotate a collection around a particular index, 5 | or find the index where a collection is already partitioned. 6 | 7 | ## Overview 8 | 9 | A _stable partition_ maintains the relative order of elements within both partitions. 10 | 11 | ```swift 12 | // partition(by:) - unstable ordering 13 | var numbers = [10, 20, 30, 40, 50, 60, 70, 80] 14 | let p1 = numbers.partition(by: { $0.isMultiple(of: 20) }) 15 | // p1 == 4 16 | // numbers == [10, 70, 30, 50, 40, 60, 20, 80] 17 | // ^ start of second partition 18 | 19 | // stablePartition(by:) - maintains relative ordering 20 | numbers = [10, 20, 30, 40, 50, 60, 70, 80] 21 | let p2 = numbers.stablePartition(by: { $0.isMultiple(of: 20) }) 22 | // p2 == 4 23 | // numbers == [10, 30, 50, 70, 20, 40, 60, 80] 24 | // ^ start of second partition 25 | ``` 26 | 27 | Use the `rotate` method to shift the elements of a collection to start at a new position, moving the displaced elements to the end: 28 | 29 | ```swift 30 | var numbers = [10, 20, 30, 40, 50, 60, 70, 80] 31 | let p = numbers.rotate(toStartAt: 2) 32 | // numbers == [30, 40, 50, 60, 70, 80, 10, 20] 33 | // p == 6 -- numbers[p] == 10 34 | ``` 35 | 36 | ## Topics 37 | 38 | ### Stable Partition 39 | 40 | - ``Swift/MutableCollection/stablePartition(by:)`` 41 | - ``Swift/MutableCollection/stablePartition(subrange:by:)`` 42 | - ``Swift/Sequence/partitioned(by:)`` 43 | - ``Swift/Collection/partitioned(by:)`` 44 | 45 | ### Partition of Subranges 46 | 47 | - ``Swift/MutableCollection/partition(subrange:by:)-5vdh7`` 48 | - ``Swift/MutableCollection/partition(subrange:by:)-4gpqz`` 49 | 50 | ### Finding a Partition Index 51 | 52 | - ``Swift/Collection/partitioningIndex(where:)`` 53 | 54 | ### Rotation 55 | 56 | - ``Swift/MutableCollection/rotate(toStartAt:)-9fp48`` 57 | - ``Swift/MutableCollection/rotate(toStartAt:)-2r55j`` 58 | - ``Swift/MutableCollection/rotate(subrange:toStartAt:)-ov6a`` 59 | - ``Swift/MutableCollection/rotate(subrange:toStartAt:)-5teoq`` 60 | 61 | ### Reversing 62 | 63 | - ``Swift/MutableCollection/reverse(subrange:)`` 64 | -------------------------------------------------------------------------------- /Guides/Cycle.md: -------------------------------------------------------------------------------- 1 | # Cycle 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Cycle.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/CycleTests.swift)] 5 | 6 | Iterate over a collection forever, or a set number of times. 7 | 8 | ```swift 9 | for (odd, n) in zip([true, false].cycled(), 1...) { 10 | // ... 11 | } 12 | 13 | for x in (1...3).cycled(times: 3) { 14 | print(x) 15 | } 16 | // Prints 1 through 3 three times 17 | ``` 18 | 19 | `cycled(times:)` provides a more expressive way of repeating a 20 | collection's elements a limited number of times than 21 | combining `repeatElement(_:count:)` and `joined()`. 22 | 23 | ## Detailed Design 24 | 25 | Two new methods are added to collections: 26 | 27 | ```swift 28 | extension Collection { 29 | func cycled() -> CycledSequence 30 | 31 | func cycled(times: Int) -> CycledTimesCollection 32 | } 33 | ``` 34 | 35 | The new `CycledSequence` type is a sequence only, given that the `Collection` 36 | protocol design makes infinitely large types impossible/impractical. 37 | `CycledSequence` also conforms to `LazySequenceProtocol` when the base type 38 | conforms. 39 | 40 | The `CycledTimesCollection` type always has `Collection` conformance, with 41 | `BidirectionalCollection`, `RandomAccessCollection`, and `LazySequenceProtocol` 42 | conformance when the base type conforms. 43 | 44 | ### Complexity 45 | 46 | Calling these methods is O(_1_). 47 | 48 | ### Naming 49 | 50 | There's a slight off-by-one ambiguity around the naming of `cycled(times:)`, 51 | since one can reasonably interpret the number of cycles as including the first 52 | run-through of elements (the actual semantics) or starting after the first 53 | set. This ambiguity is present in all the different potential names for this 54 | function: `repeated(times:)`, `cycled(repetitions:)`, etc. 55 | 56 | ### Comparison with other languages 57 | 58 | **Rust:** The `cycle` method repeats an iterator's elements forever. I don’t see 59 | anything that matches the `cycled(times:)` behavior. 60 | 61 | **Ruby:** Passing a number to `cycle` limits the number of repetitions, like the 62 | `cycled(times:)` here. 63 | 64 | **Python:** Python’s `itertools` includes a `cycle` method, which caches the 65 | elements of the iterator on the first pass. 66 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/ChainTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import XCTest 13 | 14 | @testable import Algorithms 15 | 16 | final class ChainTests: XCTestCase { 17 | func testChainSequences() { 18 | let run = chain((1...).prefix(10), 20...) 19 | expectEqualSequences(run.prefix(20), Array(1...10) + (20..<30)) 20 | } 21 | 22 | func testChainForwardCollection() { 23 | let s1 = Set(0...10) 24 | let s2 = Set(20...30) 25 | let c = chain(s1, s2) 26 | expectEqualSequences(c, Array(s1) + Array(s2)) 27 | } 28 | 29 | func testChainBidirectionalCollection() { 30 | let s1 = "ABCDEFGHIJ" 31 | let s2 = "klmnopqrstuv" 32 | let c = chain(s1, s2) 33 | 34 | expectEqualSequences(c, "ABCDEFGHIJklmnopqrstuv") 35 | expectEqualSequences(c.reversed(), "ABCDEFGHIJklmnopqrstuv".reversed()) 36 | expectEqualSequences(chain(s1.reversed(), s2), "JIHGFEDCBAklmnopqrstuv") 37 | } 38 | 39 | func testChainIndexTraversals() { 40 | let validator = IndexValidator>( 41 | indicesIncludingEnd: { chain in 42 | chain.base1.indices.map { .init(first: $0) } 43 | + chain.base2.indices.map { .init(second: $0) } 44 | + [.init(second: chain.base2.endIndex)] 45 | }) 46 | 47 | validator.validate(chain("abcd", "XYZ"), expectedCount: 4 + 3) 48 | validator.validate(chain("abcd", ""), expectedCount: 4 + 0) 49 | validator.validate(chain("", "XYZ"), expectedCount: 0 + 3) 50 | validator.validate(chain("", ""), expectedCount: 0 + 0) 51 | } 52 | 53 | func testChainIndexOffsetAcrossBoundary() { 54 | let c = chain("abc", "XYZ") 55 | 56 | do { 57 | let i = c.index(c.startIndex, offsetBy: 3, limitedBy: c.startIndex) 58 | XCTAssertNil(i) 59 | } 60 | 61 | do { 62 | let i = c.index(c.startIndex, offsetBy: 4) 63 | let j = c.index(i, offsetBy: -2) 64 | XCTAssertEqual(c[j], "c") 65 | } 66 | 67 | do { 68 | let i = c.index(c.startIndex, offsetBy: 3) 69 | let j = c.index(i, offsetBy: -1, limitedBy: i) 70 | XCTAssertNil(j) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Guides/Rotate.md: -------------------------------------------------------------------------------- 1 | # Rotate 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Rotate.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/RotateTests.swift)] 5 | 6 | A mutating method that rotates the elements of a collection to new positions. 7 | 8 | ```swift 9 | var numbers = [10, 20, 30, 40, 50, 60] 10 | let p = numbers.rotate(toStartAt: 2) 11 | // numbers == [30, 40, 50, 60, 10, 20] 12 | // p == 4 -- numbers[p] == 10 13 | ``` 14 | 15 | To work around the CoW / slice mutation problem for divide-and-conquer 16 | algorithms, which are the idiomatic use case for rotation, this also includes 17 | variants that take a range: 18 | 19 | ```swift 20 | var numbers = [10, 20, 30, 40, 50, 60] 21 | numbers.rotate(subrange: 0..<3, toStartAt: 1) 22 | // numbers = [20, 30, 10, 40, 50, 60] 23 | numbers.rotate(subrange: 3..<6, toStartAt: 4) 24 | // numbers = [20, 30, 10, 50, 60, 40] 25 | ``` 26 | 27 | ## Detailed Design 28 | 29 | This adds the two `MutableCollection` methods shown above: 30 | 31 | ```swift 32 | extension MutableCollection { 33 | mutating func rotate(toStartAt p: Index) -> Index 34 | 35 | mutating func rotate( 36 | subrange: Range, 37 | toStartAt p: Index 38 | ) -> Index 39 | } 40 | ``` 41 | 42 | ### Complexity 43 | 44 | Rotation is a O(_n_) operation, where _n_ is the length of the range being 45 | rotated. The `BidirectionalCollection` version of rotation significantly lowers 46 | the number of swaps required per element, so `rotate` would need to be a 47 | `MutableCollection` customization point were it adopted by the standard library. 48 | 49 | ### Naming 50 | 51 | The index parameter has been proposed as `shiftingToStart` in the past; this 52 | version uses the `toStartAt` label, instead. `shiftingToStart` introduces the 53 | idea of a "shift", which can sound like shifting just that single element to the 54 | beginning of the collection. 55 | 56 | For the range-based overloads, the label could be omitted. That is, instead of 57 | using `subrange:`, the method could be called as 58 | `numbers.rotate(0..<3, toStartAt: 2)`. The label is included here for 59 | consistency with other range-based mutating methods. 60 | 61 | ### Comparison with other languages 62 | 63 | **C++:** The `` library defines a `rotate` function with similar 64 | semantics to this one. 65 | 66 | **Ruby:** You can rotate the elements of an array by a number of positions, 67 | either forward or backward (by passing a negative number). For zero-indexed 68 | collections, forward rotation by e.g. 3 elements is equivalent to 69 | `rotate(toStartAt: 3)`. 70 | 71 | -------------------------------------------------------------------------------- /Guides/Stride.md: -------------------------------------------------------------------------------- 1 | # Stride 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Stride.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/StrideTests.swift)] 5 | 6 | Step over the elements of a sequence elements by a specified amount. 7 | 8 | This is available through the `striding(by:)` method on any `Sequence`. 9 | 10 | ```swift 11 | (0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10] 12 | ``` 13 | 14 | If the stride is larger than the length of the sequence, the resulting wrapper 15 | only contains the first element. 16 | 17 | The stride amount must be a positive value. 18 | 19 | ## Detailed Design 20 | 21 | The `striding(by:)` method is declared in extension of both `Sequence` and 22 | `Collection`: 23 | 24 | ```swift 25 | extension Sequence { 26 | public func striding(by step: Int) -> StridingSequence 27 | } 28 | 29 | extension Collection { 30 | public func striding(by step: Int) -> StridingCollection 31 | } 32 | ``` 33 | 34 | The reason for this distinction is subtle. The `StridingSequence.Iterator` type 35 | is unable to skip over multiple elements of the wrapped iterator at once since 36 | that's not part of `IteratorProtocol`'s interface. In order to efficiently 37 | stride over collections that provide a fast way of skipping multiple elements at 38 | once, the `StridingCollection` type was added which does not provide a custom 39 | `Iterator` type and therefore always strides over the underlying collection in 40 | the fastest way possible. See the related 41 | [GitHub issue](https://github.com/apple/swift-algorithms/issues/63) for more 42 | information. 43 | 44 | A careful thought was given to the composition of these strides by giving a 45 | custom implementation to `index(_:offsetBy:limitedBy)` which multiplies the 46 | offset by the stride amount. 47 | 48 | ```swift 49 | base.index(i.base, offsetBy: distance * stride, limitedBy: base.endIndex) 50 | ``` 51 | 52 | The following two lines of code are equivalent, including performance: 53 | 54 | ```swift 55 | (0...10).striding(by: 6) 56 | (0...10).striding(by: 2).stride(by: 3) 57 | ``` 58 | 59 | ### Complexity 60 | 61 | The call to `striding(by: k)` is always O(_1_) and access to the next value in 62 | the stride is O(_1_) if the collection conforms to `RandomAccessCollection`, 63 | otherwise O(_k_). 64 | 65 | ### Comparison with other languages 66 | 67 | [rust has `Strided`](https://docs.rs/strided/0.2.9/strided/) available in a crate. 68 | [c++ has std::slice::stride](http://www.cplusplus.com/reference/valarray/slice/stride/) 69 | 70 | The semantics of `striding` described in this documentation are equivalent. 71 | -------------------------------------------------------------------------------- /Guides/FirstNonNil.md: -------------------------------------------------------------------------------- 1 | # First Non-Nil 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/FirstNonNil.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/FirstNonNilTests.swift)] 5 | 6 | Returns the first non-`nil` result obtained from applying the given 7 | transformation to the elements of the sequence. 8 | 9 | ```swift 10 | let strings = ["three", "3.14", "-5", "2"] 11 | if let firstInt = strings.firstNonNil({ Int($0) }) { 12 | print(firstInt) 13 | // -5 14 | } 15 | ``` 16 | 17 | Like `first(where:)`, this method stops iterating the sequence once a match is 18 | found. Use `firstNonNil(_:)` to avoid having to apply a transformation twice: 19 | 20 | ```swift 21 | let strings = ["three", "3.14", "-5", "2"] 22 | if let firstIntAsString = strings.first(where: { Int($0) != nil }) { 23 | let firstInt = Int(firstIntAsString)! // :( 24 | // ... 25 | } 26 | ``` 27 | 28 | This method's behavior can also be approximated using `compactMap(_:)`: 29 | 30 | ```swift 31 | let strings = ["three", "3.14", "-5", "2"] 32 | if let firstInt = strings.compactMap({ Int($0) }).first { 33 | // ... 34 | } 35 | ``` 36 | 37 | However, unlike `firstNonNil(_:)` and `first(where:)`, `compactMap(_:)` does not 38 | stop iterating the sequence once the first match is found. Adding `.lazy` fixes 39 | this, at the cost of requiring an escaping closure that you cannot `throw` from: 40 | 41 | ```swift 42 | let strings = ["three", "3.14", "-5", "2"] 43 | if let firstInt = strings.lazy.compactMap({ Int($0) }).first { 44 | // ... 45 | } 46 | ``` 47 | 48 | ## Detailed Design 49 | 50 | The `firstNonNil(_:)` method is added as an extension method on the `Sequence` 51 | protocol: 52 | 53 | ```swift 54 | public extension Sequence { 55 | func firstNonNil( 56 | _ transform: (Element) throws -> Result? 57 | ) rethrows -> Result? 58 | } 59 | ``` 60 | 61 | ### Complexity 62 | 63 | `firstNonNil(_:)` is an O(*n*) operation, where *n* is the number of 64 | elements at the start of the sequence that result in `nil` when applying the 65 | transformation. 66 | 67 | ### Naming 68 | 69 | Some alternative names were considered: 70 | 71 | * `compactMapFirst(_:)` 72 | * `first(mapping:)` 73 | * `firstSome(_:)` 74 | * `firstMap(_:)` 75 | 76 | ### Comparison with other languages 77 | 78 | **Haskell**: Haskell includes the `firstJust` method, which has the same 79 | semantics as the `firstNonNil(_:)` method. 80 | 81 | **Rust**: Rust includes the `find_map` method, which has the same semantics as 82 | the `firstNonNil(_:)` method. 83 | 84 | **Scala**: Scala includes the `collectFirst` method, which has the same 85 | semantics as the `firstNonNil(_:)` method. 86 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 2 7 | }, 8 | "indentConditionalCompilationBlocks" : false, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : false, 13 | "lineBreakBeforeEachGenericRequirement" : false, 14 | "lineLength" : 80, 15 | "maximumBlankLines" : 1, 16 | "multiElementCollectionTrailingCommas" : true, 17 | "noAssignmentInExpressions" : { 18 | "allowedFunctions" : [ 19 | "XCTAssertNoThrow" 20 | ] 21 | }, 22 | "prioritizeKeepingFunctionOutputTogether" : false, 23 | "respectsExistingLineBreaks" : true, 24 | "rules" : { 25 | "AllPublicDeclarationsHaveDocumentation" : false, 26 | "AlwaysUseLiteralForEmptyCollectionInit" : true, 27 | "AlwaysUseLowerCamelCase" : true, 28 | "AmbiguousTrailingClosureOverload" : false, 29 | "BeginDocumentationCommentWithOneLineSummary" : true, 30 | "DoNotUseSemicolons" : true, 31 | "DontRepeatTypeInStaticProperties" : true, 32 | "FileScopedDeclarationPrivacy" : true, 33 | "FullyIndirectEnum" : true, 34 | "GroupNumericLiterals" : true, 35 | "IdentifiersMustBeASCII" : true, 36 | "NeverForceUnwrap" : true, 37 | "NeverUseForceTry" : true, 38 | "NeverUseImplicitlyUnwrappedOptionals" : true, 39 | "NoAccessLevelOnExtensionDeclaration" : true, 40 | "NoAssignmentInExpressions" : true, 41 | "NoBlockComments" : true, 42 | "NoCasesWithOnlyFallthrough" : true, 43 | "NoEmptyTrailingClosureParentheses" : true, 44 | "NoLabelsInCasePatterns" : true, 45 | "NoLeadingUnderscores" : false, 46 | "NoParensAroundConditions" : true, 47 | "NoPlaygroundLiterals" : true, 48 | "NoVoidReturnOnFunctionSignature" : true, 49 | "OmitExplicitReturns" : true, 50 | "OneCasePerLine" : true, 51 | "OneVariableDeclarationPerLine" : true, 52 | "OnlyOneTrailingClosureArgument" : true, 53 | "OrderedImports" : true, 54 | "ReplaceForEachWithForLoop" : true, 55 | "ReturnVoidInsteadOfEmptyTuple" : true, 56 | "TypeNamesShouldBeCapitalized" : true, 57 | "UseEarlyExits" : false, 58 | "UseExplicitNilCheckInConditions" : true, 59 | "UseLetInEveryBoundCaseVariable" : true, 60 | "UseShorthandTypeNames" : true, 61 | "UseSingleLinePropertyGetter" : true, 62 | "UseSynthesizedInitializer" : true, 63 | "UseTripleSlashForDocumentationComments" : true, 64 | "UseWhereClausesInForLoops" : false, 65 | "ValidateDocumentationComments" : true 66 | }, 67 | "spacesBeforeEndOfLineComments": 2, 68 | "spacesAroundRangeFormationOperators" : false, 69 | "tabWidth" : 2, 70 | "version" : 1 71 | } 72 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/EndsWithTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | import Algorithms 13 | import XCTest 14 | 15 | final class EndsWithTests: XCTestCase { 16 | func testEndsWithCorrectSuffix() { 17 | let a = 8...10 18 | let b = 1...10 19 | 20 | XCTAssertTrue(b.ends(with: a)) 21 | } 22 | 23 | func testDoesntEndWithWrongSuffix() { 24 | let a = 8...9 25 | let b = 1...10 26 | 27 | XCTAssertFalse(b.ends(with: a)) 28 | } 29 | 30 | func testDoesntEndWithTooLongSuffix() { 31 | XCTAssertFalse((2...5).ends(with: (1...10))) 32 | } 33 | 34 | func testEndsWithEmpty() { 35 | let a = 8...10 36 | let empty: [Int] = [] 37 | XCTAssertTrue(a.ends(with: empty)) 38 | } 39 | 40 | func testEmptyEndsWithEmpty() { 41 | let empty: [Int] = [] 42 | XCTAssertTrue(empty.ends(with: empty)) 43 | } 44 | 45 | func testEmptyDoesNotEndWithNonempty() { 46 | XCTAssertFalse([].ends(with: 1...10)) 47 | } 48 | } 49 | 50 | final class EndsWithNonEquatableTests: XCTestCase { 51 | func testEndsWithCorrectSuffix() { 52 | let a = nonEq(8...10) 53 | let b = nonEq(1...10) 54 | 55 | XCTAssertTrue(b.ends(with: a, by: areEquivalent)) 56 | } 57 | 58 | func testDoesntEndWithWrongSuffix() { 59 | let a = nonEq(8...9) 60 | let b = nonEq(1...10) 61 | 62 | XCTAssertFalse(b.ends(with: a, by: areEquivalent)) 63 | } 64 | 65 | func testDoesntEndWithTooLongSuffix() { 66 | XCTAssertFalse(nonEq(2...5).ends(with: nonEq(1...10), by: areEquivalent)) 67 | } 68 | 69 | func testEndsWithEmpty() { 70 | let a = nonEq(8...10) 71 | let empty: [NotEquatable] = [] 72 | XCTAssertTrue(a.ends(with: empty, by: areEquivalent)) 73 | } 74 | 75 | func testEmptyEndsWithEmpty() { 76 | let empty: [NotEquatable] = [] 77 | XCTAssertTrue(empty.ends(with: empty, by: areEquivalent)) 78 | } 79 | 80 | func testEmptyDoesNotEndWithNonempty() { 81 | XCTAssertFalse([].ends(with: nonEq(1...10), by: areEquivalent)) 82 | } 83 | 84 | private func nonEq(_ range: ClosedRange) -> [NotEquatable] { 85 | range.map(NotEquatable.init) 86 | } 87 | 88 | private func areEquivalent( 89 | lhs: NotEquatable, rhs: NotEquatable 90 | ) -> Bool { 91 | lhs.value == rhs.value 92 | } 93 | 94 | private struct NotEquatable { 95 | let value: T 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Guides/Product.md: -------------------------------------------------------------------------------- 1 | # Product 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Product.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ProductTests.swift)] 5 | 6 | A function for iterating over every pair of elements in two different 7 | collections. 8 | 9 | ```swift 10 | let seasons = ["winter", "spring", "summer", "fall"] 11 | for (year, season) in product(1900...2020, seasons) { 12 | // ... 13 | } 14 | 15 | // Is equivalent to: 16 | for years in 1900...2020 { 17 | for season in seasons { 18 | // ... 19 | } 20 | } 21 | ``` 22 | 23 | When either collection is empty, the resulting wrapper collection is also empty. 24 | 25 | ## Detailed Design 26 | 27 | The `product` function takes (at minimum) a sequence and a collection and 28 | returns a `Product2Sequence` type: 29 | 30 | ```swift 31 | public func product( 32 | _ s1: Base1, _ s2: Base2 33 | ) -> Product2Sequence 34 | ``` 35 | 36 | We require `Collection` conformance for `Base2`, since it needs to be iterated 37 | over multiple times. `Base1`, by contrast, is only iterated over a single time, 38 | so it can be a sequence. 39 | 40 | The `Product2Sequence` type wraps the base sequence and collection, and acts as 41 | a sequence in the base case, upgrading to a collection, a bidirectional 42 | collection, and a random-access collection when both base collections have those 43 | conformances. 44 | 45 | We don't provide higher arities (like `Product3Sequence`, `Product4Sequence`, 46 | etc.) at this time to match the standard library's `Zip2Sequence` type. Users 47 | can compose multiple calls to `product` if they would like higher arities. 48 | 49 | ### Complexity 50 | 51 | Since the `product` function returns a wrapper type, that call is O(1); 52 | collection operations on the resulting wrapper are O(_m \* n_). 53 | 54 | ### Naming 55 | 56 | This function and the resulting collection provide the cartesian product of two 57 | collections. While `product` is precedented in other languages, the name has an 58 | unfortunate overlap with multiplication, where `product(numbers1, numbers2)` 59 | could be seen as producing an element-wise product. 60 | 61 | ### Comparison with other languages 62 | 63 | **Ruby:** You can call the `product` method on an array, passing one or more 64 | arrays to form n-ary tuples — passing a single array gives the same semantics as 65 | this function. 66 | 67 | **Python:** Passing two or more collections to `product` returns the product of 68 | those collections. Passing one collection and a `repeat=n` parameter is 69 | equivalent to passing that collection `n` times. For example, `product("ABC", 70 | repeat=2)` yields `"AA", "AB", "AC", "BA", "BB", "BC", "CA", "CB", "CC"`. 71 | 72 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/CycleTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class CycleTests: XCTestCase { 16 | func testCycle() { 17 | let cycle = (1...4).cycled() 18 | expectEqualSequences( 19 | [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], 20 | cycle.prefix(20) 21 | ) 22 | } 23 | 24 | func testCycleClosedRangePrefix() { 25 | let a = Array((0..<17).cycled().prefix(10_000)) 26 | XCTAssertEqual(10_000, a.count) 27 | } 28 | 29 | func testEmptyCycle() { 30 | let empty = Array("".cycled()) 31 | XCTAssert(empty.isEmpty) 32 | } 33 | 34 | func testCycleLazy() { 35 | requireLazySequence((1...4).lazy.cycled()) 36 | } 37 | 38 | func testRepeated() { 39 | let repeats = (1...4).cycled(times: 3) 40 | expectEqualSequences( 41 | [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], 42 | repeats) 43 | } 44 | 45 | func testRepeatedClosedRange() { 46 | let repeats = Array((1..<5).cycled(times: 2500)) 47 | XCTAssertEqual(10_000, repeats.count) 48 | } 49 | 50 | func testRepeatedEmptyCollection() { 51 | let empty1 = Array("".cycled(times: 100)) 52 | XCTAssert(empty1.isEmpty) 53 | } 54 | 55 | func testRepeatedZeroTimesCycle() { 56 | let empty2 = Array("Hello".cycled(times: 0)) 57 | XCTAssert(empty2.isEmpty) 58 | } 59 | 60 | func testRepeatedLazy() { 61 | requireLazySequence((1...4).lazy.cycled(times: 3)) 62 | } 63 | 64 | func testRepeatedIndexMethods() { 65 | let cycle = (1..<5).cycled(times: 2) 66 | let startIndex = cycle.startIndex 67 | var nextIndex = cycle.index(after: startIndex) 68 | XCTAssertEqual(cycle.distance(from: startIndex, to: nextIndex), 1) 69 | 70 | nextIndex = cycle.index(nextIndex, offsetBy: 5) 71 | XCTAssertEqual(cycle.distance(from: startIndex, to: nextIndex), 6) 72 | 73 | nextIndex = cycle.index(nextIndex, offsetBy: 2, limitedBy: cycle.endIndex)! 74 | XCTAssertEqual(cycle.distance(from: startIndex, to: nextIndex), 8) 75 | 76 | let outOfBounds = cycle.index( 77 | nextIndex, offsetBy: 1, 78 | limitedBy: cycle.endIndex) 79 | XCTAssertNil(outOfBounds) 80 | 81 | let previousIndex = cycle.index(before: nextIndex) 82 | XCTAssertEqual(cycle.distance(from: startIndex, to: previousIndex), 7) 83 | } 84 | 85 | func testRepeatedCount() { 86 | let cycle = (1..<5).cycled(times: 2) 87 | XCTAssertEqual(cycle.count, 8) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/AdjacentPairsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | import Algorithms 13 | import XCTest 14 | 15 | final class AdjacentPairsTests: XCTestCase { 16 | func testEmptySequence() { 17 | let pairs = (0...).prefix(0).adjacentPairs() 18 | expectEqualSequences(pairs, [], by: ==) 19 | } 20 | 21 | func testOneElementSequence() { 22 | let pairs = (0...).prefix(1).adjacentPairs() 23 | expectEqualSequences(pairs, [], by: ==) 24 | } 25 | 26 | func testTwoElementSequence() { 27 | let pairs = (0...).prefix(2).adjacentPairs() 28 | expectEqualSequences(pairs, [(0, 1)], by: ==) 29 | } 30 | 31 | func testThreeElementSequence() { 32 | let pairs = (0...).prefix(3).adjacentPairs() 33 | expectEqualSequences(pairs, [(0, 1), (1, 2)], by: ==) 34 | } 35 | 36 | func testManySequences() { 37 | for n in 4...100 { 38 | let pairs = (0...).prefix(n).adjacentPairs() 39 | expectEqualSequences(pairs, zip(0..., 1...).prefix(n - 1), by: ==) 40 | } 41 | } 42 | 43 | func testZeroElements() { 44 | let pairs = (0..<0).adjacentPairs() 45 | XCTAssertEqual(pairs.startIndex, pairs.endIndex) 46 | expectEqualSequences(pairs, [], by: ==) 47 | } 48 | 49 | func testOneElement() { 50 | let pairs = (0..<1).adjacentPairs() 51 | XCTAssertEqual(pairs.startIndex, pairs.endIndex) 52 | expectEqualSequences(pairs, [], by: ==) 53 | } 54 | 55 | func testTwoElements() { 56 | let pairs = (0..<2).adjacentPairs() 57 | expectEqualSequences(pairs, [(0, 1)], by: ==) 58 | } 59 | 60 | func testThreeElements() { 61 | let pairs = (0..<3).adjacentPairs() 62 | expectEqualSequences(pairs, [(0, 1), (1, 2)], by: ==) 63 | } 64 | 65 | func testManyElements() { 66 | for n in 4...100 { 67 | let pairs = (0..>>() 74 | validator.validate((0..<0).adjacentPairs(), expectedCount: 0) 75 | validator.validate((0..<1).adjacentPairs(), expectedCount: 0) 76 | validator.validate((0..<2).adjacentPairs(), expectedCount: 1) 77 | validator.validate((0..<5).adjacentPairs(), expectedCount: 4) 78 | } 79 | 80 | func testLaziness() { 81 | requireLazySequence((0...).lazy.adjacentPairs()) 82 | requireLazyCollection((0..<100).lazy.adjacentPairs()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Guides/Split.md: -------------------------------------------------------------------------------- 1 | # Split 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Split.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/SplitTests.swift)] 5 | 6 | Lazily-evaluating versions of 7 | [`split(maxSplits:omittingEmptySubsequences:whereSeparator:)`](https://developer.apple.com/documentation/swift/sequence/3128814-split) 8 | and [`split(separator:maxSplits:omittingEmptySubsequences:)`](https://developer.apple.com/documentation/swift/sequence/3128818-split), 9 | performing the same operation as their counterparts defined on 10 | `Sequence` and `Collection`, are added to `LazySequence` and 11 | `LazyCollection`. The `LazyCollection` methods allow splitting a 12 | collection without allocating additional storage on the heap. 13 | 14 | ```swift 15 | // Splitting a lazy sequence. 16 | let numbers = stride(from: 1, through: 16, by: 1) 17 | for subsequence in numbers.lazy.split( 18 | whereSeparator: { $0 % 3 == 0 || $0 % 5 == 0 } 19 | ) { 20 | print(subsequence) 21 | } 22 | /* Prints: 23 | [1, 2] 24 | [4] 25 | [7, 8] 26 | [11] 27 | [13, 14] 28 | [16] 29 | */ 30 | 31 | // Splitting a lazy collection. 32 | let line = "BLANCHE: I don't want realism. I want magic!" 33 | for subsequence in line.lazy.split(separator: " ") { 34 | print(subsequence) 35 | } 36 | /* Prints 37 | BLANCHE: 38 | I 39 | don't 40 | want 41 | realism. 42 | I 43 | want 44 | magic! 45 | */ 46 | ``` 47 | 48 | ## Detailed Design 49 | 50 | `LazySequence` and `LazyCollection` are each extended with 51 | `split(maxSplits:omittingEmptySubsequences:whereSeparator:)` and 52 | `split(separator:maxSplits:omittingEmptySubsequences:)`. 53 | 54 | The `LazySequence` versions of those methods return an instance of 55 | `SplitSequence`. The `LazyCollection` versions return an instance of 56 | `SplitCollection`. 57 | 58 | `SplitSequence` wraps the sequence to be split, and provides an iterator whose 59 | `next` method returns a newly-allocated array containing the elements of each 60 | subsequence in the split sequence in turn. 61 | 62 | `SplitCollection` wraps the collection to be split. Its `Index` wraps a range of 63 | base collection indices. `startIndex` is computed at initialization. 64 | Subscripting a `SplitCollection` instance returns the slice of the original 65 | collection which is the subsequence of the split collection at the given index's 66 | position. 67 | 68 | ### Complexity 69 | 70 | Iterating a `SplitSequence` instance is O(_n_) in time and space, since each 71 | subsequence returned is a newly-allocated array. 72 | 73 | Iterating a `SplitCollection` instance is O(_n_) in time and O(1) in space, 74 | since each subsequence returned is a slice of the base collection. Since 75 | `startIndex` is computed at initialization, some or all of the time cost may be 76 | paid at initialization. For example, if the base collection contains no elements 77 | determined to be separators, it will be iterated entirely on initialization of 78 | the split collection, and all subsequent operations on the split collection, 79 | such as `index(after:)`, will have complexity O(1). 80 | -------------------------------------------------------------------------------- /Xcode/Shared.xcconfig: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2023 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 | SDKROOT = macosx 13 | 14 | CODE_SIGN_STYLE = Automatic 15 | CODE_SIGN_IDENTITY = - 16 | CODE_SIGN_IDENTITY[sdk=iphoneos*] = "iOS Developer" 17 | CODE_SIGN_IDENTITY[sdk=watchos*] = "iOS Developer" 18 | CODE_SIGN_IDENTITY[sdk=appletvos*] = "iOS Developer" 19 | 20 | SWIFT_VERSION = 5.5 21 | ONLY_ACTIVE_ARCH[config=Debug] = YES 22 | 23 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) ALGORITHMS_DARWIN_ONLY 24 | 25 | SWIFT_COMPILATION_MODE[config=Release] = wholemodule 26 | SWIFT_COMPILATION_MODE[config=Debug] = singlefile 27 | 28 | SWIFT_OPTIMIZATION_LEVEL[config=Release] = -O 29 | SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone 30 | 31 | SWIFT_EMIT_LOC_STRINGS = NO 32 | SWIFT_INSTALL_OBJC_HEADER = NO 33 | 34 | COPY_PHASE_STRIP = NO 35 | 36 | DEBUG_INFORMATION_FORMAT[config=Release] = dwarf-with-dsym 37 | DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf 38 | 39 | ALWAYS_SEARCH_USER_PATHS = NO 40 | 41 | ENABLE_TESTABILITY = NO 42 | ENABLE_USER_SCRIPT_SANDBOXING = YES 43 | DEAD_CODE_STRIPPING = YES 44 | 45 | GCC_DYNAMIC_NO_PIC = NO 46 | GCC_NO_COMMON_BLOCKS = YES 47 | GCC_OPTIMIZATION_LEVEL[config=Debug] = 0 48 | 49 | GCC_C_LANGUAGE_STANDARD = gnu11 50 | CLANG_CXX_LANGUAGE_STANDARD = gnu++20 51 | CLANG_ENABLE_MODULES = YES 52 | CLANG_ENABLE_OBJC_ARC = YES 53 | CLANG_ENABLE_OBJC_WEAK = YES 54 | ENABLE_NS_ASSERTIONS[config=Release] = NO 55 | ENABLE_STRICT_OBJC_MSGSEND = YES 56 | GCC_PREPROCESSOR_DEFINITIONS[config=Release] = DEBUG=1 $(inherited) 57 | 58 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES 59 | CLANG_WARN_BOOL_CONVERSION = YES 60 | CLANG_WARN_COMMA = YES 61 | CLANG_WARN_CONSTANT_CONVERSION = YES 62 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES 63 | CLANG_WARN_EMPTY_BODY = YES 64 | CLANG_WARN_ENUM_CONVERSION = YES 65 | CLANG_WARN_INFINITE_RECURSION = YES 66 | CLANG_WARN_INT_CONVERSION = YES 67 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES 68 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES 69 | CLANG_WARN_STRICT_PROTOTYPES = YES 70 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 71 | CLANG_WARN_UNREACHABLE_CODE = YES 72 | 73 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 74 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 75 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 76 | GCC_WARN_UNUSED_FUNCTION = YES 77 | GCC_WARN_UNUSED_VARIABLE = YES 78 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 79 | CLANG_WARN_SUSPICIOUS_MOVE = YES 80 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 81 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 82 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES 83 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 84 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 85 | GCC_WARN_UNDECLARED_SELECTOR = YES 86 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 87 | CLANG_ANALYZER_NONNULL = YES 88 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE 89 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/KeyedTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class KeyedTests: XCTestCase { 16 | private final class SampleError: Error {} 17 | 18 | func testUniqueKeys() { 19 | let d = ["Apple", "Banana", "Cherry"].keyed(by: { $0.first! }) 20 | XCTAssertEqual(d.count, 3) 21 | XCTAssertEqual(d["A"]!, "Apple") 22 | XCTAssertEqual(d["B"]!, "Banana") 23 | XCTAssertEqual(d["C"]!, "Cherry") 24 | XCTAssertNil(d["D"]) 25 | } 26 | 27 | func testEmpty() { 28 | let d = EmptyCollection().keyed(by: { $0.first! }) 29 | XCTAssertEqual(d.count, 0) 30 | } 31 | 32 | func testNonUniqueKeys() throws { 33 | let d = ["Apple", "Avocado", "Banana", "Cherry"].keyed(by: { $0.first! }) 34 | XCTAssertEqual(d.count, 3) 35 | XCTAssertEqual( 36 | d["A"]!, "Avocado", 37 | "On a key-collision, keyed(by:) should take the latest value.") 38 | XCTAssertEqual(d["B"]!, "Banana") 39 | XCTAssertEqual(d["C"]!, "Cherry") 40 | } 41 | 42 | func testNonUniqueKeysWithMergeFunction() { 43 | var resolveCallHistory: [(key: Character, current: String, new: String)] = 44 | [] 45 | let expectedCallHistory = [ 46 | (key: "A", current: "Apple", new: "Avocado"), 47 | (key: "C", current: "Cherry", new: "Coconut"), 48 | ] 49 | 50 | let d = ["Apple", "Avocado", "Banana", "Cherry", "Coconut"].keyed( 51 | by: { $0.first! }, 52 | resolvingConflictsWith: { key, older, newer in 53 | resolveCallHistory.append((key, older, newer)) 54 | return "\(older)-\(newer)" 55 | } 56 | ) 57 | 58 | XCTAssertEqual(d.count, 3) 59 | XCTAssertEqual(d["A"]!, "Apple-Avocado") 60 | XCTAssertEqual(d["B"]!, "Banana") 61 | XCTAssertEqual(d["C"]!, "Cherry-Coconut") 62 | XCTAssertNil(d["D"]) 63 | 64 | // Quick/dirty workaround: tuples aren't Equatable 65 | XCTAssertEqual( 66 | resolveCallHistory.map(String.init(describing:)), 67 | expectedCallHistory.map(String.init(describing:)) 68 | ) 69 | } 70 | 71 | func testThrowingFromKeyFunction() { 72 | let input = ["Apple", "Banana", "Cherry"] 73 | let error = SampleError() 74 | 75 | XCTAssertThrowsError( 76 | try input.keyed(by: { (_: String) -> Character in throw error }) 77 | ) { thrownError in 78 | XCTAssertIdentical(error, thrownError as? SampleError) 79 | } 80 | } 81 | 82 | func testThrowingFromCombineFunction() { 83 | let input = ["Apple", "Avocado", "Banana", "Cherry"] 84 | let error = SampleError() 85 | 86 | XCTAssertThrowsError( 87 | try input.keyed( 88 | by: { $0.first! }, resolvingConflictsWith: { _, _, _ in throw error }) 89 | ) { thrownError in 90 | XCTAssertIdentical(error, thrownError as? SampleError) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Algorithms 2 | 3 | **Swift Algorithms** is an open-source package of sequence and collection algorithms, along with their related types. 4 | 5 | ## Overview 6 | 7 | The Algorithms package provides a variety of sequence and collection operations, letting you cycle over a collection's elements, find combinations and permutations, create a random sample, and more. 8 | 9 | For example, the package includes a group of "chunking" methods, each of which breaks a collection into consecutive subsequences. One version tests adjacent elements to find the breaking point between chunks — you can use it to quickly separate an array into ascending runs: 10 | 11 | ```swift 12 | let numbers = [10, 20, 30, 10, 40, 40, 10, 20] 13 | let chunks = numbers.chunked(by: { $0 <= $1 }) 14 | // [[10, 20, 30], [10, 40, 40], [10, 20]] 15 | ``` 16 | 17 | Another version looks for a change in the transformation of each successive value. You can use that to separate a list of names into groups by the first character: 18 | 19 | ```swift 20 | let names = ["Cassie", "Chloe", "Jasmine", "Jordan", "Taylor"] 21 | let chunks = names.chunked(on: \.first) 22 | // [["Cassie", "Chloe"], ["Jasmine", "Jordan"], ["Taylor"]] 23 | ``` 24 | 25 | Explore more chunking methods and the remainder of the `Algorithms` package in the links below. 26 | 27 | ## Documentation 28 | 29 | For API documentation, see the library's official documentation in Xcode or on the Web. 30 | 31 | - [API documentation][docs] 32 | - [Swift.org announcement][announcement] 33 | - [API Proposals/Guides][guides] 34 | 35 | ## Adding Swift Algorithms as a Dependency 36 | 37 | To use the `Algorithms` library in a SwiftPM project, 38 | add the following line to the dependencies in your `Package.swift` file: 39 | 40 | ```swift 41 | .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), 42 | ``` 43 | 44 | Include `"Algorithms"` as a dependency for your executable target: 45 | 46 | ```swift 47 | .target(name: "", dependencies: [ 48 | .product(name: "Algorithms", package: "swift-algorithms"), 49 | ]), 50 | ``` 51 | 52 | Finally, add `import Algorithms` to your source code. 53 | 54 | ## Source Stability 55 | 56 | The Swift Algorithms package is source stable; version numbers follow [Semantic Versioning](https://semver.org/). Source breaking changes to public API can only land in a new major version. 57 | 58 | The public API of the `swift-algorithms` package consists of non-underscored declarations that are marked `public` in the `Algorithms` module. Interfaces that aren't part of the public API may continue to change in any release, including patch releases. 59 | 60 | Future minor versions of the package may introduce changes to these rules as needed. 61 | 62 | We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, we expect that new versions of this package will require clients to upgrade to a more recent Swift toolchain release. Requiring a new Swift release will only require a minor version bump. 63 | 64 | [docs]: https://swiftpackageindex.com/apple/swift-algorithms/documentation/algorithms 65 | [announcement]: https://swift.org/blog/swift-algorithms/ 66 | [guides]: https://github.com/apple/swift-algorithms/tree/main/Guides 67 | -------------------------------------------------------------------------------- /Xcode/Algorithms.xcodeproj/xcshareddata/xcschemes/Algorithms.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 40 | 46 | 47 | 48 | 49 | 50 | 60 | 61 | 67 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | To be a truly great community, Swift.org needs to welcome developers from all walks of life, 3 | with different backgrounds, and with a wide range of experience. A diverse and friendly 4 | community will have more great ideas, more unique perspectives, and produce more great 5 | code. We will work diligently to make the Swift community welcoming to everyone. 6 | 7 | To give clarity of what is expected of our members, Swift.org has adopted the code of conduct 8 | defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source 9 | communities, and we think it articulates our values well. The full text is copied below: 10 | 11 | ### Contributor Code of Conduct v1.3 12 | As contributors and maintainers of this project, and in the interest of fostering an open and 13 | welcoming community, we pledge to respect all people who contribute through reporting 14 | issues, posting feature requests, updating documentation, submitting pull requests or patches, 15 | and other activities. 16 | 17 | We are committed to making participation in this project a harassment-free experience for 18 | everyone, regardless of level of experience, gender, gender identity and expression, sexual 19 | orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or 20 | nationality. 21 | 22 | Examples of unacceptable behavior by participants include: 23 | - The use of sexualized language or imagery 24 | - Personal attacks 25 | - Trolling or insulting/derogatory comments 26 | - Public or private harassment 27 | - Publishing other’s private information, such as physical or electronic addresses, without explicit permission 28 | - Other unethical or unprofessional conduct 29 | 30 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 31 | commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of 32 | Conduct, or to ban temporarily or permanently any contributor for other behaviors that they 33 | deem inappropriate, threatening, offensive, or harmful. 34 | 35 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and 36 | consistently applying these principles to every aspect of managing this project. Project 37 | maintainers who do not follow or enforce the Code of Conduct may be permanently removed 38 | from the project team. 39 | 40 | This code of conduct applies both within project spaces and in public spaces when an 41 | individual is representing the project or its community. 42 | 43 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 44 | contacting a project maintainer at [conduct@swift.org](mailto:conduct@swift.org). All complaints will be reviewed and 45 | investigated and will result in a response that is deemed necessary and appropriate to the 46 | circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter 47 | of an incident. 48 | 49 | *This policy is adapted from the Contributor Code of Conduct [version 1.3.0](http://contributor-covenant.org/version/1/3/0/).* 50 | 51 | ### Reporting 52 | A working group of community members is committed to promptly addressing any [reported 53 | issues](mailto:conduct@swift.org). Working group members are volunteers appointed by the project lead, with a 54 | preference for individuals with varied backgrounds and perspectives. Membership is expected 55 | to change regularly, and may grow or shrink. 56 | -------------------------------------------------------------------------------- /Sources/Algorithms/Keyed.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 Sequence { 13 | /// Creates a new dictionary keyed by the result of the given closure, using 14 | /// the latest element for any duplicate keys. 15 | /// 16 | /// If the key derived for a new element collides with an existing key from a 17 | /// previous element, the latest value will be kept. 18 | /// 19 | /// - Parameter keyForValue: A closure that returns a key for each element in 20 | /// `self`. 21 | /// - Returns: A dictionary with key-value pairs generated by calling 22 | /// `keyForValue` for each element in this sequence. If `keyForValue` 23 | /// returns the same key for two or more elements, the returned dictionary 24 | /// uses the last value for that key. 25 | @inlinable 26 | public func keyed( 27 | by keyForValue: (Element) throws -> Key 28 | ) rethrows -> [Key: Element] { 29 | try self.reduce(into: [:]) { result, element in 30 | try result[keyForValue(element)] = element 31 | } 32 | } 33 | 34 | /// Creates a new dictionary keyed by the result of the first closure, using 35 | /// the second closure to resolve the ambiguity for any duplicate keys. 36 | /// 37 | /// As the dictionary is built, the initializer calls the `resolve` closure 38 | /// with the current and new values for any duplicate keys. Pass a closure as 39 | /// `resolve` that returns the value to use for that key in the resulting 40 | /// dictionary. The closure can choose between the two values, combine them to 41 | /// produce a new value, or even throw an error. 42 | /// 43 | /// - Parameters: 44 | /// - keyForValue: A closure that returns a key for each element in `self`. 45 | /// - resolve: A closure that is called with the values for any duplicate 46 | /// keys that are encountered. The closure returns the desired value for 47 | /// the final dictionary. 48 | /// - Returns: A dictionary with key-value pairs generated by calling 49 | /// `keyForValue` for each element in this sequence. If `keyForValue` 50 | /// returns the same key for two or more elements, the result of the 51 | /// `resolve` closure is used as the value for that key. 52 | @inlinable 53 | public func keyed( 54 | by keyForValue: (Element) throws -> Key, 55 | resolvingConflictsWith resolve: (Key, Element, Element) throws -> Element 56 | ) rethrows -> [Key: Element] { 57 | var result: [Key: Element] = [:] 58 | 59 | for element in self { 60 | let key = try keyForValue(element) 61 | 62 | if let oldValue = result.updateValue(element, forKey: key) { 63 | let valueToKeep = try resolve(key, oldValue, element) 64 | 65 | // This causes a second look-up for the same key. The standard library can avoid that 66 | // by calling `mutatingFind` to get access to the bucket where the value will end up, 67 | // and updating in place. 68 | // Swift Algorithms doesn't have access to that API, so we make do. 69 | // When this gets merged into the standard library, we should optimize this. 70 | result[key] = valueToKeep 71 | } 72 | } 73 | 74 | return result 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Algorithms/EndsWith.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | //===----------------------------------------------------------------------===// 13 | // EndsWith 14 | //===----------------------------------------------------------------------===// 15 | 16 | extension BidirectionalCollection where Element: Equatable { 17 | /// Returns a Boolean value indicating whether the final elements of the 18 | /// collection are the same as the elements in another collection. 19 | /// 20 | /// This example tests whether one countable range ends with the elements 21 | /// of another countable range. 22 | /// 23 | /// let a = 8...10 24 | /// let b = 1...10 25 | /// 26 | /// print(b.ends(with: a)) 27 | /// // Prints "true" 28 | /// 29 | /// Passing a collection with no elements or an empty collection as 30 | /// `possibleSuffix` always results in `true`. 31 | /// 32 | /// print(b.ends(with: [])) 33 | /// // Prints "true" 34 | /// 35 | /// - Parameter possibleSuffix: A collection to compare to this collection. 36 | /// - Returns: `true` if the initial elements of the collection are the same as 37 | /// the elements of `possibleSuffix`; otherwise, `false`. If 38 | /// `possibleSuffix` has no elements, the return value is `true`. 39 | /// 40 | /// - Complexity: O(*m*), where *m* is the lesser of the length of the 41 | /// collection and the length of `possibleSuffix`. 42 | @inlinable 43 | public func ends( 44 | with possibleSuffix: PossibleSuffix 45 | ) -> Bool where PossibleSuffix.Element == Element { 46 | self.ends(with: possibleSuffix, by: ==) 47 | } 48 | } 49 | 50 | extension BidirectionalCollection { 51 | /// Returns a Boolean value indicating whether the final elements of the 52 | /// collection are equivalent to the elements in another collection, using 53 | /// the given predicate as the equivalence test. 54 | /// 55 | /// The predicate must be an *equivalence relation* over the elements. That 56 | /// is, for any elements `a`, `b`, and `c`, the following conditions must 57 | /// hold: 58 | /// 59 | /// - `areEquivalent(a, a)` is always `true`. (Reflexivity) 60 | /// - `areEquivalent(a, b)` implies `areEquivalent(b, a)`. (Symmetry) 61 | /// - If `areEquivalent(a, b)` and `areEquivalent(b, c)` are both `true`, then 62 | /// `areEquivalent(a, c)` is also `true`. (Transitivity) 63 | /// 64 | /// - Parameters: 65 | /// - possibleSuffix: A collection to compare to this collection. 66 | /// - areEquivalent: A predicate that returns `true` if its two arguments 67 | /// are equivalent; otherwise, `false`. 68 | /// - Returns: `true` if the initial elements of the collection are equivalent 69 | /// to the elements of `possibleSuffix`; otherwise, `false`. If 70 | /// `possibleSuffix` has no elements, the return value is `true`. 71 | /// 72 | /// - Complexity: O(*m*), where *m* is the lesser of the length of the 73 | /// collection and the length of `possibleSuffix`. 74 | @inlinable 75 | public func ends( 76 | with possibleSuffix: PossibleSuffix, 77 | by areEquivalent: (Element, PossibleSuffix.Element) throws -> Bool 78 | ) rethrows -> Bool { 79 | try self 80 | .reversed() 81 | .starts(with: possibleSuffix.reversed(), by: areEquivalent) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Guides/Keyed.md: -------------------------------------------------------------------------------- 1 | # Keyed 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Keyed.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/KeyedTests.swift)] 5 | 6 | Stores the elements of a sequence as the values of a Dictionary, keyed by the result of the given closure. 7 | 8 | ```swift 9 | let fruits = ["Apricot", "Banana", "Apple", "Cherry", "Blackberry", "Avocado", "Coconut"] 10 | let fruitByLetter = fruits.keyed(by: { $0.first! }) 11 | // Results in: 12 | // [ 13 | // "A": "Avocado", 14 | // "B": "Blackberry", 15 | // "C": "Coconut", 16 | // ] 17 | ``` 18 | 19 | On a key-collision, the latest element is kept by default. Alternatively, you can provide a closure which specifies which value to keep: 20 | 21 | ```swift 22 | let fruits = ["Apricot", "Banana", "Apple", "Cherry", "Blackberry", "Avocado", "Coconut"] 23 | let fruitsByLetter = fruits.keyed( 24 | by: { $0.first! }, 25 | resolvingConflictsWith: { key, old, new in old } // Always pick the first fruit 26 | ) 27 | // Results in: 28 | // [ 29 | // "A": "Apricot", 30 | // "B": "Banana", 31 | // "C": "Cherry", 32 | // ] 33 | ``` 34 | 35 | ## Detailed Design 36 | 37 | The `keyed(by:)` and `keyed(by:resolvingConflictsWith:)` methods are declared in an `Sequence` extension, both returning `[Key: Element]`. 38 | 39 | ```swift 40 | extension Sequence { 41 | public func keyed( 42 | by keyForValue: (Element) throws -> Key 43 | ) rethrows -> [Key: Element] 44 | 45 | public func keyed( 46 | by keyForValue: (Element) throws -> Key, 47 | resolvingConflictsWith resolve: ((Key, Element, Element) throws -> Element)? = nil 48 | ) rethrows -> [Key: Element] 49 | } 50 | ``` 51 | 52 | ### Complexity 53 | 54 | Calling `keyed(by:)` is an O(_n_) operation. 55 | 56 | ### Comparison with other languages 57 | 58 | | Language | "Keying" API | 59 | |---------------|-------------| 60 | | Java | [`toMap`](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/stream/Collectors.html#toMap(java.util.function.Function,java.util.function.Function)) | 61 | | Kotlin | [`associatedBy`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/associate-by.html) | 62 | | C# | [`ToDictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.todictionary?view=net-7.0#system-linq-enumerable-todictionary) | 63 | | Ruby (ActiveSupport) | [`index_by`](https://rubydoc.info/gems/activesupport/7.0.5/Enumerable#index_by-instance_method) | 64 | | PHP (Laravel) | [`keyBy`](https://laravel.com/docs/10.x/collections#method-keyby) | 65 | 66 | #### Rejected alternative names 67 | 68 | 1. Java's `toMap` is referring to `Map`/`HashMap`, their naming for Dictionaries and other associative collections. It's easy to confuse with the transformation function, `Sequence.map(_:)`. 69 | 2. C#'s `toXXX()` naming doesn't suite Swift well, which tends to prefer `Foo.init` over `toFoo()` methods. 70 | 3. Ruby's `index_by` naming doesn't fit Swift well, where "index" is a specific term (e.g. the `associatedtype Index` on `Collection`). There is also a [`index(by:)`](Index.md) method in swift-algorithms, is specifically to do with matching elements up with their indices, and not any arbitrary derived value. 71 | 72 | #### Alternative names 73 | 74 | Kotlin's `associatedBy` naming is a good alternative, and matches the past tense of [Swift's API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/), though perhaps we'd spell it `associated(by:)`. 75 | 76 | #### Customization points 77 | 78 | Java and C# are interesting in that they provide overloads that let you customize the type of the outermost collection. E.g. using an `OrderedDictionary` instead of the default (hashed, unordered) `Dictionary`. 79 | -------------------------------------------------------------------------------- /Guides/Grouped.md: -------------------------------------------------------------------------------- 1 | # Grouped 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Grouped.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/GroupedTests.swift)] 5 | 6 | Groups up elements of a sequence into a new Dictionary, whose values are Arrays of grouped elements, each keyed by the result of the given closure. 7 | 8 | ```swift 9 | let fruits = ["Apricot", "Banana", "Apple", "Cherry", "Avocado", "Coconut"] 10 | let fruitsByLetter = fruits.grouped(by: { $0.first! }) 11 | // Results in: 12 | // [ 13 | // "B": ["Banana"], 14 | // "A": ["Apricot", "Apple", "Avocado"], 15 | // "C": ["Cherry", "Coconut"], 16 | // ] 17 | ``` 18 | 19 | If you wish to achieve a similar effect but for single values (instead of Arrays of grouped values), see [`keyed(by:)`](Keyed.md). 20 | 21 | ## Detailed Design 22 | 23 | The `grouped(by:)` method is declared as a `Sequence` extension returning 24 | `[GroupKey: [Element]]`. 25 | 26 | ```swift 27 | extension Sequence { 28 | public func grouped( 29 | by keyForValue: (Element) throws -> GroupKey 30 | ) rethrows -> [GroupKey: [Element]] 31 | } 32 | ``` 33 | 34 | ### Complexity 35 | 36 | Calling `grouped(by:)` is an O(_n_) operation. 37 | 38 | ### Comparison with other languages 39 | 40 | | Language | Grouping API | 41 | |---------------|--------------| 42 | | Java | [`groupingBy`](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/stream/Collectors.html#groupingBy(java.util.function.Function)) | 43 | | Kotlin | [`groupBy`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/group-by.html) | 44 | | C# | [`GroupBy`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.groupby?view=net-7.0#system-linq-enumerable-groupby) | 45 | | Rust | [`group_by`](https://doc.rust-lang.org/std/primitive.slice.html#method.group_by) | 46 | | Ruby | [`group_by`](https://ruby-doc.org/3.2.2/Enumerable.html#method-i-group_by) | 47 | | Python | [`groupby`](https://docs.python.org/3/library/itertools.html#itertools.groupby) | 48 | | PHP (Laravel) | [`groupBy`](https://laravel.com/docs/10.x/collections#method-groupby) | 49 | 50 | #### Naming 51 | 52 | All the surveyed languages name this operation with a variant of "grouped" or "grouping". The past tense `grouped(by:)` best fits [Swift's API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/). 53 | 54 | #### Customization points 55 | 56 | Java and C# are interesting in that they provide multiple overloads with several points of customization: 57 | 58 | 1. Changing the type of the groups. 59 | 1. E.g. the groups can be Sets instead of Arrays. 60 | 1. Akin to calling `.transformValues { group in Set(group) }` on the resultant dictionary, but avoiding the intermediate allocation of Arrays of each group. 61 | 2. Picking which elements end up in the groupings. 62 | 1. The default is the elements of the input sequence, but can be changed. 63 | 2. Akin to calling `.transformValues { group in group.map(someTransform) }` on the resultant dictionary, but avoiding the intermediate allocation of Arrays of each group. 64 | 3. Changing the type of the outermost collection. 65 | 1. E.g using an `OrderedDictionary`, `SortedDictionary` or `TreeDictionary` instead of the default (hashed, unordered) `Dictionary`. 66 | 2. There's no great way to achieve this with the `grouped(by:)`. One could wrap the resultant dictionary in an initializer to one of the other dictionary types, but that isn't sufficient: Once the `Dictionary` loses the ordering, there's no way to get it back when constructing one of the ordered dictionary variants. 67 | 68 | It is not clear which of these points of customization are worth supporting, or what the best way to express them might be. 69 | -------------------------------------------------------------------------------- /Guides/Combinations.md: -------------------------------------------------------------------------------- 1 | # Combinations 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Combinations.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/CombinationsTests.swift)] 5 | 6 | A type that computes combinations of a collection’s elements. 7 | 8 | The `combinations(ofCount:)` method returns a sequence of all the different 9 | combinations of a collection’s elements, with each combination in the order of 10 | the original collection. 11 | 12 | ```swift 13 | let numbers = [10, 20, 30, 40] 14 | for combo in numbers.combinations(ofCount: 2) { 15 | print(combo) 16 | } 17 | // [10, 20] 18 | // [10, 30] 19 | // [10, 40] 20 | // [20, 30] 21 | // [20, 40] 22 | // [30, 40] 23 | ``` 24 | 25 | The combinations of elements are presented in increasing lexicographic order of 26 | the collection’s original ordering. Values that are repeated in the original 27 | collection are always treated as separate values in the resulting combinations: 28 | 29 | ```swift 30 | let numbers2 = [20, 10, 10] 31 | for combo in numbers2.combinations(ofCount: 2) { 32 | print(combo) 33 | } 34 | // [20, 10] 35 | // [20, 10] 36 | // [10, 10] 37 | ``` 38 | 39 | Given a range, the `combinations(ofCount:)` method returns a sequence of all 40 | the different combinations of the given sizes of a collection’s elements in 41 | increasing order of size. 42 | 43 | ```swift 44 | let numbers = [10, 20, 30, 40] 45 | for combo in numbers.combinations(ofCount: 2...3) { 46 | print(combo) 47 | } 48 | // [10, 20] 49 | // [10, 30] 50 | // [10, 40] 51 | // [20, 30] 52 | // [20, 40] 53 | // [30, 40] 54 | // [10, 20, 30] 55 | // [10, 20, 40] 56 | // [10, 30, 40] 57 | // [20, 30, 40] 58 | ``` 59 | 60 | ## Detailed Design 61 | 62 | The `combinations(ofCount:)` method is declared as a `Collection` extension, 63 | and returns a `CombinationsSequence` type: 64 | 65 | ```swift 66 | extension Collection { 67 | public func combinations(ofCount k: Int) -> CombinationsSequence 68 | } 69 | ``` 70 | 71 | Since the `CombinationsSequence` type needs to store an array of the 72 | collection’s indices and mutate the array to generate each permutation, 73 | `CombinationsSequence` only has `Sequence` conformance. Adding `Collection` 74 | conformance would require storing the array in the index type, which would in 75 | turn lead to copying the array at every index advancement. 76 | `CombinationsSequence` does conform to `LazySequenceProtocol` when the base type 77 | conforms. 78 | 79 | ### Complexity 80 | 81 | Calling `combinations(ofCount:)` accesses the count of the collection, so it’s 82 | an O(1) operation for random-access collections, or an O(_n_) operation 83 | otherwise. Creating the iterator for a `CombinationsSequence` instance and each 84 | call to `CombinationsSequence.Iterator.next()` is an O(_n_) operation. 85 | 86 | ### Naming 87 | 88 | The parameter label in `combination(ofCount:)` is the best match for the 89 | [Swift's API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/). A few other options were considered: 90 | 91 | - When the standard library uses `of` as a label, the parameter is generally 92 | the object of the operation, as in `type(of:)` and `firstIndex(of:)`, and 93 | not a configuration detail. 94 | - The `count` label typically indicates the count of the resulting collection, 95 | as in the `repeatElement(_:count:)` function or the `String(repeating:count:)` 96 | initializer. 97 | - `k` is used a term of art for combinations and permutations, but isn't 98 | widely used in programming contexts. 99 | 100 | ### Comparison with other languages 101 | 102 | **Rust/Ruby/Python:** Rust, Ruby, and Python all define functions with 103 | essentially the same semantics as the method described here. 104 | -------------------------------------------------------------------------------- /Sources/Algorithms/Indexed.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | /// A collection wrapper that iterates over the indices and elements of a 13 | /// collection together. 14 | public struct IndexedCollection { 15 | /// The base collection. 16 | @usableFromInline 17 | internal let base: Base 18 | 19 | @inlinable 20 | internal init(base: Base) { 21 | self.base = base 22 | } 23 | } 24 | 25 | extension IndexedCollection: Collection { 26 | /// The element type for an `IndexedCollection` collection. 27 | public typealias Element = (index: Base.Index, element: Base.Element) 28 | 29 | @inlinable 30 | public var startIndex: Base.Index { 31 | base.startIndex 32 | } 33 | 34 | @inlinable 35 | public var endIndex: Base.Index { 36 | base.endIndex 37 | } 38 | 39 | @inlinable 40 | public subscript(position: Base.Index) -> Element { 41 | (index: position, element: base[position]) 42 | } 43 | 44 | @inlinable 45 | public func index(after i: Base.Index) -> Base.Index { 46 | base.index(after: i) 47 | } 48 | 49 | @inlinable 50 | public func index(_ i: Base.Index, offsetBy distance: Int) -> Base.Index { 51 | base.index(i, offsetBy: distance) 52 | } 53 | 54 | @inlinable 55 | public func index( 56 | _ i: Base.Index, 57 | offsetBy distance: Int, 58 | limitedBy limit: Base.Index 59 | ) -> Base.Index? { 60 | base.index(i, offsetBy: distance, limitedBy: limit) 61 | } 62 | 63 | @inlinable 64 | public func distance(from start: Base.Index, to end: Base.Index) -> Int { 65 | base.distance(from: start, to: end) 66 | } 67 | 68 | @inlinable 69 | public var indices: Base.Indices { 70 | base.indices 71 | } 72 | } 73 | 74 | extension IndexedCollection: BidirectionalCollection 75 | where Base: BidirectionalCollection { 76 | @inlinable 77 | public func index(before i: Base.Index) -> Base.Index { 78 | base.index(before: i) 79 | } 80 | } 81 | 82 | extension IndexedCollection: RandomAccessCollection 83 | where Base: RandomAccessCollection {} 84 | 85 | extension IndexedCollection: LazySequenceProtocol, LazyCollectionProtocol 86 | where Base: LazySequenceProtocol {} 87 | 88 | //===----------------------------------------------------------------------===// 89 | // indexed() 90 | //===----------------------------------------------------------------------===// 91 | 92 | extension Collection { 93 | /// Returns a collection of pairs *(i, x)*, where *i* represents an index of 94 | /// the collection, and *x* represents an element. 95 | /// 96 | /// The `indexed()` method is similar to the standard library's `enumerated()` 97 | /// method, but provides the index with each element instead of a zero-based 98 | /// counter. 99 | /// 100 | /// This example iterates over the indices and elements of a set, building an 101 | /// array consisting of indices of names with five or fewer letters. 102 | /// 103 | /// let names: Set = ["Sofia", "Camilla", "Martina", "Mateo", "Nicolás"] 104 | /// var shorterIndices: [Set.Index] = [] 105 | /// for (i, name) in names.indexed() { 106 | /// if name.count <= 5 { 107 | /// shorterIndices.append(i) 108 | /// } 109 | /// } 110 | /// 111 | /// - Returns: A collection of paired indices and elements of this collection. 112 | @inlinable 113 | public func indexed() -> IndexedCollection { 114 | IndexedCollection(base: self) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/IntersperseTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import XCTest 13 | 14 | @testable import Algorithms 15 | 16 | final class IntersperseTests: XCTestCase { 17 | func testSequence() { 18 | let interspersed = (1...).prefix(5).interspersed(with: 0) 19 | expectEqualSequences(interspersed, [1, 0, 2, 0, 3, 0, 4, 0, 5]) 20 | } 21 | 22 | func testSequenceEmpty() { 23 | let interspersed = (1...).prefix(0).interspersed(with: 0) 24 | expectEqualSequences(interspersed, []) 25 | } 26 | 27 | func testString() { 28 | let interspersed = "ABCDE".interspersed(with: "-") 29 | expectEqualSequences(interspersed, "A-B-C-D-E") 30 | } 31 | 32 | func testStringEmpty() { 33 | let interspersed = "".interspersed(with: "-") 34 | expectEqualSequences(interspersed, "") 35 | } 36 | 37 | func testArray() { 38 | let interspersed = [1, 2, 3, 4].interspersed(with: 0) 39 | expectEqualSequences(interspersed, [1, 0, 2, 0, 3, 0, 4]) 40 | } 41 | 42 | func testArrayEmpty() { 43 | let interspersed = [].interspersed(with: 0) 44 | expectEqualSequences(interspersed, []) 45 | } 46 | 47 | func testCollection() { 48 | let interspersed = ["A", "B", "C", "D"].interspersed(with: "-") 49 | XCTAssertEqual(interspersed.count, 7) 50 | } 51 | 52 | func testBidirectionalCollection() { 53 | let reversed = "ABCDE".interspersed(with: "-").reversed() 54 | expectEqualSequences(reversed, "E-D-C-B-A") 55 | } 56 | 57 | func testIndexTraversals() { 58 | let validator = IndexValidator>() 59 | validator.validate("".interspersed(with: "-"), expectedCount: 0) 60 | validator.validate("A".interspersed(with: "-"), expectedCount: 1) 61 | validator.validate("AB".interspersed(with: "-"), expectedCount: 3) 62 | validator.validate("ABCDE".interspersed(with: "-"), expectedCount: 9) 63 | } 64 | 65 | func testIntersperseLazy() { 66 | requireLazySequence((1...).prefix(0).lazy.interspersed(with: 0)) 67 | requireLazyCollection("ABCDE".lazy.interspersed(with: "-")) 68 | } 69 | 70 | func testInterspersedMap() { 71 | expectEqualSequences( 72 | (0..<0).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), 73 | []) 74 | 75 | expectEqualSequences( 76 | (0..<1).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), 77 | [0]) 78 | 79 | expectEqualSequences( 80 | (0..<5).lazy.interspersedMap({ $0 }, with: { $0 + $1 + 100 }), 81 | [0, 101, 1, 103, 2, 105, 3, 107, 4]) 82 | } 83 | 84 | func testInterspersedMapLazy() { 85 | requireLazySequence( 86 | AnySequence([]).lazy.interspersedMap({ $0 }, with: { _, _ in 100 })) 87 | requireLazyCollection( 88 | (0..<0).lazy.interspersedMap({ $0 }, with: { _, _ in 100 })) 89 | } 90 | 91 | func testInterspersedMapIndexTraversals() { 92 | let validator = IndexValidator, Int>>() 93 | validator.validate( 94 | (0..<0).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), 95 | expectedCount: 0) 96 | validator.validate( 97 | (0..<1).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), 98 | expectedCount: 1) 99 | validator.validate( 100 | (0..<2).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), 101 | expectedCount: 3) 102 | validator.validate( 103 | (0..<5).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), 104 | expectedCount: 9) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/RotateTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import XCTest 13 | 14 | @testable import Algorithms 15 | 16 | final class RotateTests: XCTestCase { 17 | /// Tests the example given in the `_reverse(subrange:until:)` documentation. 18 | func testUnderscoreReverse() { 19 | var input = [ 20 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", 21 | "p", 22 | ] 23 | let limit: Int = 4 24 | let (lower, upper) = input._reverse( 25 | subrange: input.startIndex.. 10 } 39 | XCTAssertEqual(resultsNoHeadMatch, [1, 3, 5, 7, 9]) 40 | } 41 | 42 | func testBothEndsMatch() { 43 | // Both ends match, some string of >1 elements do not (return that string). 44 | let results = [2, 10, 11, 15, 20, 21, 100].trimming { $0.isMultiple(of: 2) } 45 | XCTAssertEqual(results, [11, 15, 20, 21]) 46 | } 47 | 48 | func testEverythingMatches() { 49 | // Everything matches (trim everything). 50 | let resultsAllMatch = [1, 3, 5, 7, 9, 11, 13, 15].trimming { _ in true } 51 | XCTAssertEqual(resultsAllMatch, []) 52 | } 53 | 54 | func testEverythingButOneMatches() { 55 | // Both ends match, one element does not (trim all except that element). 56 | let resultsOne = [2, 10, 12, 15, 20, 100].trimming { $0.isMultiple(of: 2) } 57 | XCTAssertEqual(resultsOne, [15]) 58 | } 59 | 60 | func testTrimmingPrefix() { 61 | let results = [2, 10, 12, 15, 20, 100].trimmingPrefix { 62 | $0.isMultiple(of: 2) 63 | } 64 | XCTAssertEqual(results, [15, 20, 100]) 65 | } 66 | 67 | func testTrimmingSuffix() { 68 | let results = [2, 10, 12, 15, 20, 100].trimmingSuffix { 69 | $0.isMultiple(of: 2) 70 | } 71 | XCTAssertEqual(results, [2, 10, 12, 15]) 72 | } 73 | 74 | // Self == Self.Subsequence 75 | func testTrimNoAmbiguity() { 76 | var values = [2, 10, 12, 15, 20, 100] as ArraySlice 77 | values.trim { $0.isMultiple(of: 2) } 78 | XCTAssertEqual(values, [15]) 79 | } 80 | 81 | // Self == Self.Subsequence 82 | func testTrimPrefixNoAmbiguity() { 83 | var values = [2, 10, 12, 15, 20, 100] as ArraySlice 84 | values.trimPrefix { $0.isMultiple(of: 2) } 85 | XCTAssertEqual(values, [15, 20, 100]) 86 | } 87 | 88 | // Self == Self.Subsequence 89 | func testTrimSuffixNoAmbiguity() { 90 | var values = [2, 10, 12, 15, 20, 100] as ArraySlice 91 | values.trimSuffix { $0.isMultiple(of: 2) } 92 | XCTAssertEqual(values, [2, 10, 12, 15]) 93 | } 94 | 95 | func testTrimRangeReplaceable() { 96 | var values = [2, 10, 12, 15, 20, 100] 97 | values.trim { $0.isMultiple(of: 2) } 98 | XCTAssertEqual(values, [15]) 99 | } 100 | 101 | func testTrimPrefixRangeReplaceable() { 102 | var values = [2, 10, 12, 15, 20, 100] 103 | values.trimPrefix { $0.isMultiple(of: 2) } 104 | XCTAssertEqual(values, [15, 20, 100]) 105 | } 106 | 107 | func testTrimSuffixRangeReplaceable() { 108 | var values = [2, 10, 12, 15, 20, 100] 109 | values.trimSuffix { $0.isMultiple(of: 2) } 110 | XCTAssertEqual(values, [2, 10, 12, 15]) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Algorithms/Suffix.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | //===----------------------------------------------------------------------===// 13 | // suffix(while:) 14 | //===----------------------------------------------------------------------===// 15 | 16 | extension BidirectionalCollection { 17 | /// Returns a subsequence containing the suffix of this collection where 18 | /// all elements pass the given closure. 19 | /// 20 | /// - Parameter predicate: A closure that takes an element of the sequence as 21 | /// its argument and returns `true` if the element should be included or 22 | /// `false` if it should be excluded. Once the predicate returns `false` it 23 | /// will not be called again. 24 | /// - Returns: A subsequence that is a suffix of the collection, where 25 | /// `predicate` returns `true` for all the elements of the returned 26 | /// subsequence. 27 | /// 28 | /// - Complexity: O(*n*), where *n* is the length of the collection. 29 | @inlinable 30 | public func suffix( 31 | while predicate: (Element) throws -> Bool 32 | ) rethrows -> SubSequence { 33 | try self[startOfSuffix(while: predicate)...] 34 | } 35 | } 36 | 37 | //===----------------------------------------------------------------------===// 38 | // endOfPrefix(while:) 39 | //===----------------------------------------------------------------------===// 40 | 41 | extension Collection { 42 | /// Returns the exclusive upper bound of the prefix of elements that satisfy 43 | /// the predicate. 44 | /// 45 | /// - Parameter predicate: A closure that takes an element of the collection 46 | /// as its argument and returns `true` if the element is part of the prefix 47 | /// or `false` if it is not. Once the predicate returns `false` it will not 48 | /// be called again. 49 | /// - Returns: The index of the first element for which `predicate` does not 50 | /// succeed, or `endIndex` if all the collection's elements pass. 51 | /// 52 | /// - Complexity: O(*n*), where *n* is the length of the collection. 53 | @inlinable 54 | public func endOfPrefix( 55 | while predicate: (Element) throws -> Bool 56 | ) rethrows -> Index { 57 | var index = startIndex 58 | while try index != endIndex && predicate(self[index]) { 59 | formIndex(after: &index) 60 | } 61 | return index 62 | } 63 | } 64 | 65 | //===----------------------------------------------------------------------===// 66 | // startOfSuffix(while:) 67 | //===----------------------------------------------------------------------===// 68 | 69 | extension BidirectionalCollection { 70 | /// Returns the inclusive lower bound of the suffix of elements that satisfy 71 | /// the predicate. 72 | /// 73 | /// - Parameter predicate: A closure that takes an element of the collection 74 | /// as its argument and returns `true` if the element is part of the suffix 75 | /// or `false` if it is not. Once the predicate returns `false` it will not 76 | /// be called again. 77 | /// - Returns: The index of the last element for which `predicate` does not 78 | /// succeed, or `startIndex` if all the collection's elements pass. 79 | /// 80 | /// - Complexity: O(*n*), where *n* is the length of the collection. 81 | @inlinable 82 | public func startOfSuffix( 83 | while predicate: (Element) throws -> Bool 84 | ) rethrows -> Index { 85 | var index = endIndex 86 | while index != startIndex { 87 | let after = index 88 | formIndex(before: &index) 89 | if try !predicate(self[index]) { 90 | return after 91 | } 92 | } 93 | return index 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Guides/Joined.md: -------------------------------------------------------------------------------- 1 | # Joined 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Joined.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/JoinedTests.swift)] 5 | 6 | Concatenate a sequence of sequences, inserting a separator between each element. 7 | 8 | The separator can be either a single element or a sequence of elements, and it 9 | can optionally depend on the sequences right before and after it by returning it 10 | from a closure: 11 | 12 | ```swift 13 | for number in [[1], [2, 3], [4, 5, 6]].joined(by: 100) { 14 | print(number) 15 | } 16 | // 1, 100, 2, 3, 100, 4, 5, 6 17 | 18 | for number in [[10], [20, 30], [40, 50, 60]].joined(by: { [$0.count, $1.count] }) { 19 | print(number) 20 | } 21 | // 10, 1, 2, 20, 30, 2, 3, 40, 50, 60 22 | ``` 23 | 24 | ## Detailed Design 25 | 26 | The versions that take a closure are executed eagerly and are defined on 27 | `Sequence`: 28 | 29 | ```swift 30 | extension Sequence where Element: Sequence { 31 | public func joined( 32 | by separator: (Element, Element) throws -> Element.Element 33 | ) rethrows -> [Element.Element] 34 | 35 | public func joined( 36 | by separator: (Element, Element) throws -> Separator 37 | ) rethrows -> [Element.Element] 38 | where Separator: Sequence, Separator.Element == Element.Element 39 | } 40 | ``` 41 | 42 | The versions that do not take a closure are defined on both `Sequence` and 43 | `Collection` because the resulting collections need to precompute their start 44 | index to ensure O(1) access: 45 | 46 | ```swift 47 | extension Sequence where Element: Sequence { 48 | public func joined(by separator: Element.Element) 49 | -> JoinedBySequence> 50 | 51 | public func joined( 52 | by separator: Separator 53 | ) -> JoinedBySequence 54 | where Separator: Collection, Separator.Element == Element.Element 55 | } 56 | 57 | extension Collection where Element: Sequence { 58 | public func joined(by separator: Element.Element) 59 | -> JoinedByCollection> 60 | 61 | public func joined( 62 | by separator: Separator 63 | ) -> JoinedByCollection 64 | where Separator: Collection, Separator.Element == Element.Element 65 | } 66 | ``` 67 | 68 | Note that the sequence separator of the closure-less version defined on 69 | `Sequence` is required to be a `Collection`, because a plain `Sequence` cannot in 70 | general be iterated over multiple times. 71 | 72 | The closure-based versions also have lazy variants that are defined on both 73 | lazy sequences and collections for the same reason as explained above: 74 | 75 | ```swift 76 | extension LazySequenceProtocol where Element: Sequence { 77 | public func joined( 78 | by separator: @escaping (Element, Element) -> Element.Element 79 | ) -> JoinedByClosureSequence> 80 | 81 | public func joined( 82 | by separator: @escaping (Element, Element) -> Separator 83 | ) -> JoinedByClosureSequence 84 | } 85 | 86 | extension LazySequenceProtocol where Self: Collection, Element: Collection { 87 | public func joined( 88 | by separator: @escaping (Element, Element) -> Element.Element 89 | ) -> JoinedByClosureCollection> 90 | 91 | public func joined( 92 | by separator: @escaping (Element, Element) -> Separator 93 | ) -> JoinedByClosureCollection 94 | } 95 | ``` 96 | 97 | `JoinedBySequence`, `JoinedByClosureSequence`, `JoinedByCollection`, and 98 | `JoinedByClosureCollection` conform to `LazySequenceProtocol` when the base 99 | sequence conforms. `JoinedByCollection` and `JoinedByClosureCollection` also 100 | conform to `BidirectionalCollection` when the base collection conforms. 101 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/WindowsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import XCTest 13 | 14 | @testable import Algorithms 15 | 16 | final class WindowsTests: XCTestCase { 17 | func testWindowsOfString() { 18 | let s = "swift" 19 | let w = s.windows(ofCount: 2) 20 | var i = w.startIndex 21 | 22 | expectEqualSequences(w[i], "sw") 23 | w.formIndex(after: &i) 24 | expectEqualSequences(w[i], "wi") 25 | w.formIndex(after: &i) 26 | expectEqualSequences(w[i], "if") 27 | w.formIndex(after: &i) 28 | expectEqualSequences(w[i], "ft") 29 | 30 | // w.index(after: w.endIndex) // ← Precondition failed: windows index is out of range 31 | // w.index(before: w.startIndex) // ← Precondition failed: windows index is out of range 32 | // w.formIndex(after: &i); w[i] // ← Precondition failed: windows index is out of range 33 | } 34 | 35 | func testWindowsOfRange() { 36 | let a = 0...100 37 | 38 | XCTAssertTrue(a.windows(ofCount: 200).isEmpty) 39 | 40 | let w = a.windows(ofCount: 10) 41 | 42 | expectEqualSequences(w.first!, 0..<10) 43 | expectEqualSequences(w.last!, 91..<101) 44 | } 45 | 46 | func testWindowsOfInt() { 47 | let a = [0, 1, 0, 1].windows(ofCount: 2) 48 | 49 | XCTAssertEqual(a.count, 3) 50 | XCTAssertEqual(a.map { $0.reduce(0, +) }, [1, 1, 1]) 51 | 52 | let a2 = [0, 1, 2, 3, 4, 5, 6].windows(ofCount: 3).map { 53 | $0.reduce(0, +) 54 | }.reduce(0, +) 55 | 56 | XCTAssertEqual(a2, 3 + 6 + 9 + 12 + 15) 57 | } 58 | 59 | func testWindowsCount() { 60 | let a = [0, 1, 2, 3, 4, 5] 61 | XCTAssertEqual(a.windows(ofCount: 3).count, 4) 62 | 63 | let a2 = [0, 1, 2, 3, 4] 64 | XCTAssertEqual(a2.windows(ofCount: 6).count, 0) 65 | 66 | let a3: [Int] = [] 67 | XCTAssertEqual(a3.windows(ofCount: 2).count, 0) 68 | } 69 | 70 | func testWindowsSecondAndLast() { 71 | let a = [0, 1, 2, 3, 4, 5] 72 | let w = a.windows(ofCount: 4) 73 | let snd = w[w.index(after: w.startIndex)] 74 | expectEqualSequences(snd, [1, 2, 3, 4]) 75 | 76 | let w2 = a.windows(ofCount: 3) 77 | expectEqualSequences(w2.last!, [3, 4, 5]) 78 | } 79 | 80 | func testWindowsIndexAfterAndBefore() { 81 | let a = [0, 1, 2, 3, 4, 5].windows(ofCount: 2) 82 | var i = a.startIndex 83 | a.formIndex(after: &i) 84 | a.formIndex(after: &i) 85 | a.formIndex(before: &i) 86 | expectEqualSequences(a[i], [1, 2]) 87 | } 88 | 89 | func testWindowsIndexTraversals() { 90 | let validator = IndexValidator>( 91 | indicesIncludingEnd: { windows in 92 | let endIndex = windows.base.endIndex 93 | let indices = windows.base.indices + [endIndex] 94 | return zip(indices, indices.dropFirst(windows.windowSize)) 95 | .map { .init(lowerBound: $0, upperBound: $1) } 96 | + [.init(lowerBound: endIndex, upperBound: endIndex)] 97 | }) 98 | 99 | validator.validate("".windows(ofCount: 1), expectedCount: 0) 100 | validator.validate("a".windows(ofCount: 1), expectedCount: 1) 101 | validator.validate("ab".windows(ofCount: 1), expectedCount: 2) 102 | validator.validate("abc".windows(ofCount: 1), expectedCount: 3) 103 | validator.validate("".windows(ofCount: 3), expectedCount: 0) 104 | validator.validate("a".windows(ofCount: 3), expectedCount: 0) 105 | validator.validate("abc".windows(ofCount: 3), expectedCount: 1) 106 | validator.validate("abcdefgh".windows(ofCount: 3), expectedCount: 6) 107 | } 108 | 109 | func testWindowsLazy() { 110 | requireLazyCollection([0, 1, 2, 3].lazy.windows(ofCount: 2)) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/ReductionsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class ReductionsTests: XCTestCase { 16 | struct TestError: Error {} 17 | 18 | // MARK: - Exclusive Reductions 19 | 20 | func testExclusiveLazy() { 21 | expectEqualSequences( 22 | (1...).prefix(4).lazy.reductions(0, +), [0, 1, 3, 6, 10]) 23 | expectEqualSequences((1...).prefix(1).lazy.reductions(0, +), [0, 1]) 24 | expectEqualSequences((1...).prefix(0).lazy.reductions(0, +), [0]) 25 | 26 | expectEqualCollections( 27 | [1, 2, 3, 4].lazy.reductions(0, +), [0, 1, 3, 6, 10]) 28 | expectEqualCollections([1].lazy.reductions(0, +), [0, 1]) 29 | expectEqualCollections(EmptyCollection().lazy.reductions(0, +), [0]) 30 | 31 | XCTAssertEqual( 32 | [1, 2, 3, 4].lazy.reductions(into: 0, +=), [0, 1, 3, 6, 10]) 33 | 34 | XCTAssertEqual([1].lazy.reductions(into: 0, +=), [0, 1]) 35 | 36 | XCTAssertEqual(EmptyCollection().lazy.reductions(into: 0, +=), [0]) 37 | 38 | requireLazySequence((1...).prefix(1).lazy.reductions(0, +)) 39 | requireLazySequence([1].lazy.reductions(0, +)) 40 | requireLazyCollection([1].lazy.reductions(0, +)) 41 | } 42 | 43 | func testExclusiveEager() { 44 | XCTAssertEqual([1, 2, 3, 4].reductions(0, +), [0, 1, 3, 6, 10]) 45 | XCTAssertEqual([1].reductions(0, +), [0, 1]) 46 | XCTAssertEqual(EmptyCollection().reductions(0, +), [0]) 47 | 48 | XCTAssertEqual([1, 2, 3, 4].reductions(into: 0, +=), [0, 1, 3, 6, 10]) 49 | 50 | XCTAssertEqual([1].reductions(into: 0, +=), [0, 1]) 51 | 52 | XCTAssertEqual(EmptyCollection().reductions(into: 0, +=), [0]) 53 | 54 | XCTAssertNoThrow(try [].reductions(0) { _, _ in throw TestError() }) 55 | XCTAssertThrowsError(try [1].reductions(0) { _, _ in throw TestError() }) 56 | } 57 | 58 | func testExclusiveIndexTraversals() { 59 | let validator = IndexValidator< 60 | ExclusiveReductionsSequence, Int> 61 | >() 62 | validator.validate((0..<0).lazy.reductions(0, +), expectedCount: 1) 63 | validator.validate((0..<1).lazy.reductions(0, +), expectedCount: 2) 64 | validator.validate((0..<4).lazy.reductions(0, +), expectedCount: 5) 65 | } 66 | 67 | // MARK: - Inclusive Reductions 68 | 69 | func testInclusiveLazy() { 70 | expectEqualSequences((1...).prefix(4).lazy.reductions(+), [1, 3, 6, 10]) 71 | expectEqualSequences((1...).prefix(1).lazy.reductions(+), [1]) 72 | expectEqualSequences((1...).prefix(0).lazy.reductions(+), []) 73 | 74 | expectEqualCollections([1, 2, 3, 4].lazy.reductions(+), [1, 3, 6, 10]) 75 | expectEqualCollections([1].lazy.reductions(+), [1]) 76 | expectEqualCollections(EmptyCollection().lazy.reductions(+), []) 77 | 78 | requireLazySequence((1...).prefix(1).lazy.reductions(+)) 79 | requireLazySequence([1].lazy.reductions(+)) 80 | requireLazyCollection([1].lazy.reductions(+)) 81 | } 82 | 83 | func testInclusiveEager() { 84 | XCTAssertEqual([1, 2, 3, 4].reductions(+), [1, 3, 6, 10]) 85 | XCTAssertEqual([1].reductions(+), [1]) 86 | XCTAssertEqual(EmptyCollection().reductions(+), []) 87 | 88 | XCTAssertNoThrow(try [].reductions { _, _ in throw TestError() }) 89 | XCTAssertNoThrow(try [1].reductions { _, _ in throw TestError() }) 90 | XCTAssertThrowsError(try [1, 1].reductions { _, _ in throw TestError() }) 91 | } 92 | 93 | func testInclusiveIndexTraversals() { 94 | let validator = IndexValidator>>() 95 | validator.validate((0..<0).lazy.reductions(+), expectedCount: 0) 96 | validator.validate((0..<1).lazy.reductions(+), expectedCount: 1) 97 | validator.validate((0..<4).lazy.reductions(+), expectedCount: 4) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Guides/MinMax.md: -------------------------------------------------------------------------------- 1 | # Minima and/or Maxima 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/MinMax.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/MinMaxTests.swift)] 5 | 6 | Returns the smallest or largest elements of this collection, sorted by a 7 | predicate or in the order defined by `Comparable` conformance. 8 | 9 | If you need to sort a collection but only need access to a prefix or suffix of 10 | the sorted elements, using these methods can give you a performance boost over 11 | sorting the entire collection. The order of equal elements is guaranteed to be 12 | preserved. 13 | 14 | ```swift 15 | let numbers = [7, 1, 6, 2, 8, 3, 9] 16 | let smallestThree = numbers.min(count: 3, sortedBy: <) 17 | // [1, 2, 3] 18 | ``` 19 | 20 | Return the smallest and largest elements of this sequence, determined by a 21 | predicate or in the order defined by `Comparable` conformance. 22 | 23 | If you need both the minimum and maximum values of a collection, using these 24 | methods can give you a performance boost over running the `min` method followed 25 | by the `max` method. Plus they work with single-pass sequences. 26 | 27 | ```swift 28 | let numbers = [7, 1, 6, 2, 8, 3, 9] 29 | if let (smallest, largest) = numbers.minAndMax(by: <) { 30 | // Work with 1 and 9.... 31 | } 32 | ``` 33 | 34 | ## Detailed Design 35 | 36 | This adds the `Collection` methods shown below: 37 | 38 | ```swift 39 | extension Collection { 40 | public func min( 41 | count: Int, 42 | sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool 43 | ) rethrows -> [Element] 44 | 45 | public func max( 46 | count: Int, 47 | sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool 48 | ) rethrows -> [Element] 49 | } 50 | ``` 51 | 52 | And the `Sequence` method: 53 | 54 | ```swift 55 | extension Sequence { 56 | public func minAndMax( 57 | by areInIncreasingOrder: (Element, Element) throws -> Bool 58 | ) rethrows -> (min: Element, max: Element)? 59 | } 60 | ``` 61 | 62 | Additionally, versions of these methods for `Comparable` types are also 63 | provided: 64 | 65 | ```swift 66 | extension Collection where Element: Comparable { 67 | public func min(count: Int) -> [Element] 68 | 69 | public func max(count: Int) -> [Element] 70 | } 71 | 72 | extension Sequence where Element: Comparable { 73 | public func minAndMax() -> (min: Element, max: Element)? 74 | } 75 | ``` 76 | 77 | ### Complexity 78 | 79 | The algorithm used for minimal- or maximal-ordered subsets is based on 80 | [Soroush Khanlou's research on this matter](https://khanlou.com/2018/12/analyzing-complexity/). 81 | The total complexity is `O(k log k + nk)`, which will result in a runtime close 82 | to `O(n)` if *k* is a small amount. If *k* is a large amount (more than 10% of 83 | the collection), we fall back to sorting the entire array. Realistically, this 84 | means the worst case is actually `O(n log n)`. 85 | 86 | Here are some benchmarks we made that demonstrates how this implementation 87 | (SmallestM) behaves when *k* increases (before implementing the fallback): 88 | 89 | ![Benchmark](Resources/SortedPrefix/FewElements.png) 90 | ![Benchmark 2](Resources/SortedPrefix/ManyElements.png) 91 | 92 | The algorithm used for simultaneous minimum and maximum is slightly optimized. 93 | At each iteration, two elements are read, their relative order is determined, 94 | then each is compared against exactly one of the current extrema for potential 95 | replacement. When a comparison predicate has to analyze every component of both 96 | operands, the optimized algorithm isn't much faster than the straightforward 97 | approach. But when a predicate only needs to compare a small part of each 98 | instance, the optimization shines through. 99 | 100 | ### Comparison with other languages 101 | 102 | **C++:** The `` library defines a `partial_sort` function where the 103 | entire array is returned using a partial heap sort. It also defines a 104 | `minmax_element` function that scans a range for its minimal and maximal 105 | elements. 106 | 107 | **Python:** Defines a `heapq` priority queue that can be used to manually 108 | achieve the same result. 109 | 110 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/UniquePermutationsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class UniquePermutationsTests: XCTestCase { 16 | static let numbers = [1, 1, 1, 2, 3] 17 | 18 | static let numbersPermutations: [[[Int]]] = [ 19 | // k = 0 20 | [[]], 21 | // 1 22 | [[1], [2], [3]], 23 | // 2 24 | [[1, 1], [1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]], 25 | // 3 26 | [ 27 | [1, 1, 1], [1, 1, 2], [1, 1, 3], 28 | [1, 2, 1], [1, 2, 3], [1, 3, 1], [1, 3, 2], 29 | [2, 1, 1], [2, 1, 3], [2, 3, 1], 30 | [3, 1, 1], [3, 1, 2], [3, 2, 1], 31 | ], 32 | // 4 33 | [ 34 | [1, 1, 1, 2], [1, 1, 1, 3], 35 | [1, 1, 2, 1], [1, 1, 2, 3], 36 | [1, 1, 3, 1], [1, 1, 3, 2], 37 | [1, 2, 1, 1], [1, 2, 1, 3], [1, 2, 3, 1], 38 | [1, 3, 1, 1], [1, 3, 1, 2], [1, 3, 2, 1], 39 | [2, 1, 1, 1], [2, 1, 1, 3], [2, 1, 3, 1], [2, 3, 1, 1], 40 | [3, 1, 1, 1], [3, 1, 1, 2], [3, 1, 2, 1], [3, 2, 1, 1], 41 | ], 42 | // 5 43 | [ 44 | [1, 1, 1, 2, 3], [1, 1, 1, 3, 2], 45 | [1, 1, 2, 1, 3], [1, 1, 2, 3, 1], 46 | [1, 1, 3, 1, 2], [1, 1, 3, 2, 1], 47 | [1, 2, 1, 1, 3], [1, 2, 1, 3, 1], [1, 2, 3, 1, 1], 48 | [1, 3, 1, 1, 2], [1, 3, 1, 2, 1], [1, 3, 2, 1, 1], 49 | [2, 1, 1, 1, 3], [2, 1, 1, 3, 1], [2, 1, 3, 1, 1], [2, 3, 1, 1, 1], 50 | [3, 1, 1, 1, 2], [3, 1, 1, 2, 1], [3, 1, 2, 1, 1], [3, 2, 1, 1, 1], 51 | ], 52 | ] 53 | } 54 | 55 | extension UniquePermutationsTests { 56 | func testEmpty() { 57 | expectEqualSequences(([] as [Int]).uniquePermutations(), [[]]) 58 | expectEqualSequences(([] as [Int]).uniquePermutations(ofCount: 0), [[]]) 59 | expectEqualSequences(([] as [Int]).uniquePermutations(ofCount: 1), []) 60 | expectEqualSequences( 61 | ([] as [Int]).uniquePermutations(ofCount: 1...3), []) 62 | } 63 | 64 | func testSingleCounts() { 65 | for (k, expectation) in Self.numbersPermutations.enumerated() { 66 | expectEqualSequences( 67 | expectation, 68 | Self.numbers.uniquePermutations(ofCount: k)) 69 | } 70 | 71 | expectEqualSequences( 72 | Self.numbersPermutations[5], 73 | Self.numbers.uniquePermutations()) 74 | } 75 | 76 | func testRanges() { 77 | for lower in Self.numbersPermutations.indices { 78 | // upper bounded 79 | expectEqualSequences( 80 | Self.numbersPermutations[...lower].joined(), 81 | Self.numbers.uniquePermutations(ofCount: ...lower)) 82 | 83 | // lower bounded 84 | expectEqualSequences( 85 | Self.numbersPermutations[lower...].joined(), 86 | Self.numbers.uniquePermutations(ofCount: lower...)) 87 | 88 | for upper in lower.. Bool { 106 | lhs.value == rhs.value 107 | } 108 | 109 | func hash(into hasher: inout Hasher) { 110 | hasher.combine(value) 111 | } 112 | } 113 | 114 | func testFirstUnique() { 115 | // When duplicate elements are encountered, all permutations use the first 116 | // instance of the duplicated elements. 117 | let numbers = Self.numbers.map(IntBox.init) 118 | for k in 0...numbers.count { 119 | for p in numbers.uniquePermutations(ofCount: k) { 120 | XCTAssertTrue( 121 | p.filter { $0.value == 1 }.allSatisfy { $0 === numbers[0] }) 122 | } 123 | } 124 | } 125 | 126 | func testLaziness() { 127 | requireLazySequence("ABCD".lazy.uniquePermutations()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/RandomSampleTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import XCTest 13 | 14 | @testable import Algorithms 15 | 16 | func validateRandomSamples( 17 | _ samples: [Int: Int], 18 | elements: S, 19 | expectedValue: Int, 20 | file: StaticString = (#file), line: UInt = #line 21 | ) where S.Element == Int { 22 | let expectedRange = ((expectedValue / 3) * 2)...((expectedValue / 3) * 4) 23 | expectEqualSequences( 24 | samples.keys.sorted(), elements, 25 | file: file, line: line) 26 | for v in samples.values { 27 | XCTAssert(expectedRange.contains(v), file: file, line: line) 28 | } 29 | } 30 | 31 | private let n = 100 32 | private let k = 12 33 | private let iterations = 10_000 34 | private let c = 0.. UInt64 { 0 } 107 | } 108 | var zero = ZeroGenerator() 109 | _ = nextOffset(w: 1, using: &zero) // must not crash 110 | 111 | struct AlmostAllZeroGenerator: RandomNumberGenerator { 112 | private var forward: SplitMix64 113 | private var count: Int = 0 114 | 115 | init(seed: UInt64) { 116 | forward = SplitMix64(seed: seed) 117 | } 118 | 119 | mutating func next() -> UInt64 { 120 | defer { count &+= 1 } 121 | if count % 1000 == 0 { return forward.next() } 122 | return 0 123 | } 124 | } 125 | 126 | var almostAllZero = AlmostAllZeroGenerator(seed: 0) 127 | _ = s.randomSample(count: k, using: &almostAllZero) // must not crash 128 | almostAllZero = AlmostAllZeroGenerator(seed: 0) 129 | _ = c.randomSample(count: k, using: &almostAllZero) // must not crash 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Guides/Partition.md: -------------------------------------------------------------------------------- 1 | # Partition 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Partition.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/PartitionTests.swift)] 5 | 6 | Methods for performing a stable partition on mutable collections, and for 7 | finding the partitioning index in an already partitioned collection. 8 | 9 | The standard library’s existing `partition(by:)` method, which re-orders the 10 | elements in a collection into two partitions based on a given predicate, doesn’t 11 | guarantee stability for either partition. That is, the order of the elements in 12 | each partition doesn’t necessarily match their relative order in the original 13 | collection. These new methods expand on the existing `partition(by:)` by 14 | providing stability for one or both partitions. 15 | 16 | ```swift 17 | // existing partition(by:) - unstable ordering 18 | var numbers = [10, 20, 30, 40, 50, 60, 70, 80] 19 | let p1 = numbers.partition(by: { $0.isMultiple(of: 20) }) 20 | // p1 == 4 21 | // numbers == [10, 70, 30, 50, 40, 60, 20, 80] 22 | 23 | // new stablePartition(by:) - keeps the relative order of both partitions 24 | numbers = [10, 20, 30, 40, 50, 60, 70, 80] 25 | let p2 = numbers.stablePartition(by: { $0.isMultiple(of: 20) }) 26 | // p2 == 4 27 | // numbers == [10, 30, 50, 70, 20, 40, 60, 80] 28 | ``` 29 | 30 | Since partitioning is frequently used in divide-and-conquer algorithms, we also 31 | include a variant that accepts a range parameter to avoid copying when mutating 32 | slices, as well as a range-based variant of the existing standard library 33 | partition. 34 | 35 | The `partitioningIndex(where:)` method returns the index of the start of the 36 | second partition when called on an already partitioned collection. 37 | 38 | ```swift 39 | let numbers = [10, 30, 50, 70, 20, 40, 60] 40 | let p = numbers.partitioningIndex(where: { $0.isMultiple(of: 20) }) 41 | // numbers[.. Bool 68 | ) rethrows -> Index 69 | 70 | mutating func stablePartition( 71 | subrange: Range, 72 | by belongsInSecondPartition: (Element) throws -> Bool 73 | ) rethrows -> Index 74 | 75 | mutating func partition( 76 | subrange: Range, 77 | by belongsInSecondPartition: (Element) throws -> Bool 78 | ) rethrows -> Index 79 | } 80 | 81 | extension Collection { 82 | func partitioningIndex( 83 | where belongsInSecondPartition: (Element) throws -> Bool 84 | ) rethrows -> Index 85 | } 86 | 87 | extension Sequence { 88 | public func partitioned( 89 | by predicate: (Element) throws -> Bool 90 | ) rethrows -> (falseElements: [Element], trueElements: [Element]) 91 | } 92 | ``` 93 | 94 | ### Complexity 95 | 96 | The existing partition is an O(_n_) operation, where _n_ is the length of the 97 | range to be partitioned, while the stable partition is O(_n_ log _n_). Both 98 | partitions have algorithms with improved performance for bidirectional 99 | collections, so it would be ideal for those to be customization points were they 100 | to eventually land in the standard library. 101 | 102 | `partitioningIndex(where:)` is a slight generalization of a binary search, and 103 | is an O(log _n_) operation for random-access collections; O(_n_) otherwise. 104 | 105 | `partitioned(by:)` is an O(_n_) operation, where _n_ is the number of elements 106 | in the original sequence. 107 | 108 | ### Comparison with other languages 109 | 110 | **C++:** The `` library defines `partition`, `stable_partition`, and 111 | `partition_point` functions with similar semantics to these. Notably, in the C++ 112 | implementation, the result of partitioning has the opposite polarity, with the 113 | passing elements in the first partition and failing elements in the second. 114 | 115 | **Rust:** Rust includes the `partition` method, which returns separate 116 | collections of passing and failing elements, and `partition_in_place`, which 117 | matches the Swift standard library’s existing `partition(by:)` method. 118 | -------------------------------------------------------------------------------- /Guides/Permutations.md: -------------------------------------------------------------------------------- 1 | # Permutations 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Permutations.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/PermutationsTests.swift)] 5 | 6 | Methods that compute permutations of a collection’s elements, or of a subset of 7 | those elements. 8 | 9 | The `permutations(ofCount:)` method, when called without the `ofCount` 10 | parameter, returns a sequence of all the different permutations of a 11 | collection’s elements: 12 | 13 | ```swift 14 | let numbers = [10, 20, 30] 15 | for perm in numbers.permutations() { 16 | print(perm) 17 | } 18 | // [10, 20, 30] 19 | // [10, 30, 20] 20 | // [20, 10, 30] 21 | // [20, 30, 10] 22 | // [30, 10, 20] 23 | // [30, 20, 10] 24 | ``` 25 | 26 | Passing a value for `ofCount` generates partial permutations, each with the 27 | specified number of elements: 28 | 29 | ```swift 30 | for perm in numbers.permutations(ofCount: 2) { 31 | print(perm) 32 | } 33 | // [10, 20] 34 | // [10, 30] 35 | // [20, 10] 36 | // [20, 30] 37 | // [30, 10] 38 | // [30, 20] 39 | ``` 40 | 41 | The permutations or partial permutations are generated in increasing 42 | lexicographic order of the collection’s original ordering (rather than the order 43 | of the elements themselves). The first permutation will always consist of 44 | elements in their original order, and the last will have the elements in the 45 | reverse of their original order. 46 | 47 | Values that are repeated in the original collection are always treated as 48 | separate values in the resulting permutations: 49 | 50 | ```swift 51 | let numbers2 = [20, 10, 10] 52 | for perm in numbers2.permutations() { 53 | print(perm) 54 | } 55 | // [20, 10, 10] 56 | // [20, 10, 10] 57 | // [10, 20, 10] 58 | // [10, 10, 20] 59 | // [10, 20, 10] 60 | // [10, 10, 20] 61 | ``` 62 | 63 | To generate only unique permutations, use the `uniquePermutations(ofCount:)` method: 64 | 65 | ```swift 66 | for perm in numbers2.uniquePermutations() { 67 | print(perm) 68 | } 69 | // [20, 10, 10] 70 | // [10, 20, 10] 71 | // [10, 10, 20] 72 | ``` 73 | 74 | Given a range, the methods return a sequence of all the different permutations of the given sizes of a collection’s elements in increasing order of size. 75 | 76 | ```swift 77 | let numbers = [10, 20, 30] 78 | for perm in numbers.permutations(ofCount: 0...) { 79 | print(perm) 80 | } 81 | // [] 82 | // [10] 83 | // [20] 84 | // [30] 85 | // [10, 20] 86 | // [10, 30] 87 | // [20, 10] 88 | // [20, 30] 89 | // [30, 10] 90 | // [30, 20] 91 | // [10, 20, 30] 92 | // [10, 30, 20] 93 | // [20, 10, 30] 94 | // [20, 30, 10] 95 | // [30, 10, 20] 96 | // [30, 20, 10] 97 | ``` 98 | 99 | ## Detailed Design 100 | 101 | The `permutations(ofCount:)` and `uniquePermutations(ofCount:)` methods are 102 | declared as `Collection` extensions, and return `PermutationsSequence` and 103 | `UniquePermutationsSequence` instances, respectively: 104 | 105 | ```swift 106 | extension Collection { 107 | public func permutations(ofCount k: Int? = nil) -> PermutationsSequence 108 | public func permutations(ofCount kRange: R) -> PermutationsSequence 109 | where R: RangeExpression, R.Bound == Int 110 | } 111 | 112 | extension Collection where Element: Hashable { 113 | public func uniquePermutations(ofCount k: Int? = nil) -> UniquePermutationsSequence 114 | public func uniquePermutations(ofCount kRange: R) -> UniquePermutationsSequence 115 | where R: RangeExpression, R.Bound == Int 116 | } 117 | ``` 118 | 119 | Since both result types need to store an array of the collection’s 120 | indices and mutate the array to generate each permutation, they only 121 | have `Sequence` conformance. Adding `Collection` conformance would require 122 | storing the array in the index type, which would in turn lead to copying the 123 | array at every index advancement. The `PermutationsSequence` and 124 | `UniquePermutationsSequence` types conforms to `LazySequenceProtocol` when their 125 | base type conforms. 126 | 127 | ### Complexity 128 | 129 | Calling `permutations()` is an O(1) operation. Creating the iterator for a 130 | `PermutationsSequence` instance and each call to 131 | `PermutationsSequence.Iterator.next()` is an O(_n_) operation. 132 | 133 | Calling `uniquePermutations()` is an O(_n_) operation, because it preprocesses 134 | the collection to find duplicate elements. Creating the iterator for and each 135 | call to `next()` is also an O(_n_) operation. 136 | 137 | ### Naming 138 | 139 | See the ["Naming" section for `combinations(ofCount:)`](Combinations.md#naming) for detail. 140 | 141 | ### Comparison with other languages 142 | 143 | **C++:** The `` library defines a `next_permutation` function that 144 | advances an array of comparable values through their lexicographic orderings. 145 | This function is very similar to the `uniquePermutations(ofCount:)` method. 146 | 147 | **Rust/Ruby/Python:** Rust, Ruby, and Python all define functions with 148 | essentially the same semantics as the `permutations(ofCount:)` method 149 | described here. 150 | -------------------------------------------------------------------------------- /Sources/Algorithms/Unique.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | /// A sequence wrapper that leaves out duplicate elements of a base sequence. 13 | public struct UniquedSequence { 14 | /// The base collection. 15 | @usableFromInline 16 | internal let base: Base 17 | 18 | /// The projection function. 19 | @usableFromInline 20 | internal let projection: (Base.Element) -> Subject 21 | 22 | @usableFromInline 23 | internal init(base: Base, projection: @escaping (Base.Element) -> Subject) { 24 | self.base = base 25 | self.projection = projection 26 | } 27 | } 28 | 29 | extension UniquedSequence: Sequence { 30 | /// The iterator for a `UniquedSequence` instance. 31 | public struct Iterator: IteratorProtocol { 32 | @usableFromInline 33 | internal var base: Base.Iterator 34 | 35 | @usableFromInline 36 | internal let projection: (Base.Element) -> Subject 37 | 38 | @usableFromInline 39 | internal var seen: Set = [] 40 | 41 | @usableFromInline 42 | internal init( 43 | base: Base.Iterator, 44 | projection: @escaping (Base.Element) -> Subject 45 | ) { 46 | self.base = base 47 | self.projection = projection 48 | } 49 | 50 | @inlinable 51 | public mutating func next() -> Base.Element? { 52 | while let element = base.next() { 53 | if seen.insert(projection(element)).inserted { 54 | return element 55 | } 56 | } 57 | return nil 58 | } 59 | } 60 | 61 | @inlinable 62 | public func makeIterator() -> Iterator { 63 | Iterator(base: base.makeIterator(), projection: projection) 64 | } 65 | } 66 | 67 | extension UniquedSequence: LazySequenceProtocol 68 | where Base: LazySequenceProtocol {} 69 | 70 | //===----------------------------------------------------------------------===// 71 | // uniqued() 72 | //===----------------------------------------------------------------------===// 73 | 74 | extension Sequence where Element: Hashable { 75 | /// Returns a sequence with only the unique elements of this sequence, in the 76 | /// order of the first occurrence of each unique element. 77 | /// 78 | /// let animals = ["dog", "pig", "cat", "ox", "dog", "cat"] 79 | /// let uniqued = animals.uniqued() 80 | /// print(Array(uniqued)) 81 | /// // Prints '["dog", "pig", "cat", "ox"]' 82 | /// 83 | /// - Returns: A sequence with only the unique elements of this sequence. 84 | /// . 85 | /// - Complexity: O(1). 86 | @inlinable 87 | public func uniqued() -> UniquedSequence { 88 | UniquedSequence(base: self, projection: { $0 }) 89 | } 90 | } 91 | 92 | extension Sequence { 93 | /// Returns an array with the unique elements of this sequence (as determined 94 | /// by the given projection), in the order of the first occurrence of each 95 | /// unique element. 96 | /// 97 | /// This example finds the elements of the `animals` array with unique 98 | /// first characters: 99 | /// 100 | /// let animals = ["dog", "pig", "cat", "ox", "cow", "owl"] 101 | /// let uniqued = animals.uniqued(on: { $0.first }) 102 | /// print(uniqued) 103 | /// // Prints '["dog", "pig", "cat", "ox"]' 104 | /// 105 | /// - Parameter projection: A closure that transforms an element into the 106 | /// value to use for uniqueness. If `projection` returns the same value for 107 | /// two different elements, the second element will be excluded from the 108 | /// resulting array. 109 | /// 110 | /// - Returns: An array with only the unique elements of this sequence, as 111 | /// determined by the result of `projection` for each element. 112 | /// 113 | /// - Complexity: O(*n*), where *n* is the length of the sequence. 114 | @inlinable 115 | public func uniqued( 116 | on projection: (Element) throws -> Subject 117 | ) rethrows -> [Element] { 118 | var seen: Set = [] 119 | var result: [Element] = [] 120 | for element in self { 121 | if seen.insert(try projection(element)).inserted { 122 | result.append(element) 123 | } 124 | } 125 | return result 126 | } 127 | } 128 | 129 | //===----------------------------------------------------------------------===// 130 | // lazy.uniqued() 131 | //===----------------------------------------------------------------------===// 132 | 133 | extension LazySequenceProtocol { 134 | /// Returns a lazy sequence with the unique elements of this sequence (as 135 | /// determined by the given projection), in the order of the first occurrence 136 | /// of each unique element. 137 | /// 138 | /// - Complexity: O(1). 139 | @inlinable 140 | public func uniqued( 141 | on projection: @escaping (Element) -> Subject 142 | ) -> UniquedSequence { 143 | UniquedSequence(base: self, projection: projection) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/JoinedTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | import XCTest 13 | 14 | @testable import Algorithms 15 | 16 | final class JoinedTests: XCTestCase { 17 | let stringArrays = [ 18 | [], 19 | [""], 20 | ["", ""], 21 | ["foo"], 22 | ["foo", "bar"], 23 | ["", "", "foo", "", "bar", "baz", ""], 24 | ] 25 | 26 | func testJoined() { 27 | let expected = ["", "", "", "foo", "foobar", "foobarbaz"] 28 | 29 | for (strings, expected) in zip(stringArrays, expected) { 30 | // regular sequence 31 | expectEqualSequences(AnySequence(strings).joined(), expected) 32 | 33 | // lazy sequence 34 | expectEqualSequences(AnySequence(strings).lazy.joined(), expected) 35 | 36 | // regular collection 37 | expectEqualSequences(strings.joined(), expected) 38 | 39 | // lazy collection 40 | expectEqualSequences( 41 | strings.lazy.joined() as FlattenCollection, expected) 42 | } 43 | } 44 | 45 | func testJoinedByElement() { 46 | let separator: Character = " " 47 | let expected = ["", "", " ", "foo", "foo bar", " foo bar baz "] 48 | 49 | for (strings, expected) in zip(stringArrays, expected) { 50 | expectEqualSequences( 51 | AnySequence(strings).joined(by: separator), expected) 52 | expectEqualSequences( 53 | AnySequence(strings).lazy.joined(by: separator), expected) 54 | expectEqualSequences(strings.joined(by: separator), expected) 55 | expectEqualSequences(strings.lazy.joined(by: separator), expected) 56 | } 57 | } 58 | 59 | func testJoinedBySequence() { 60 | let separator = ", " 61 | let expected = ["", "", ", ", "foo", "foo, bar", ", , foo, , bar, baz, "] 62 | 63 | for (strings, expected) in zip(stringArrays, expected) { 64 | expectEqualSequences( 65 | AnySequence(strings).joined(by: separator), expected) 66 | expectEqualSequences( 67 | AnySequence(strings).lazy.joined(by: separator), expected) 68 | expectEqualSequences(strings.joined(by: separator), expected) 69 | expectEqualSequences(strings.lazy.joined(by: separator), expected) 70 | } 71 | } 72 | 73 | func testJoinedByElementClosure() { 74 | let separator = { (left: String, right: String) -> Character in 75 | left.isEmpty || right.isEmpty ? " " : "-" 76 | } 77 | 78 | let expected = ["", "", " ", "foo", "foo-bar", " foo bar-baz "] 79 | 80 | for (strings, expected) in zip(stringArrays, expected) { 81 | expectEqualSequences( 82 | AnySequence(strings).joined(by: separator), expected) 83 | expectEqualSequences( 84 | AnySequence(strings).lazy.joined(by: separator), expected) 85 | expectEqualSequences(strings.joined(by: separator), expected) 86 | expectEqualSequences(strings.lazy.joined(by: separator), expected) 87 | } 88 | } 89 | 90 | func testJoinedBySequenceClosure() { 91 | let separator = { (left: String, right: String) in 92 | "(\(left.count), \(right.count))" 93 | } 94 | 95 | let expected = [ 96 | "", 97 | "", 98 | "(0, 0)", 99 | "foo", 100 | "foo(3, 3)bar", 101 | "(0, 0)(0, 3)foo(3, 0)(0, 3)bar(3, 3)baz(3, 0)", 102 | ] 103 | 104 | for (strings, expected) in zip(stringArrays, expected) { 105 | expectEqualSequences( 106 | AnySequence(strings).joined(by: separator), expected) 107 | expectEqualSequences( 108 | AnySequence(strings).lazy.joined(by: separator), expected) 109 | expectEqualSequences(strings.joined(by: separator), expected) 110 | expectEqualSequences(strings.lazy.joined(by: separator), expected) 111 | } 112 | } 113 | 114 | func testJoinedLazy() { 115 | requireLazySequence(AnySequence([[1], [2]]).lazy.joined()) 116 | requireLazySequence(AnySequence([[1], [2]]).lazy.joined(by: 1)) 117 | requireLazySequence( 118 | AnySequence([[1], [2]]).lazy.joined(by: { _, _ in 1 })) 119 | requireLazyCollection([[1], [2]].lazy.joined()) 120 | requireLazyCollection([[1], [2]].lazy.joined(by: 1)) 121 | requireLazyCollection([[1], [2]].lazy.joined(by: { _, _ in 1 })) 122 | } 123 | 124 | func testJoinedIndexTraversals() { 125 | let validator = IndexValidator>() 126 | 127 | // the last test case takes too long to run 128 | for strings in stringArrays.dropLast() { 129 | validator.validate(strings.joined() as FlattenCollection) 130 | } 131 | } 132 | 133 | func testJoinedByIndexTraversals() { 134 | let validator1 = IndexValidator>() 135 | let validator2 = IndexValidator< 136 | JoinedByClosureCollection<[String], String> 137 | >() 138 | 139 | // the last test case takes too long to run 140 | for (strings, separator) in product( 141 | stringArrays.dropLast(), ["", " ", ", "]) 142 | { 143 | validator1.validate(strings.joined(by: separator)) 144 | validator2.validate(strings.lazy.joined(by: { _, _ in separator })) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/StrideTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020 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 | import Algorithms 13 | import XCTest 14 | 15 | final class StridingTests: XCTestCase { 16 | 17 | func testStride() { 18 | let a = 0...10 19 | expectEqualSequences(a.striding(by: 1), (0...10)) 20 | expectEqualSequences(a.striding(by: 2), [0, 2, 4, 6, 8, 10]) 21 | expectEqualSequences(a.striding(by: 3), [0, 3, 6, 9]) 22 | expectEqualSequences(a.striding(by: 4), [0, 4, 8]) 23 | expectEqualSequences(a.striding(by: 5), [0, 5, 10]) 24 | expectEqualSequences(a.striding(by: 10), [0, 10]) 25 | expectEqualSequences(a.striding(by: 11), [0]) 26 | 27 | let s = (0...).prefix(11) 28 | expectEqualSequences(s.striding(by: 1), (0...10)) 29 | expectEqualSequences(s.striding(by: 2), [0, 2, 4, 6, 8, 10]) 30 | expectEqualSequences(s.striding(by: 3), [0, 3, 6, 9]) 31 | expectEqualSequences(s.striding(by: 4), [0, 4, 8]) 32 | expectEqualSequences(s.striding(by: 5), [0, 5, 10]) 33 | expectEqualSequences(s.striding(by: 10), [0, 10]) 34 | expectEqualSequences(s.striding(by: 11), [0]) 35 | 36 | let empty = (0...).prefix(0) 37 | expectEqualSequences(empty.striding(by: 2), []) 38 | } 39 | 40 | func testStrideString() { 41 | let s = "swift" 42 | expectEqualSequences(s.striding(by: 2), ["s", "i", "t"]) 43 | } 44 | 45 | func testStrideReversed() { 46 | let a = [0, 1, 2, 3, 4, 5] 47 | expectEqualSequences(a.striding(by: 3).reversed(), [3, 0]) 48 | expectEqualSequences(a.reversed().striding(by: 2), [5, 3, 1]) 49 | } 50 | 51 | func testStrideIndexes() { 52 | let a = [0, 1, 2, 3, 4, 5].striding(by: 2) 53 | var i = a.startIndex 54 | XCTAssertEqual(a[i], 0) 55 | a.formIndex(after: &i) 56 | XCTAssertEqual(a[i], 2) 57 | a.formIndex(after: &i) 58 | XCTAssertEqual(a[i], 4) 59 | a.formIndex(before: &i) 60 | XCTAssertEqual(a[i], 2) 61 | a.formIndex(before: &i) 62 | XCTAssertEqual(a[i], 0) 63 | // a.formIndex(before: &i) // Precondition failed: Incrementing past start index 64 | // a.index(after: a.endIndex) // Precondition failed: Advancing past end index 65 | } 66 | 67 | func testStrideLast() { 68 | XCTAssertEqual((1...10).striding(by: 2).last, 9) // 1, 3, 5, 7, 9 69 | XCTAssertEqual((1...10).striding(by: 3).last, 10) // 1, 4, 7, 10 70 | XCTAssertEqual((1...10).striding(by: 4).last, 9) // 1, 5, 9 71 | XCTAssertEqual((1...10).striding(by: 5).last, 6) // 1, 6 72 | XCTAssertEqual((1...100).striding(by: 50).last, 51) // 1, 51 73 | XCTAssertEqual((1...5).striding(by: 2).last, 5) // 1, 3, 5 74 | XCTAssertEqual([Int]().striding(by: 2).last, nil) // empty 75 | } 76 | 77 | func testCount() { 78 | let empty = [Int]().striding(by: 2) 79 | XCTAssertEqual(empty.count, 0) 80 | let a = (0...10) 81 | XCTAssertEqual(a.striding(by: 1).count, (0...10).count) 82 | XCTAssertEqual(a.striding(by: 2).count, [0, 2, 4, 6, 8, 10].count) 83 | XCTAssertEqual(a.striding(by: 3).count, [0, 3, 6, 9].count) 84 | XCTAssertEqual(a.striding(by: 4).count, [0, 4, 8].count) 85 | XCTAssertEqual(a.striding(by: 5).count, [0, 5, 10].count) 86 | XCTAssertEqual(a.striding(by: 10).count, [0, 10].count) 87 | XCTAssertEqual(a.striding(by: 11).count, [0].count) 88 | } 89 | 90 | func testIndexTraversals() { 91 | do { 92 | let empty: [Int] = [] 93 | let validator = IndexValidator>() 94 | validator.validate(empty.striding(by: 1)) 95 | validator.validate(empty.striding(by: 2)) 96 | } 97 | 98 | do { 99 | let range = 0...100 100 | let validator = IndexValidator>>() 101 | validator.validate(range.striding(by: 10)) 102 | validator.validate(range.striding(by: 11)) 103 | validator.validate(range.striding(by: 101)) 104 | } 105 | 106 | do { 107 | let array = Array(0...100) 108 | let validator = IndexValidator>() 109 | validator.validate(array.striding(by: 10)) 110 | validator.validate(array.striding(by: 11)) 111 | validator.validate(array.striding(by: 101)) 112 | } 113 | 114 | do { 115 | let string = "swift rocks" 116 | let validator = IndexValidator>() 117 | validator.validate(string.striding(by: 1)) 118 | validator.validate(string.striding(by: 2)) 119 | validator.validate(string.striding(by: 10)) 120 | } 121 | } 122 | 123 | func testOffsetBy() { 124 | let a = (0...100).striding(by: 22) 125 | let b = [0, 22, 44, 66, 88] 126 | for i in 0..( 31 | _ initial: Result, 32 | _ transform: @escaping (Result, Element) -> Result 33 | ) -> ExclusiveReductionsSequence 34 | 35 | public func reductions( 36 | into initial: Result, 37 | _ transform: @escaping (inout Result, Element) -> Void 38 | ) -> ExclusiveReductionsSequence 39 | 40 | public func reductions( 41 | _ transform: @escaping (Element, Element) -> Element 42 | ) -> InclusiveReductionsSequence 43 | } 44 | ``` 45 | 46 | ```swift 47 | extension Sequence { 48 | public func reductions( 49 | _ initial: Result, 50 | _ transform: (Result, Element) throws -> Result 51 | ) rethrows -> [Result] 52 | 53 | public func reductions( 54 | into initial: Result, 55 | _ transform: (inout Result, Element) throws -> Void 56 | ) rethrows -> [Result] 57 | 58 | public func reductions( 59 | _ transform: (Element, Element) throws -> Element 60 | ) rethrows -> [Element] 61 | } 62 | ``` 63 | 64 | ### Complexity 65 | 66 | Calling the lazy methods, those defined on `LazySequenceProtocol`, is O(_1_). 67 | Calling the eager methods, those returning an array, is O(_n_). 68 | 69 | ### Naming 70 | 71 | While the name `scan` is the term of art for this function, it has been 72 | discussed that `reductions` aligns better with the existing `reduce` function 73 | and will aid newcomers that might not know the existing `scan` term. 74 | 75 | Deprecated `scan` methods have been added for people who are familiar with the 76 | term, so they can easily discover the `reductions` methods via compiler 77 | deprecation warnings. 78 | 79 | Below are two quotes from the Swift forum [discussion about SE-0045][SE-0045] 80 | which proposed adding `scan` to the standard library and one from 81 | [issue #25][Issue 25] on the swift-algorithms GitHub project. These provide 82 | the reasoning to use the name `reductions`. 83 | 84 | [Brent Royal-Gordon][Brent_Royal-Gordon]: 85 | > I really like the `reduce`/`reductions` pairing instead of `reduce`/`scan`; 86 | it does a really good job of explaining the relationship between the two 87 | functions. 88 | 89 | [David Rönnqvist][David Rönnqvist]: 90 | > As other have already pointed out, I also feel that `scan` is the least 91 | intuitive name among these and that the `reduce`/`reductions` pairing would do 92 | a good job at explaining the relation between the two. 93 | 94 | [Kyle Macomber][Kyle Macomber]: 95 | > As someone unfamiliar with the prior art, `reductions` strikes me as very 96 | approachable—I feel like I can extrapolate the expected behavior purely from my 97 | familiarity with `reduce`. 98 | 99 | As part of early discussions, it was decided to have two variants, one which 100 | takes an initial value to use for the first element in the returned sequence, 101 | and another which uses the first value of the base sequence as the initial 102 | value. C++ calls these variants exclusive and inclusive respectively and so 103 | these terms carry through as the name for the lazy sequences; 104 | `ExclusiveReductionsSequence` and `InclusiveReductionsSequence`. 105 | 106 | [SE-0045]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382 107 | [Issue 25]: https://github.com/apple/swift-algorithms/issues/25 108 | [Brent_Royal-Gordon]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382/6 109 | [David Rönnqvist]: https://forums.swift.org/t/review-se-0045-add-scan-prefix-while-drop-while-and-iterate-to-the-stdlib/2382/8 110 | [Kyle Macomber]: https://github.com/apple/swift-algorithms/issues/25#issuecomment-709317894 111 | 112 | ### Comparison with other languages 113 | 114 | **C++:** As of C++17, the `` library includes both 115 | [`exclusive_scan`][C++ Exclusive] and [`inclusive_scan`][C++ Inclusive] 116 | functions. 117 | 118 | **[Clojure][Clojure]:** Clojure 1.2 added a `reductions` function. 119 | 120 | **[Haskell][Haskell]:** Haskell includes a `scan` function for its 121 | `Traversable` type, which is akin to Swift's `Sequence`. 122 | 123 | **Python:** Python’s `itertools` includes an `accumulate` method. In version 124 | 3.3, a function parameter was added. Version 3.8 added the optional initial 125 | parameter. 126 | 127 | **[Rust][Rust]:** Rust provides a `scan` function. 128 | 129 | [C++ Exclusive]: https://en.cppreference.com/w/cpp/algorithm/exclusive_scan 130 | [C++ Inclusive]: https://en.cppreference.com/w/cpp/algorithm/inclusive_scan 131 | [Clojure]: http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/reductions 132 | [Haskell]: http://hackage.haskell.org/package/base-4.8.2.0/docs/Prelude.html#v:scanl 133 | [Rust]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.scan 134 | -------------------------------------------------------------------------------- /Sources/Algorithms/Compacted.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms 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 | /// A `Sequence` that iterates over every non-nil element from the original 13 | /// `Sequence`. 14 | public struct CompactedSequence: Sequence 15 | where Base.Element == Element? { 16 | 17 | @usableFromInline 18 | let base: Base 19 | 20 | @inlinable 21 | init(base: Base) { 22 | self.base = base 23 | } 24 | 25 | public struct Iterator: IteratorProtocol { 26 | @usableFromInline 27 | var base: Base.Iterator 28 | 29 | @inlinable 30 | init(base: Base.Iterator) { 31 | self.base = base 32 | } 33 | 34 | @inlinable 35 | public mutating func next() -> Element? { 36 | while let wrapped = base.next() { 37 | guard let some = wrapped else { continue } 38 | return some 39 | } 40 | return nil 41 | } 42 | } 43 | 44 | @inlinable 45 | public func makeIterator() -> Iterator { 46 | Iterator(base: base.makeIterator()) 47 | } 48 | } 49 | 50 | extension Sequence { 51 | /// Returns a new `Sequence` that iterates over every non-nil element from the 52 | /// original `Sequence`. 53 | /// 54 | /// Produces the same result as `c.compactMap { $0 }`. 55 | /// 56 | /// let c = [1, nil, 2, 3, nil] 57 | /// for num in c.compacted() { 58 | /// print(num) 59 | /// } 60 | /// // 1 61 | /// // 2 62 | /// // 3 63 | /// 64 | /// - Returns: A `Sequence` where the element is the unwrapped original 65 | /// element and iterates over every non-nil element from the original 66 | /// `Sequence`. 67 | /// 68 | /// Complexity: O(1) 69 | @inlinable 70 | public func compacted() -> CompactedSequence 71 | where Element == Unwrapped? { 72 | CompactedSequence(base: self) 73 | } 74 | } 75 | 76 | /// A `Collection` that iterates over every non-nil element from the original 77 | /// `Collection`. 78 | public struct CompactedCollection: Collection 79 | where Base.Element == Element? { 80 | 81 | @usableFromInline 82 | let base: Base 83 | 84 | @inlinable 85 | init(base: Base) { 86 | self.base = base 87 | let idx = base.firstIndex(where: { $0 != nil }) ?? base.endIndex 88 | self.startIndex = Index(base: idx) 89 | } 90 | 91 | public struct Index { 92 | @usableFromInline 93 | let base: Base.Index 94 | 95 | @inlinable 96 | init(base: Base.Index) { 97 | self.base = base 98 | } 99 | } 100 | 101 | public var startIndex: Index 102 | 103 | @inlinable 104 | public var endIndex: Index { Index(base: base.endIndex) } 105 | 106 | @inlinable 107 | public subscript(position: Index) -> Element { 108 | // swift-format-ignore: NeverForceUnwrap 109 | // All indices are only for non-`nil` elements. 110 | base[position.base]! 111 | } 112 | 113 | @inlinable 114 | public func index(after i: Index) -> Index { 115 | precondition(i != endIndex, "Index out of bounds") 116 | 117 | let baseIdx = base.index(after: i.base) 118 | guard let idx = base[baseIdx...].firstIndex(where: { $0 != nil }) 119 | else { return endIndex } 120 | return Index(base: idx) 121 | } 122 | } 123 | 124 | extension CompactedCollection: BidirectionalCollection 125 | where Base: BidirectionalCollection { 126 | 127 | @inlinable 128 | public func index(before i: Index) -> Index { 129 | precondition(i != startIndex, "Index out of bounds") 130 | 131 | guard 132 | let idx = 133 | base[startIndex.base.. Bool { 146 | lhs.base < rhs.base 147 | } 148 | } 149 | 150 | extension CompactedCollection.Index: Hashable 151 | where Base.Index: Hashable {} 152 | 153 | extension Collection { 154 | /// Returns a new `Collection` that iterates over every non-nil element from 155 | /// the original `Collection`. 156 | /// 157 | /// Produces the same result as `c.compactMap { $0 }`. 158 | /// 159 | /// let c = [1, nil, 2, 3, nil] 160 | /// for num in c.compacted() { 161 | /// print(num) 162 | /// } 163 | /// // 1 164 | /// // 2 165 | /// // 3 166 | /// 167 | /// - Returns: A `Collection` where the element is the unwrapped original 168 | /// element and iterates over every non-nil element from the original 169 | /// `Collection`. 170 | /// 171 | /// Complexity: O(*n*) where *n* is the number of elements in the 172 | /// original `Collection`. 173 | @inlinable 174 | public func compacted() -> CompactedCollection 175 | where Element == Unwrapped? { 176 | CompactedCollection(base: self) 177 | } 178 | } 179 | 180 | //===----------------------------------------------------------------------===// 181 | // Protocol Conformances 182 | //===----------------------------------------------------------------------===// 183 | 184 | extension CompactedSequence: LazySequenceProtocol 185 | where Base: LazySequenceProtocol {} 186 | 187 | extension CompactedCollection: LazySequenceProtocol, LazyCollectionProtocol 188 | where Base: LazySequenceProtocol {} 189 | -------------------------------------------------------------------------------- /Guides/Chunked.md: -------------------------------------------------------------------------------- 1 | # Chunked 2 | 3 | [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Chunked.swift) | 4 | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ChunkedTests.swift)] 5 | 6 | Break a collection into nonoverlapping subsequences: 7 | 8 | * `chunked(by:)` forms chunks of consecutive elements that pass a binary predicate, 9 | * `chunked(on:)` forms chunks of consecutive elements that project to equal values, 10 | * `chunks(ofCount:)` forms chunks of a given size, and 11 | * `evenlyChunked(in:)` forms a given number of equally-sized chunks. 12 | 13 | `chunked(by:)` uses a binary predicate to test consecutive elements, separating 14 | chunks where the predicate returns `false`. For example, you can chunk a 15 | collection into ascending sequences using this method: 16 | 17 | ```swift 18 | let numbers = [10, 20, 30, 10, 40, 40, 10, 20] 19 | let chunks = numbers.chunked(by: { $0 <= $1 }) 20 | // [[10, 20, 30], [10, 40, 40], [10, 20]] 21 | ``` 22 | 23 | The `chunked(on:)` method, by contrast, takes a projection of each element and 24 | separates chunks where the projection of two consecutive elements is not equal. 25 | The result includes both the projected value and the subsequence that groups 26 | elements with that projected value: 27 | 28 | ```swift 29 | let names = ["David", "Kyle", "Karoy", "Nate"] 30 | let chunks = names.chunked(on: \.first!) 31 | // [("D", ["David"]), ("K", ["Kyle", "Karoy"]), ("N", ["Nate"])] 32 | ``` 33 | 34 | The `chunks(ofCount:)` method takes a `count` parameter (required to be > 0) and 35 | separates the collection into chunks of this given count. If the length of the 36 | collection is a multiple of the `count` parameter, all chunks will have the 37 | a count equal to the parameter. Otherwise, the last chunk will contain the remaining elements. 38 | 39 | ```swift 40 | let names = ["David", "Kyle", "Karoy", "Nate"] 41 | let evenly = names.chunks(ofCount: 2) 42 | // equivalent to [["David", "Kyle"], ["Karoy", "Nate"]] 43 | 44 | let remaining = names.chunks(ofCount: 3) 45 | // equivalent to [["David", "Kyle", "Karoy"], ["Nate"]] 46 | ``` 47 | 48 | The `chunks(ofCount:)` method was previously [proposed](proposal) for inclusion 49 | in the standard library. 50 | 51 | The `evenlyChunked(in:)` method takes a `count` parameter and divides the 52 | collection into `count` number of equally-sized chunks. If the length of the 53 | collection is not a multiple of the `count` parameter, the chunks at the start 54 | will be longer than the chunks at the end. 55 | 56 | ```swift 57 | let evenChunks = (0..<15).evenlyChunked(in: 3) 58 | // equivalent to [0..<5, 5..<10, 10..<15] 59 | 60 | let nearlyEvenChunks = (0..<15).evenlyChunked(in: 4) 61 | // equivalent to [0..<4, 4..<8, 8..<12, 12..<15] 62 | ``` 63 | 64 | When "chunking" a collection, the entire collection is included in the result, 65 | unlike the `split` family of methods, where separators are dropped. 66 | Joining the result of a chunking method call results in a collection equivalent 67 | to the original. 68 | 69 | ```swift 70 | c.elementsEqual(c.chunked(...).joined()) 71 | // true 72 | ``` 73 | 74 | [proposal]: https://github.com/apple/swift-evolution/pull/935 75 | 76 | ## Detailed Design 77 | 78 | The four methods are added to `Collection`, with matching versions of 79 | `chunked(by:)` and `chunked(on:)` that return a lazy wrapper added to 80 | `LazyCollectionProtocol`. 81 | 82 | ```swift 83 | extension Collection { 84 | public func chunked( 85 | by belongInSameGroup: (Element, Element) -> Bool 86 | ) -> [SubSequence] 87 | 88 | public func chunked( 89 | on projection: (Element) -> Subject 90 | ) -> [SubSequence] 91 | 92 | public func chunks(ofCount count: Int) -> ChunkedByCount 93 | 94 | public func evenlyChunked(in count: Int) -> EvenlyChunkedCollection 95 | } 96 | 97 | extension LazyCollectionProtocol { 98 | public func chunked( 99 | by belongInSameGroup: @escaping (Element, Element) -> Bool 100 | ) -> ChunkedByCollection 101 | 102 | public func chunked( 103 | on projection: @escaping (Element) -> Subject 104 | ) -> ChunkedOnCollection 105 | } 106 | ``` 107 | 108 | Each of the "chunked" collection types are bidirectional when the wrapped 109 | collection is bidirectional. `ChunksOfCountCollection` and 110 | `EvenlyChunkedCollection` also conform to `RandomAccessCollection` and 111 | `LazySequenceProtocol` when their base collections conform. 112 | 113 | ### Complexity 114 | 115 | The eager methods are O(_n_) where _n_ is the number of elements in the 116 | collection. The lazy methods are O(_n_) because the start index is pre-computed. 117 | 118 | ### Naming 119 | 120 | The operation performed by these methods is similar to other ways of breaking a 121 | collection up into subsequences. In particular, the predicate-based 122 | `split(where:)` method looks similar to `chunked(on:)`. You can draw a 123 | distinction between these different operations based on the resulting 124 | subsequences: 125 | 126 | - `split`: *In the standard library.* Breaks a collection into subsequences, 127 | removing any elements that are considered "separators". The original collection 128 | cannot be recovered from the result of splitting. 129 | - `chunked`: *In this package.* Breaks a collection into subsequences, 130 | preserving each element in its initial ordering. Joining the resulting 131 | subsequences re-forms the original collection. 132 | - `sliced`: *Not included in this package or the stdlib.* Breaks a collection 133 | into potentially overlapping subsequences. 134 | 135 | ### Comparison with other languages 136 | 137 | **Ruby:** Ruby’s `Enumerable` class defines `chunk_while` and `chunk`, which map 138 | to the proposed `chunked(by:)` and `chunked(on:)` methods. 139 | 140 | **Rust:** Rust defines a variety of size-based `chunks` methods, of which the 141 | standard version corresponds to the `chunks(ofCount:)` method defined here. 142 | -------------------------------------------------------------------------------- /Tests/SwiftAlgorithmsTests/CombinationsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Algorithms open source project 4 | // 5 | // Copyright (c) 2020-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 | import Algorithms 13 | import XCTest 14 | 15 | final class CombinationsTests: XCTestCase { 16 | func testCount() { 17 | let c = "ABCD" 18 | 19 | /// XCTAsserts that `x`'s `count` and `underestimatedCount` are both `l` at 20 | /// the given `file` and `line`. 21 | func check( 22 | _ x: CombinationsSequence, countsAre l: Int, 23 | file: StaticString, line: UInt 24 | ) { 25 | XCTAssertEqual(x.count, l, "unexpected count", file: file, line: line) 26 | XCTAssertEqual( 27 | x.underestimatedCount, l, "unexpected underestimatedCount", 28 | file: file, line: line) 29 | } 30 | 31 | /// XCTAsserts that the `count` and `underestimatedCount` of 32 | /// `c.combinations(ofCount: l)` are both `n` at the given `file` and 33 | /// `line`. 34 | func check( 35 | cHas n: Int, 36 | combinationsOfLength l: Int, 37 | file: StaticString = #filePath, line: UInt = #line 38 | ) { 39 | check(c.combinations(ofCount: l), countsAre: n, file: file, line: line) 40 | } 41 | 42 | /// XCTAsserts that the `count` and `underestimatedCount` of 43 | /// `c.combinations(ofCount: l)` are both `n` at the given `file` and 44 | /// `line`. 45 | func check( 46 | cHas n: Int, 47 | combinationsOfLengths l: R, 48 | file: StaticString = #filePath, line: UInt = #line 49 | ) where R.Bound == Int { 50 | check(c.combinations(ofCount: l), countsAre: n, file: file, line: line) 51 | } 52 | 53 | check(cHas: 1, combinationsOfLength: 0) 54 | check(cHas: 4, combinationsOfLength: 1) 55 | check(cHas: 6, combinationsOfLength: 2) 56 | check(cHas: 1, combinationsOfLength: 4) 57 | 58 | check(cHas: 1, combinationsOfLengths: 0...0) 59 | check(cHas: 4, combinationsOfLengths: 1...1) 60 | check(cHas: 10, combinationsOfLengths: 1...2) 61 | check(cHas: 14, combinationsOfLengths: 1...3) 62 | check(cHas: 11, combinationsOfLengths: 2...4) 63 | 64 | // `k` greater than element count results in same number of combinations 65 | check(cHas: 5, combinationsOfLengths: 3...10) 66 | 67 | // `k` greater than element count results in same number of combinations 68 | check(cHas: 1, combinationsOfLengths: 4...10) 69 | 70 | // `k` entirely greater than element count results in no combinations 71 | check(cHas: 0, combinationsOfLengths: 5...10) 72 | 73 | check(cHas: 16, combinationsOfLengths: 0...) 74 | check(cHas: 15, combinationsOfLengths: ...3) 75 | } 76 | 77 | func testCombinations() { 78 | let c = "ABCD" 79 | 80 | let c1 = c.combinations(ofCount: 1) 81 | XCTAssertEqual(["A", "B", "C", "D"], c1.map { String($0) }) 82 | 83 | let c2 = c.combinations(ofCount: 2) 84 | XCTAssertEqual(["AB", "AC", "AD", "BC", "BD", "CD"], c2.map { String($0) }) 85 | 86 | let c3 = c.combinations(ofCount: 3) 87 | XCTAssertEqual(["ABC", "ABD", "ACD", "BCD"], c3.map { String($0) }) 88 | 89 | let c4 = c.combinations(ofCount: 4) 90 | XCTAssertEqual(["ABCD"], c4.map { String($0) }) 91 | 92 | let c5 = c.combinations(ofCount: 2...4) 93 | XCTAssertEqual( 94 | ["AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", "ACD", "BCD", "ABCD"], 95 | c5.map { String($0) }) 96 | 97 | let c6 = c.combinations(ofCount: 0...4) 98 | XCTAssertEqual( 99 | [ 100 | "", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", 101 | "ABD", "ACD", "BCD", "ABCD", 102 | ], c6.map { String($0) }) 103 | 104 | let c7 = c.combinations(ofCount: 0...) 105 | XCTAssertEqual( 106 | [ 107 | "", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", 108 | "ABD", "ACD", "BCD", "ABCD", 109 | ], c7.map { String($0) }) 110 | 111 | let c8 = c.combinations(ofCount: ...4) 112 | XCTAssertEqual( 113 | [ 114 | "", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", 115 | "ABD", "ACD", "BCD", "ABCD", 116 | ], c8.map { String($0) }) 117 | 118 | let c9 = c.combinations(ofCount: ...3) 119 | XCTAssertEqual( 120 | [ 121 | "", "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", 122 | "ABD", "ACD", "BCD", 123 | ], c9.map { String($0) }) 124 | 125 | let c10 = c.combinations(ofCount: 1...) 126 | XCTAssertEqual( 127 | [ 128 | "A", "B", "C", "D", "AB", "AC", "AD", "BC", "BD", "CD", "ABC", "ABD", 129 | "ACD", "BCD", "ABCD", 130 | ], c10.map { String($0) }) 131 | } 132 | 133 | func testEmpty() { 134 | // `k == 0` results in one zero-length combination 135 | expectEqualSequences([[]], "".combinations(ofCount: 0)) 136 | expectEqualSequences([[]], "".combinations(ofCount: 0...0)) 137 | expectEqualSequences([[]], "ABCD".combinations(ofCount: 0)) 138 | expectEqualSequences([[]], "ABCD".combinations(ofCount: 0...0)) 139 | 140 | // `k` greater than element count results in zero combinations 141 | expectEqualSequences([], "".combinations(ofCount: 5)) 142 | expectEqualSequences([], "".combinations(ofCount: 5...10)) 143 | expectEqualSequences([], "ABCD".combinations(ofCount: 5)) 144 | expectEqualSequences([], "ABCD".combinations(ofCount: 5...10)) 145 | } 146 | 147 | func testCombinationsLazy() { 148 | requireLazySequence("ABC".lazy.combinations(ofCount: 1)) 149 | requireLazySequence("ABC".lazy.combinations(ofCount: 1...3)) 150 | requireLazySequence("ABC".lazy.combinations(ofCount: 1...)) 151 | requireLazySequence("ABC".lazy.combinations(ofCount: ...3)) 152 | requireLazySequence("ABC".lazy.combinations(ofCount: 0...)) 153 | } 154 | } 155 | --------------------------------------------------------------------------------