diff --git a/CHANGELOG.md b/CHANGELOG.md index b070cf5a..614e24db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,13 @@ This project follows semantic versioning. ## [Unreleased] -*No new changes.* +### Additions + + One new addition to the list of algorithms: + + - `recursiveMap(option:_:)` Produces a sequence containing the original sequence and + the recursive mapped sequence. The order of ouput elements affects by the traversal + option. ([#185]) --- @@ -308,6 +314,7 @@ This changelog's format is based on [Keep a Changelog](https://keepachangelog.co [#130]: https://github.com/apple/swift-algorithms/pull/130 [#138]: https://github.com/apple/swift-algorithms/pull/138 [#162]: https://github.com/apple/swift-algorithms/pull/162 +[#185]: https://github.com/apple/swift-algorithms/pull/185 diff --git a/Guides/RecursiveMap.md b/Guides/RecursiveMap.md new file mode 100644 index 00000000..ef9d5df2 --- /dev/null +++ b/Guides/RecursiveMap.md @@ -0,0 +1,96 @@ +# RecursiveMap + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/RecursiveMap.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/RecursiveMapTests.swift)] + +## Proposed Solution + +Produces a sequence containing the original sequence and the recursive mapped sequence. The order of ouput elements affects by the traversal option. + +```swift +struct Node { + var id: Int + var children: [Node] = [] +} +let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), +] +for node in tree.recursiveMap({ $0.children }) { + print(node.id) +} +// 1 +// 2 +// 3 +// 4 +// 5 +// 6 +``` + +### Traversal Option + +This function comes with two different traversal methods. This option affects the element order of the output sequence. + +- `depthFirst`: The algorithm will go down first and produce the resulting path. The algorithm starts with original + sequence and calling the supplied closure first. This is default option. + + With the structure of tree: + ```swift + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + ``` + + The resulting sequence will be 1 -> 2 -> 3 -> 4 -> 5 -> 6 + + The sequence using a buffer keep tracking the path of nodes. It should not using this option for searching the indefinite deep of tree. + +- `breadthFirst`: The algorithm will go through the previous sequence first and chaining all the occurring sequences. + + With the structure of tree: + ```swift + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + ``` + + The resulting sequence will be 1 -> 6 -> 2 -> 3 -> 5 -> 4 + + The sequence using a buffer storing occuring nodes of sequences. It should not using this option for searching the indefinite length of occuring sequences. + +## Detailed Design + +The `recursiveMap(option:_:)` method is declared as `Sequence` extensions, and return `RecursiveMapSequence` instance: + +```swift +extension Sequence { + public func recursiveMap( + option: RecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @escaping (Element) -> S + ) -> RecursiveMapSequence +} +``` + +### Complexity + +Calling this method is O(_1_). diff --git a/README.md b/README.md index 270055fb..1c981376 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`indexed()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md): Iterate over tuples of a collection's indices and elements. - [`interspersed(with:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Intersperse.md): Place a value between every two elements of a sequence. - [`partitioningIndex(where:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Partition.md): Returns the starting index of the partition of a collection that matches a predicate. +- [`recursiveMap(option:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/RecursiveMap.md): Produces a sequence containing the original sequence and the recursive mapped sequence. The order of ouput elements affects by the traversal option. - [`reductions(_:)`, `reductions(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md): Returns all the intermediate states of reducing the elements of a sequence or collection. - [`split(maxSplits:omittingEmptySubsequences:whereSeparator)`, `split(separator:maxSplits:omittingEmptySubsequences)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Split.md): Lazy versions of the Standard Library's eager operations that split sequences and collections into subsequences separated by the specified separator element. - [`windows(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Windows.md): Breaks a collection into overlapping subsequences where elements are slices from the original collection. diff --git a/Sources/Algorithms/RecursiveMap.swift b/Sources/Algorithms/RecursiveMap.swift new file mode 100644 index 00000000..6082a238 --- /dev/null +++ b/Sources/Algorithms/RecursiveMap.swift @@ -0,0 +1,187 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Returns a sequence containing the original sequence and the recursive mapped sequence. + /// The order of ouput elements affects by the traversal option. + /// + /// ``` + /// struct Node { + /// var id: Int + /// var children: [Node] = [] + /// } + /// let tree = [ + /// Node(id: 1, children: [ + /// Node(id: 2), + /// Node(id: 3, children: [ + /// Node(id: 4), + /// ]), + /// Node(id: 5), + /// ]), + /// Node(id: 6), + /// ] + /// for node in tree.recursiveMap({ $0.children }) { + /// print(node.id) + /// } + /// // 1 + /// // 2 + /// // 3 + /// // 4 + /// // 5 + /// // 6 + /// ``` + /// + /// - Parameters: + /// - option: Traversal option. This option affects the element order of the output sequence. default depth-first. + /// - transform: A closure that map the element to new sequence. + /// - Returns: A sequence of the original sequence followed by recursive mapped sequence. + /// + /// - Complexity: O(1) + @inlinable + public func recursiveMap( + option: RecursiveMapSequence.TraversalOption = .depthFirst, + _ transform: @escaping (Element) -> S + ) -> RecursiveMapSequence { + return RecursiveMapSequence(self, option, transform) + } +} + +/// A sequence containing the original sequence and the recursive mapped sequence. +/// The order of ouput elements affects by the traversal option. +public struct RecursiveMapSequence: Sequence where Base.Element == Transformed.Element { + + @usableFromInline + let base: Base + + @usableFromInline + let option: TraversalOption + + @usableFromInline + let transform: (Base.Element) -> Transformed + + @inlinable + init( + _ base: Base, + _ option: TraversalOption, + _ transform: @escaping (Base.Element) -> Transformed + ) { + self.base = base + self.option = option + self.transform = transform + } + + @inlinable + public func makeIterator() -> Iterator { + return Iterator(base, option, transform) + } +} + +extension RecursiveMapSequence { + + /// Traversal option. This option affects the element order of the output sequence. + public enum TraversalOption { + + /// The algorithm will go down first and produce the resulting path. + case depthFirst + + /// The algorithm will go through the previous sequence first and chaining all the occurring sequences. + case breadthFirst + + } + + public struct Iterator: IteratorProtocol { + + @usableFromInline + var base: Base.Iterator? + + @usableFromInline + let option: TraversalOption + + @usableFromInline + var mapped: ArraySlice = [] + + @usableFromInline + var mapped_iterator: Transformed.Iterator? + + @usableFromInline + let transform: (Base.Element) -> Transformed + + @inlinable + init( + _ base: Base, + _ option: TraversalOption, + _ transform: @escaping (Base.Element) -> Transformed + ) { + self.base = base.makeIterator() + self.option = option + self.transform = transform + } + + @inlinable + public mutating func next() -> Base.Element? { + + switch option { + + case .depthFirst: + + while self.mapped_iterator != nil { + + if let element = self.mapped_iterator!.next() { + mapped.append(self.mapped_iterator!) + self.mapped_iterator = transform(element).makeIterator() + return element + } + + self.mapped_iterator = mapped.popLast() + } + + if self.base != nil { + + if let element = self.base!.next() { + self.mapped_iterator = transform(element).makeIterator() + return element + } + + self.base = nil + } + + return nil + + case .breadthFirst: + + if self.base != nil { + + if let element = self.base!.next() { + mapped.append(transform(element).makeIterator()) + return element + } + + self.base = nil + self.mapped_iterator = mapped.popFirst() + } + + while self.mapped_iterator != nil { + + if let element = self.mapped_iterator!.next() { + mapped.append(transform(element).makeIterator()) + return element + } + + self.mapped_iterator = mapped.popFirst() + } + + return nil + } + } + } +} + +extension RecursiveMapSequence: LazySequenceProtocol where Base: LazySequenceProtocol { } diff --git a/Tests/SwiftAlgorithmsTests/RecursiveMapTests.swift b/Tests/SwiftAlgorithmsTests/RecursiveMapTests.swift new file mode 100644 index 00000000..853ea699 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/RecursiveMapTests.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import Algorithms + +final class RecursiveMapTests: XCTestCase { + + func testRecursiveMap() { + + struct Dir: Hashable { + + var id: UUID = UUID() + + var parent: UUID? + + var name: String + + } + + struct Path: Hashable { + + var id: UUID + + var path: String + + } + + var list: [Dir] = [] + list.append(Dir(name: "root")) + list.append(Dir(parent: list[0].id, name: "images")) + list.append(Dir(parent: list[0].id, name: "Users")) + list.append(Dir(parent: list[2].id, name: "Susan")) + list.append(Dir(parent: list[3].id, name: "Desktop")) + list.append(Dir(parent: list[1].id, name: "test.jpg")) + + let answer = [ + Path(id: list[0].id, path: "/root"), + Path(id: list[1].id, path: "/root/images"), + Path(id: list[2].id, path: "/root/Users"), + Path(id: list[5].id, path: "/root/images/test.jpg"), + Path(id: list[3].id, path: "/root/Users/Susan"), + Path(id: list[4].id, path: "/root/Users/Susan/Desktop"), + ] + + let result = list.lazy.compactMap { $0.parent == nil ? Path(id: $0.id, path: "/\($0.name)") : nil } + .recursiveMap(option: .breadthFirst) { parent in list.lazy.compactMap { $0.parent == parent.id ? Path(id: $0.id, path: "\(parent.path)/\($0.name)") : nil } } + + XCTAssertEqualSequences(result, answer) + } + + func testRecursiveMap2() { + + struct Node { + + var id: Int + + var children: [Node] = [] + } + + let tree = [ + Node(id: 1, children: [ + Node(id: 2), + Node(id: 3, children: [ + Node(id: 4), + ]), + Node(id: 5), + ]), + Node(id: 6), + ] + + let nodes = tree.recursiveMap { $0.children } // default depthFirst option + + XCTAssertEqualSequences(nodes.map { $0.id }, 1...6) + } + + func testRecursiveMap3() { + + struct Node { + + var id: Int + + var children: [Node] = [] + } + + let tree = [ + Node(id: 1, children: [ + Node(id: 3), + Node(id: 4, children: [ + Node(id: 6), + ]), + Node(id: 5), + ]), + Node(id: 2), + ] + + let nodes = tree.recursiveMap(option: .breadthFirst) { $0.children } + + XCTAssertEqualSequences(nodes.map { $0.id }, 1...6) + } + +}