├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Nodes │ └── Nodes.swift └── Tests ├── LinuxMain.swift └── NodesTests ├── NodesTests.swift ├── SimpleNode.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | # 51 | # Add this line if you want to avoid checking in source code from the Xcode workspace 52 | # *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # Accio dependency management 62 | Dependencies/ 63 | .accio/ 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | # Code Injection 78 | # 79 | # After new code Injection tools there's a generated folder /iOSInjectionProject 80 | # https://github.com/johnno1962/injectionforxcode 81 | 82 | iOSInjectionProject/ 83 | .DS_Store 84 | .swiftpm 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Devran "Cosmo" Uenal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Nodes", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Nodes", 12 | targets: ["Nodes"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "Nodes", 23 | dependencies: []), 24 | .testTarget( 25 | name: "NodesTests", 26 | dependencies: ["Nodes"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nodes 2 | 3 | Nodes is a class protocol for tree data structures. 4 | A `class` which conforms the `Node`-Protocol, will gain useful properties to easily navigate in the tree. 5 | 6 | ## Usage 7 | 8 | Create a new `class` and conform it to the `Node`-Protocol: 9 | 10 | ```swift 11 | final class SimpleNode: Node { 12 | typealias Value = String 13 | var value: Value 14 | weak var parent: SimpleNode? 15 | var children: [SimpleNode] 16 | 17 | init(value: Value) { 18 | self.value = value 19 | self.children = [] 20 | } 21 | } 22 | 23 | extension SimpleNode: Equatable, CustomStringConvertible { 24 | static func == (lhs: SimpleNode, rhs: SimpleNode) -> Bool { 25 | return lhs.value == rhs.value && lhs.parent == rhs.parent 26 | } 27 | 28 | public var description: String { 29 | return "\(value)" 30 | } 31 | } 32 | ``` 33 | 34 | Create a root node: 35 | 36 | ```swift 37 | let root = SimpleNode(value: "Hand") 38 | ``` 39 | 40 | Add children with `addChild(node: Node)`: 41 | 42 | ```swift 43 | root.addChild(node: SimpleNode(value: "Thumb")) 44 | root.addChild(node: SimpleNode(value: "Index finger")) 45 | root.addChild(node: SimpleNode(value: "Middle finger")) 46 | root.addChild(node: SimpleNode(value: "Ring finger")) 47 | root.addChild(node: SimpleNode(value: "Little finger")) 48 | ``` 49 | 50 | Print tree to console: 51 | 52 | ```swift 53 | print(root.lineBasedDescription) 54 | ``` 55 | 56 | Result: 57 | 58 | ``` 59 | Hand 60 | ├── Thumb 61 | ├── Index finger 62 | ├── Middle finger 63 | ├── Ring finger 64 | └── Little finger 65 | ``` 66 | 67 | ## Features 68 | 69 | ### Ancestors 70 | 71 | ```swift 72 | /// Returns all parent nodes. 73 | var ancestors: [Node] 74 | ``` 75 | 76 | ```swift 77 | /// Returns all parent nodes, including the current node. 78 | var ancestorsIncludingSelf: [Node] 79 | ``` 80 | 81 | ```swift 82 | /// A Boolean value indicating whether the current node is the top node. 83 | var isRoot: Bool 84 | ``` 85 | 86 | ```swift 87 | /// Returns the top node. 88 | var root: Node 89 | ``` 90 | 91 | ### Descendants 92 | 93 | ```swift 94 | /// Adds a sub-node. 95 | func addChild(node: Node) 96 | ``` 97 | 98 | ```swift 99 | /// Returns the number of children. 100 | var degree: Int 101 | ``` 102 | 103 | ```swift 104 | /// Returns all descendants, traversing the entire tree. 105 | var descendants: [Node] 106 | ``` 107 | 108 | ### Leaves 109 | 110 | ```swift 111 | /// A Boolean value indicating whether the node is without children. 112 | var isLeaf: Bool 113 | ``` 114 | 115 | ```swift 116 | /// Returns all nodes with no children. 117 | var leaves: [Node] 118 | ``` 119 | 120 | ```swift 121 | /// Returns the number of leaves. 122 | var breadth: Int 123 | ``` 124 | 125 | ### Branches 126 | 127 | ```swift 128 | /// A Boolean value indicating whether the node has children. 129 | var isBranch: Bool 130 | ``` 131 | 132 | ```swift 133 | /// Returns all nodes with at least one child. 134 | var branches: [Node] 135 | ``` 136 | 137 | ### Siblings 138 | 139 | ```swift 140 | /// Returns all other nodes with the same parent. 141 | var siblings: [Node] 142 | ``` 143 | 144 | ```swift 145 | /// Returns all nodes (including the current node) with the same parent. 146 | var siblingsIncludingSelf: [Node] 147 | ``` 148 | 149 | ### Position 150 | 151 | ```swift 152 | /// Returns the distance between a node and the root. 153 | var depth: Int 154 | ``` 155 | 156 | ```swift 157 | /// The number of edges between the current node and the root. 158 | var level: Int 159 | ``` 160 | 161 | ### Textual representation 162 | 163 | ```swift 164 | var lineBasedDescription: String 165 | ``` 166 | 167 | 168 | ## Example 169 | 170 | ```swift 171 | let root = SimpleNode(value: "Apple") 172 | 173 | let desktops = SimpleNode(value: "Desktops") 174 | root.addChild(node: desktops) 175 | 176 | let macPro = SimpleNode(value: "Mac Pro") 177 | desktops.addChild(node: macPro) 178 | 179 | let macMini = SimpleNode(value: "Mac Mini") 180 | desktops.addChild(node: macMini) 181 | 182 | let iMac = SimpleNode(value: "iMac") 183 | desktops.addChild(node: iMac) 184 | 185 | let notebooks = SimpleNode(value: "Notebooks") 186 | root.addChild(node: notebooks) 187 | 188 | let macBookPro = SimpleNode(value: "MacBook Pro") 189 | notebooks.addChild(node: macBookPro) 190 | 191 | let devices = SimpleNode(value: "Devices") 192 | root.addChild(node: devices) 193 | 194 | let handhelds = SimpleNode(value: "Handhelds") 195 | devices.addChild(node: handhelds) 196 | 197 | let ipod = SimpleNode(value: "iPod") 198 | handhelds.addChild(node: ipod) 199 | 200 | let iphone = SimpleNode(value: "iPhone") 201 | handhelds.addChild(node: iphone) 202 | 203 | let newton = SimpleNode(value: "Newton") 204 | handhelds.addChild(node: newton) 205 | 206 | let setTopBoxes = SimpleNode(value: "Set-top boxes") 207 | devices.addChild(node: setTopBoxes) 208 | 209 | let appleTV = SimpleNode(value: "Apple TV") 210 | setTopBoxes.addChild(node: appleTV) 211 | 212 | 213 | print(root.lineBasedDescription) 214 | ``` 215 | 216 | Output: 217 | 218 | ``` 219 | Apple 220 | ├── Desktops 221 | │ ├── Mac Pro 222 | │ ├── Mac Mini 223 | │ └── iMac 224 | ├── Notebooks 225 | │ └── MacBook Pro 226 | └── Devices 227 | ├── Handhelds 228 | │ ├── iPod 229 | │ ├── iPhone 230 | │ └── Newton 231 | └── Set-top boxes 232 | └── Apple TV 233 | ``` 234 | 235 | 236 | ## Other Projects 237 | 238 | * [BinaryKit](https://github.com/Cosmo/BinaryKit) — BinaryKit helps you to break down binary data into bits and bytes and easily access specific parts. 239 | * [Clippy](https://github.com/Cosmo/Clippy) — Clippy from Microsoft Office is back and runs on macOS! Written in Swift. 240 | * [GrammaticalNumber](https://github.com/Cosmo/GrammaticalNumber) — Turns singular words to the plural and vice-versa in Swift. 241 | * [HackMan](https://github.com/Cosmo/HackMan) — Stop writing boilerplate code yourself. Let hackman do it for you via the command line. 242 | * [ISO8859](https://github.com/Cosmo/ISO8859) — Convert ISO8859 1-16 Encoded Text to String in Swift. Supports iOS, tvOS, watchOS and macOS. 243 | * [SpriteMap](https://github.com/Cosmo/SpriteMap) — SpriteMap helps you to extract sprites out of a sprite map. Written in Swift. 244 | * [StringCase](https://github.com/Cosmo/StringCase) — Converts String to lowerCamelCase, UpperCamelCase and snake_case. Tested and written in Swift. 245 | * [TinyConsole](https://github.com/Cosmo/TinyConsole) — TinyConsole is a micro-console that can help you log and display information inside an iOS application, where having a connection to a development computer is not possible. 246 | 247 | ## License 248 | 249 | Nodes is released under the [MIT License](http://www.opensource.org/licenses/MIT). 250 | -------------------------------------------------------------------------------- /Sources/Nodes/Nodes.swift: -------------------------------------------------------------------------------- 1 | public protocol Node: class, Equatable { 2 | associatedtype Value 3 | var value: Value { get set } 4 | var parent: Self? { get set } 5 | 6 | /// Holds an array of sub-nodes 7 | var children: [Self] { get set } 8 | 9 | init(value: Value) 10 | } 11 | 12 | extension Node { 13 | // MARK: - Ancestors 14 | 15 | /// Returns all parent nodes. 16 | public var ancestors: [Self] { 17 | var nodes = [Self]() 18 | if let parent = parent { 19 | nodes.append(parent) 20 | nodes.append(contentsOf: parent.ancestors) 21 | } 22 | return nodes 23 | } 24 | 25 | /// Returns all parent nodes, including the current node. 26 | public var ancestorsIncludingSelf: [Self] { 27 | return [self] + ancestors 28 | } 29 | 30 | /// A Boolean value indicating whether the current node is the top node. 31 | public var isRoot: Bool { 32 | return parent == nil 33 | } 34 | 35 | /// Returns the top node. 36 | public var root: Self { 37 | return parent?.root ?? self 38 | } 39 | 40 | // MARK: - Descendants 41 | 42 | /// Adds a sub-node. 43 | public func addChild(node: Self) { 44 | children.append(node) 45 | node.parent = self 46 | } 47 | 48 | /// Returns the number of children. 49 | public var degree: Int { 50 | return children.count 51 | } 52 | 53 | /// Returns all descendants, traversing the entire tree. 54 | public var descendants: [Self] { 55 | var nodes = [Self]() 56 | if isBranch { 57 | nodes.append(contentsOf: children) 58 | for child in children { 59 | nodes.append(contentsOf: child.descendants) 60 | } 61 | } 62 | return nodes 63 | } 64 | 65 | // MARK: - Leaves 66 | 67 | /// A Boolean value indicating whether the node is without children. 68 | public var isLeaf: Bool { 69 | return children.isEmpty 70 | } 71 | 72 | /// Returns all nodes with no children. 73 | public var leaves: [Self] { 74 | return children.filter { $0.isLeaf } 75 | } 76 | 77 | /// Returns the number of leaves. 78 | public var breadth: Int { 79 | return leaves.count 80 | } 81 | 82 | // MARK: - Branches 83 | 84 | /// A Boolean value indicating whether the node has children. 85 | public var isBranch: Bool { 86 | return !children.isEmpty 87 | } 88 | 89 | /// Returns all nodes with at least one child. 90 | public var branches: [Self] { 91 | return children.filter { $0.isBranch } 92 | } 93 | 94 | // MARK: - Siblings 95 | 96 | /// Returns all other nodes with the same parent. 97 | public var siblings: [Self] { 98 | return siblingsIncludingSelf.filter { $0 != self } 99 | } 100 | 101 | /// Returns all nodes (including the current node) with the same parent. 102 | public var siblingsIncludingSelf: [Self] { 103 | return parent?.children ?? [] 104 | } 105 | 106 | // MARK: - Position 107 | 108 | /// Returns the distance between a node and the root. 109 | public var depth: Int { 110 | return ancestors.count 111 | } 112 | 113 | /// The number of edges between the current node and the root. 114 | public var level: Int { 115 | return depth + 1 116 | } 117 | } 118 | 119 | // MARK: - Textual representation 120 | 121 | extension Node { 122 | /// Returns a line based tree representation starting with the current node. 123 | public var lineBasedDescription: String { 124 | return buildLines() 125 | } 126 | 127 | private typealias PrefixStrings = (prefix: String, childrenPrefix: String) 128 | private var linePrefixes: PrefixStrings { return ("├── ", "│ ") } 129 | private var lastLinePrefixes: PrefixStrings { return ("└── ", " ") } 130 | 131 | private func buildLines(_ previousPrefixes: PrefixStrings = ("", "")) -> String { 132 | return children.reduce("\(previousPrefixes.prefix)\(value)\n") { 133 | let currentPrefixStrings = children.last == $1 ? lastLinePrefixes : linePrefixes 134 | let prefixes = (previousPrefixes.childrenPrefix + currentPrefixStrings.prefix, 135 | previousPrefixes.childrenPrefix + currentPrefixStrings.childrenPrefix) 136 | return $0 + $1.buildLines(prefixes) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import NodesTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += NodesTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/NodesTests/NodesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Nodes 3 | 4 | final class NodesTests: XCTestCase { 5 | func testTree() { 6 | let root = SimpleNode(value: "Apple") 7 | 8 | let desktops = SimpleNode(value: "Desktops") 9 | root.addChild(node: desktops) 10 | 11 | let macPro = SimpleNode(value: "Mac Pro") 12 | desktops.addChild(node: macPro) 13 | 14 | let macMini = SimpleNode(value: "Mac Mini") 15 | desktops.addChild(node: macMini) 16 | 17 | let iMac = SimpleNode(value: "iMac") 18 | desktops.addChild(node: iMac) 19 | 20 | let notebooks = SimpleNode(value: "Notebooks") 21 | root.addChild(node: notebooks) 22 | 23 | let macBookPro = SimpleNode(value: "MacBook Pro") 24 | notebooks.addChild(node: macBookPro) 25 | 26 | let devices = SimpleNode(value: "Devices") 27 | root.addChild(node: devices) 28 | 29 | let handhelds = SimpleNode(value: "Handhelds") 30 | devices.addChild(node: handhelds) 31 | 32 | let ipod = SimpleNode(value: "iPod") 33 | handhelds.addChild(node: ipod) 34 | 35 | let iphone = SimpleNode(value: "iPhone") 36 | handhelds.addChild(node: iphone) 37 | 38 | let newton = SimpleNode(value: "Newton") 39 | handhelds.addChild(node: newton) 40 | 41 | let setTopBoxes = SimpleNode(value: "Set-top boxes") 42 | devices.addChild(node: setTopBoxes) 43 | 44 | let appleTV = SimpleNode(value: "Apple TV") 45 | setTopBoxes.addChild(node: appleTV) 46 | 47 | let ipodHiFi = SimpleNode(value: "iPod HiFi") 48 | devices.addChild(node: ipodHiFi) 49 | 50 | XCTAssertEqual(ipod.siblings, [iphone, newton]) 51 | XCTAssertEqual(ipod.siblingsIncludingSelf, [ipod, iphone, newton]) 52 | XCTAssertEqual(root.siblingsIncludingSelf, []) 53 | XCTAssertEqual(appleTV.ancestors, [setTopBoxes, devices, root]) 54 | XCTAssertEqual(iphone.ancestorsIncludingSelf, [iphone, handhelds, devices, root]) 55 | XCTAssertTrue(newton.isLeaf) 56 | XCTAssertEqual(devices.leaves, [ipodHiFi]) 57 | XCTAssertEqual(devices.breadth, 1) 58 | XCTAssertTrue(devices.isBranch) 59 | XCTAssertEqual(devices.branches, [handhelds, setTopBoxes]) 60 | XCTAssertFalse(macBookPro.isBranch) 61 | XCTAssertEqual(root.depth, 0) 62 | XCTAssertEqual(root.level, 1) 63 | XCTAssertEqual(appleTV.depth, 3) 64 | XCTAssertEqual(appleTV.level, 4) 65 | XCTAssertTrue(root.isRoot) 66 | XCTAssertFalse(newton.isRoot) 67 | XCTAssertEqual(newton.root, root) 68 | XCTAssertEqual(root.root, root) 69 | XCTAssertEqual(root.degree, 3) 70 | XCTAssertEqual(root.descendants.sorted { $0.value < $1.value }, [desktops, macPro, macMini, iMac, notebooks, macBookPro, devices, handhelds, ipod, iphone, newton, setTopBoxes, appleTV, ipodHiFi].sorted { $0.value < $1.value }) 71 | XCTAssertEqual(ipodHiFi.description, "iPod HiFi") 72 | 73 | let treeRepresentation = """ 74 | Apple 75 | ├── Desktops 76 | │ ├── Mac Pro 77 | │ ├── Mac Mini 78 | │ └── iMac 79 | ├── Notebooks 80 | │ └── MacBook Pro 81 | └── Devices 82 | ├── Handhelds 83 | │ ├── iPod 84 | │ ├── iPhone 85 | │ └── Newton 86 | ├── Set-top boxes 87 | │ └── Apple TV 88 | └── iPod HiFi 89 | 90 | """ 91 | 92 | XCTAssertEqual(root.lineBasedDescription, treeRepresentation) 93 | } 94 | 95 | static var allTests = [ 96 | ("testTree", testTree), 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /Tests/NodesTests/SimpleNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Devran on 01.11.19. 6 | // 7 | 8 | @testable import Nodes 9 | 10 | final class SimpleNode: Node { 11 | typealias Value = String 12 | var value: Value 13 | weak var parent: SimpleNode? 14 | var children: [SimpleNode] 15 | init(value: Value) { 16 | self.value = value 17 | self.children = [] 18 | } 19 | } 20 | 21 | extension SimpleNode: Equatable { 22 | static func == (lhs: SimpleNode, rhs: SimpleNode) -> Bool { 23 | return lhs.value == rhs.value && lhs.parent == rhs.parent 24 | } 25 | } 26 | 27 | extension SimpleNode: CustomStringConvertible { 28 | public var description: String { 29 | return "\(value)" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/NodesTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(NodesTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------