├── .spi.yml ├── Tests ├── TestGraph.swift ├── Algorithms │ ├── FilterAndMap │ │ ├── ValueFilterTests.swift │ │ ├── MapTests.swift │ │ ├── EdgeFilterTests.swift │ │ └── NodeFilterTests.swift │ ├── ComponentTests.swift │ ├── AncestorCountsTests.swift │ ├── SCCTests.swift │ ├── TransitiveReductionTests.swift │ ├── EssentialEdgesTests.swift │ └── CondensationGraphTests.swift ├── NodeTests.swift ├── ValueTests.swift └── EdgeTests.swift ├── Documentation └── architecture.png ├── .gitignore ├── Code ├── SwiftyToolz Candidates │ └── Dictionary+SwiftyToolz.swift ├── Graph+CreateAndAccess │ ├── Graph+NodeAccess.swift │ ├── Graph+ValueAccess.swift │ ├── Graph+ConvenientInitializers.swift │ └── Graph+EdgeAccess.swift ├── Graph+Algorithms │ ├── Graph+Components.swift │ ├── Graph+AncestorCount.swift │ ├── Graph+FilterAndMap.swift │ ├── Graph+CondensationGraph.swift │ ├── Graph+TransitiveReduction.swift │ ├── Graph+EssentialEdges.swift │ └── Graph+StronglyConnectedComponents.swift └── Graph │ ├── GraphEdge.swift │ ├── GraphNode.swift │ └── Graph.swift ├── Package.swift ├── LICENSE └── README.md /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftNodes] 5 | -------------------------------------------------------------------------------- /Tests/TestGraph.swift: -------------------------------------------------------------------------------- 1 | import SwiftNodes 2 | 3 | typealias TestGraph = HashableValuesGraph 4 | -------------------------------------------------------------------------------- /Documentation/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftNodes/HEAD/Documentation/architecture.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | xcuserdata/ 8 | 9 | # Swift Package Manager 10 | # 11 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 12 | # Packages/ 13 | # Package.pins 14 | Package.resolved 15 | .build/ 16 | .swiftpm/ -------------------------------------------------------------------------------- /Tests/Algorithms/FilterAndMap/ValueFilterTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class ValueFilterTests: XCTestCase { 5 | 6 | func testValueFilter() { 7 | let graph: TestGraph = [1, 2, 10, 20] 8 | let filteredGraph = graph.filtered { $0 < 10 } 9 | let expectedFilteredGraph: Graph = [1, 2] 10 | XCTAssertEqual(filteredGraph, expectedFilteredGraph) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/Algorithms/FilterAndMap/MapTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class MapTests: XCTestCase { 5 | 6 | func testMappingIntValuesToString() { 7 | let graph: TestGraph = [1, 2, 3] 8 | let stringGraph = graph.map { "\($0)" } 9 | let expectedStringGraph: Graph = [1: "1", 2: "2", 3: "3"] 10 | XCTAssertEqual(stringGraph, expectedStringGraph) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Code/SwiftyToolz Candidates/Dictionary+SwiftyToolz.swift: -------------------------------------------------------------------------------- 1 | public extension Dictionary 2 | { 3 | init(values: some Sequence) where Value: Identifiable, Value.ID == Key 4 | { 5 | self.init(uniqueKeysWithValues: values.map({ ($0.id, $0) })) 6 | } 7 | 8 | init(values: some Sequence) where Value: Hashable, Value == Key 9 | { 10 | self.init(uniqueKeysWithValues: values.map({ ($0, $0) })) 11 | } 12 | 13 | mutating func insert(_ value: Value) where Value: Identifiable, Value.ID == Key 14 | { 15 | self[value.id] = value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftNodes", 7 | platforms: [.iOS(.v13), .tvOS(.v13), .macOS(.v10_15), .watchOS(.v6)], 8 | products: [ 9 | .library( 10 | name: "SwiftNodes", 11 | targets: ["SwiftNodes"] 12 | ), 13 | ], 14 | dependencies: [ 15 | .package( 16 | url: "https://github.com/flowtoolz/SwiftyToolz.git", 17 | exact: "0.5.1" 18 | ) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "SwiftNodes", 23 | dependencies: [ 24 | "SwiftyToolz" 25 | ], 26 | path: "Code" 27 | ), 28 | .testTarget( 29 | name: "SwiftNodesTests", 30 | dependencies: ["SwiftNodes", "SwiftyToolz"], 31 | path: "Tests" 32 | ), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 flowtoolz 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 | -------------------------------------------------------------------------------- /Tests/NodeTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class NodeTests: XCTestCase { 5 | 6 | func testInsertingNodes() throws { 7 | var graph = TestGraph() 8 | let node1 = graph.insert(1) 9 | 10 | let valueForID1 = graph.value(for: 1) 11 | let nodeForID1 = graph.node(with: 1) 12 | 13 | XCTAssertEqual(valueForID1, node1.value) 14 | XCTAssertEqual(nodeForID1?.id, node1.id) 15 | } 16 | 17 | func testUUIDAsID() throws { 18 | var graph = Graph() 19 | let node1 = graph.update(1, for: UUID()) 20 | let node2 = graph.update(1, for: UUID()) 21 | XCTAssertNotEqual(node1.id, node2.id) 22 | XCTAssertEqual(node1.value, node2.value) 23 | XCTAssertNotEqual(node1.id, node2.id) 24 | } 25 | 26 | func testOmittingClosureForIdentifiableValues() throws { 27 | struct IdentifiableValue: Identifiable { let id = UUID() } 28 | var graph = Graph() 29 | let node = graph.insert(IdentifiableValue()) // node.id == node.value.id 30 | XCTAssertEqual(node.id, node.value.id) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Algorithms/FilterAndMap/EdgeFilterTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class EdgeFilterTests: XCTestCase { 5 | 6 | func testEdgeFilterCopyingGraph() { 7 | let graph = TestGraph(values: [1, 2, 3, 4], 8 | edges: [(1, 2), (2, 3), (3, 4)]) 9 | 10 | let filteredGraph = graph.filteredEdges { 11 | $0.originID != 3 && $0.destinationID != 3 12 | } 13 | 14 | let expectedGraph = TestGraph(values: [1, 2, 3, 4], 15 | edges: [(1, 2)]) 16 | 17 | // this also compares node neighbour caches, so we also test that the filter correctly updates those... 18 | XCTAssertEqual(filteredGraph, expectedGraph) 19 | } 20 | 21 | func testEdgeFilterMutatingGraph() { 22 | var graph = TestGraph(values: [1, 2, 3, 4], 23 | edges: [(1, 2), (2, 3), (3, 4)]) 24 | 25 | graph.filterEdges { 26 | $0.originID != 3 && $0.destinationID != 3 27 | } 28 | 29 | let expectedGraph = TestGraph(values: [1, 2, 3, 4], 30 | edges: [(1, 2)]) 31 | 32 | // this also compares node neighbour caches, so we also test that the filter correctly updates those... 33 | XCTAssertEqual(graph, expectedGraph) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/Algorithms/ComponentTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class ComponentTests: XCTestCase { 5 | 6 | func testEmptyGraph() { 7 | XCTAssertEqual(TestGraph().findComponents().count, 0) 8 | } 9 | 10 | func testGraphWithoutEdges() { 11 | let graph = TestGraph(values: [1, 2, 3]) 12 | 13 | let expectedComponents: Set> = [[1], [2], [3]] 14 | 15 | XCTAssertEqual(graph.findComponents(), expectedComponents) 16 | } 17 | 18 | func testGraphWithOneTrueComponent() { 19 | let graph = TestGraph(values: [1, 2, 3], edges: [(1, 2), (2, 3)]) 20 | 21 | let expectedComponents: Set> = [[1, 2, 3]] 22 | 23 | XCTAssertEqual(graph.findComponents(), expectedComponents) 24 | } 25 | 26 | func testGraphWithMultipleComponents() { 27 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 28 | edges: [(2, 3), (4, 5), (5, 6)]) 29 | 30 | let expectedComponents: Set> = [[1], [2, 3], [4, 5, 6]] 31 | 32 | XCTAssertEqual(graph.findComponents(), expectedComponents) 33 | } 34 | 35 | func testGraphWithMultipleComponentsAndCycles() { 36 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 37 | edges: [(2, 3), (3, 2), (4, 5), (5, 6), (6, 4)]) 38 | 39 | let expectedComponents: Set> = [[1], [2, 3], [4, 5, 6]] 40 | 41 | XCTAssertEqual(graph.findComponents(), expectedComponents) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Algorithms/AncestorCountsTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class AncestorCountsTests: XCTestCase { 5 | 6 | func testEmptyGraph() { 7 | XCTAssertEqual(TestGraph().findNumberOfNodeAncestors(), 8 | [:]) 9 | } 10 | 11 | func testGraphWithoutEdges() { 12 | let graph = TestGraph(values: [1, 2, 3]) 13 | 14 | XCTAssertEqual(graph.findNumberOfNodeAncestors(), 15 | [1: 0, 2: 0, 3: 0]) 16 | } 17 | 18 | func testGraphWithoutTransitiveEdges() { 19 | let graph = TestGraph(values: [1, 2, 3], 20 | edges: [(1, 2), (2, 3)]) 21 | 22 | XCTAssertEqual(graph.findNumberOfNodeAncestors(), 23 | [1: 0, 2: 1, 3: 2]) 24 | } 25 | 26 | func testGraphWithOneTransitiveEdge() { 27 | let graph = TestGraph(values: [1, 2, 3], 28 | edges: [(1, 2), (2, 3), (1, 3)]) 29 | 30 | XCTAssertEqual(graph.findNumberOfNodeAncestors(), 31 | [1: 0, 2: 1, 3: 2]) 32 | } 33 | 34 | func testGraphWithTwoComponentsEachWithOneTransitiveEdge() { 35 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 36 | edges: [(1, 2), (2, 3), (1, 3), (4, 5), (5, 6), (4, 6)]) 37 | 38 | XCTAssertEqual(graph.findNumberOfNodeAncestors(), 39 | [1: 0, 2: 1, 3: 2, 4: 0, 5: 1, 6: 2]) 40 | } 41 | 42 | func testGraphWithTwoSourcesAnd4PathsToSink() { 43 | let graph = TestGraph(values: [0, 1, 2, 3, 4, 5], 44 | edges: [(0, 2), (1, 2), (2, 3), (2, 4), (3, 5), (4, 5)]) 45 | 46 | XCTAssertEqual(graph.findNumberOfNodeAncestors(), 47 | [0: 0, 1: 0, 2: 2, 3: 3, 4: 3, 5: 5]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Code/Graph+CreateAndAccess/Graph+NodeAccess.swift: -------------------------------------------------------------------------------- 1 | public extension Graph 2 | { 3 | /// Remove the node with the given ID also removing its in- and outgoing edges 4 | @discardableResult 5 | mutating func removeNode(with nodeID: NodeID) -> Node? 6 | { 7 | guard let node = nodesByID.removeValue(forKey: nodeID) else 8 | { 9 | return nil 10 | } 11 | 12 | for ancestorID in node.ancestorIDs 13 | { 14 | removeEdge(with: .init(ancestorID, nodeID)) 15 | } 16 | 17 | for descendantID in node.descendantIDs 18 | { 19 | removeEdge(with: .init(nodeID, descendantID)) 20 | } 21 | 22 | return node 23 | } 24 | 25 | /** 26 | All source nodes of the `Graph`, see ``GraphNode/isSource`` 27 | */ 28 | var sources: some Collection 29 | { 30 | nodesByID.values.filter { $0.isSource } 31 | } 32 | 33 | /** 34 | All sink nodes of the `Graph`, see ``GraphNode/isSink`` 35 | */ 36 | var sinks: some Collection 37 | { 38 | nodesByID.values.filter { $0.isSink } 39 | } 40 | 41 | /** 42 | Whether the `Graph` contains a ``GraphNode`` with the given ``GraphNode/id`` 43 | */ 44 | func contains(_ nodeID: NodeID) -> Bool 45 | { 46 | nodesByID.keys.contains(nodeID) 47 | } 48 | 49 | /** 50 | ``GraphNode`` with the given ``GraphNode/id`` if one exists, otherwise `nil` 51 | */ 52 | func node(with id: NodeID) -> Node? 53 | { 54 | nodesByID[id] 55 | } 56 | 57 | /** 58 | All ``GraphNode``s of the `Graph` 59 | */ 60 | var nodes: some Collection 61 | { 62 | nodesByID.values 63 | } 64 | 65 | /** 66 | The ``GraphNode/id``s of all ``GraphNode``s of the `Graph` 67 | */ 68 | var nodeIDs: some Collection 69 | { 70 | nodesByID.keys 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Code/Graph+Algorithms/Graph+Components.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension Graph 4 | { 5 | /** 6 | Find the [components](https://en.wikipedia.org/wiki/Component_(graph_theory)) of the `Graph` 7 | 8 | - Returns: Multiple sets of nodes which represent the components of the graph 9 | */ 10 | func findComponents() -> Set 11 | { 12 | var visitedNodes = NodeIDs() 13 | 14 | var components = Set() 15 | 16 | for node in nodeIDs 17 | { 18 | if visitedNodes.contains(node) { continue } 19 | 20 | // this node has not been visited yet 21 | 22 | components += Set(findLackingNodes(forComponent: [], 23 | startingAt: node, 24 | visitedNodes: &visitedNodes)) 25 | 26 | visitedNodes.insert(node) 27 | } 28 | 29 | return components 30 | } 31 | 32 | /// startNode is connected to the incompleteComponent but not contained in it. both will be in the resulting actual component. 33 | private func findLackingNodes(forComponent incompleteComponent: [NodeID], 34 | startingAt startNode: NodeID, 35 | visitedNodes: inout NodeIDs) -> [NodeID] 36 | { 37 | var lackingNodes = [startNode] 38 | 39 | visitedNodes += startNode 40 | 41 | let neighbours = node(with: startNode)?.neighbourIDs ?? [] 42 | 43 | for neighbour in neighbours 44 | { 45 | if visitedNodes.contains(neighbour) { continue } 46 | 47 | let extendedComponent = incompleteComponent + lackingNodes 48 | lackingNodes += findLackingNodes(forComponent: extendedComponent, 49 | startingAt: neighbour, 50 | visitedNodes: &visitedNodes) 51 | } 52 | 53 | return lackingNodes 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Code/Graph+Algorithms/Graph+AncestorCount.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension Graph 4 | { 5 | /** 6 | Find the total (recursive) number of ancestors for each ``GraphNode`` of an **acyclic** `Graph` 7 | 8 | The ancestor count of a node is basically the number of other nodes from which the node can be reached. This only works on acyclic graphs right now and might return incorrect results for nodes in cycles. 9 | 10 | Ancestor counts can serve as a proxy for [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting). 11 | 12 | Note that the worst case space complexity is quadratic in the number of nodes. 13 | 14 | - Returns: A dictionary containing the ancestor count for every node ``GraphNode/id`` of the `Graph` 15 | */ 16 | func findNumberOfNodeAncestors() -> [NodeID: Int] 17 | { 18 | var ancestorsByNodeID = [NodeID: Set]() 19 | 20 | sinks.forEach 21 | { 22 | getAncestors(for: $0.id, ancestorsByNodeID: &ancestorsByNodeID) 23 | } 24 | 25 | return ancestorsByNodeID.mapValues { $0.count } 26 | } 27 | 28 | @discardableResult 29 | private func getAncestors(for node: NodeID, 30 | ancestorsByNodeID: inout [NodeID: Set]) -> Set 31 | { 32 | if let ancestors = ancestorsByNodeID[node] { return ancestors } 33 | 34 | ancestorsByNodeID[node] = [] // mark node as visited to avoid infinite loops in cyclic graphs 35 | 36 | guard let directAncestors = self.node(with: node)?.ancestorIDs else 37 | { 38 | log(error: "No node for node ID exists, but it should.") 39 | return [] 40 | } 41 | 42 | var ancestors = directAncestors 43 | 44 | for directAncestor in directAncestors 45 | { 46 | ancestors += getAncestors(for: directAncestor, 47 | ancestorsByNodeID: &ancestorsByNodeID) 48 | } 49 | 50 | ancestorsByNodeID[node] = ancestors 51 | 52 | return ancestors 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/Algorithms/SCCTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class SCCTests: XCTestCase { 5 | 6 | func testEmptyGraph() { 7 | XCTAssertEqual(TestGraph().findStronglyConnectedComponents().count, 0) 8 | } 9 | 10 | func testGraphWithoutEdges() { 11 | let graph = TestGraph(values: [1, 2, 3]) 12 | 13 | let expectedComponents: Set> = [[1], [2], [3]] 14 | 15 | XCTAssertEqual(graph.findStronglyConnectedComponents(), expectedComponents) 16 | } 17 | 18 | func testGraphWithOneTrueComponent() { 19 | let graph = TestGraph(values: [1, 2, 3], edges: [(1, 2), (2, 3)]) 20 | 21 | let expectedComponents: Set> = [[1], [2], [3]] 22 | 23 | XCTAssertEqual(graph.findStronglyConnectedComponents(), expectedComponents) 24 | } 25 | 26 | func testGraphWithMultipleComponents() { 27 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 28 | edges: [(2, 3), (4, 5), (5, 6)]) 29 | 30 | let expectedComponents: Set> = [[1], [2], [3], [4], [5], [6]] 31 | 32 | XCTAssertEqual(graph.findStronglyConnectedComponents(), expectedComponents) 33 | } 34 | 35 | func testGraphWithMultipleComponentsAndCycles() { 36 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 37 | edges: [(2, 3), (3, 2), (4, 5), (5, 6), (6, 4)]) 38 | 39 | let expectedComponents: Set> = [[1], [2, 3], [4, 5, 6]] 40 | 41 | XCTAssertEqual(graph.findStronglyConnectedComponents(), expectedComponents) 42 | } 43 | 44 | func testGraphWithOneBigCycle() { 45 | let graph = TestGraph(values: [1, 2, 3, 4, 5], 46 | edges: [(1, 2), (2, 3), (3, 4), (4, 5), (5, 1)]) 47 | 48 | let expectedComponents: Set> = [[1, 2, 3, 4, 5]] 49 | 50 | XCTAssertEqual(graph.findStronglyConnectedComponents(), expectedComponents) 51 | } 52 | 53 | func testGraphWithTwoConnectedCycles() { 54 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 55 | edges: [(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)]) 56 | 57 | let expectedComponents: Set> = [[1, 2, 3], [4, 5, 6]] 58 | 59 | XCTAssertEqual(graph.findStronglyConnectedComponents(), expectedComponents) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Code/Graph+CreateAndAccess/Graph+ValueAccess.swift: -------------------------------------------------------------------------------- 1 | public extension Graph 2 | { 3 | subscript(_ nodeID: NodeID) -> NodeValue? 4 | { 5 | get 6 | { 7 | nodesByID[nodeID]?.value 8 | } 9 | 10 | set 11 | { 12 | guard let newValue else 13 | { 14 | removeNode(with: nodeID) 15 | return 16 | } 17 | 18 | update(newValue, for: nodeID) 19 | } 20 | } 21 | 22 | @discardableResult 23 | mutating func insert(_ value: NodeValue) -> Node where NodeValue: Identifiable, NodeValue.ID == NodeID 24 | { 25 | update(value, for: value.id) 26 | } 27 | 28 | @discardableResult 29 | mutating func insert(_ value: NodeValue) -> Node where NodeID == NodeValue 30 | { 31 | update(value, for: value) 32 | } 33 | 34 | @discardableResult 35 | mutating func remove(_ value: NodeValue) -> Node? where NodeValue: Identifiable, NodeValue.ID == NodeID 36 | { 37 | removeNode(with: value.id) 38 | } 39 | 40 | @discardableResult 41 | mutating func remove(_ value: NodeValue) -> Node? where NodeID == NodeValue 42 | { 43 | removeNode(with: value) 44 | } 45 | 46 | /** 47 | Insert a `NodeValue` and get the (new) ``GraphNode`` that stores it 48 | 49 | - Returns: The (possibly new) ``GraphNode`` holding the value 50 | */ 51 | @discardableResult 52 | mutating func update(_ value: NodeValue, for nodeID: NodeID) -> Node 53 | { 54 | if var node = nodesByID[nodeID] 55 | { 56 | node.value = value 57 | nodesByID[nodeID] = node 58 | return node 59 | } 60 | else 61 | { 62 | let node = Node(id: nodeID, value: value) 63 | nodesByID[nodeID] = node 64 | return node 65 | } 66 | } 67 | 68 | @discardableResult 69 | mutating func removeValue(for nodeID: NodeID) -> NodeValue? 70 | { 71 | removeNode(with: nodeID)?.value 72 | } 73 | 74 | /** 75 | ``GraphNode/value`` of the ``GraphNode`` with the given ``GraphNode/id`` if one exists, otherwise `nil` 76 | */ 77 | func value(for nodeID: NodeID) -> NodeValue? 78 | { 79 | nodesByID[nodeID]?.value 80 | } 81 | 82 | /** 83 | All `NodeValue`s of the `Graph` 84 | */ 85 | var values: some Collection 86 | { 87 | nodesByID.values.map { $0.value } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Code/Graph+Algorithms/Graph+FilterAndMap.swift: -------------------------------------------------------------------------------- 1 | public extension Graph 2 | { 3 | // MARK: - Value Mapper 4 | 5 | func map(_ transform: (NodeValue) throws -> MappedValue) rethrows -> Graph 6 | { 7 | let idsWithMappedValues = try nodes.map { ($0.id, try transform($0.value)) } 8 | 9 | return .init(valuesByID: .init(uniqueKeysWithValues: idsWithMappedValues), 10 | edges: edges) 11 | } 12 | 13 | // MARK: - Edge Filters 14 | 15 | func filteredEdges(_ isIncluded: EdgeIDs) -> Self 16 | { 17 | var result = self 18 | result.filterEdges(isIncluded) 19 | return result 20 | } 21 | 22 | mutating func filterEdges(_ isIncluded: EdgeIDs) 23 | { 24 | filterEdges { isIncluded.contains($0.id) } 25 | } 26 | 27 | func filteredEdges(_ isIncluded: (Edge) throws -> Bool) rethrows -> Self 28 | { 29 | var result = self 30 | try result.filterEdges(isIncluded) 31 | return result 32 | } 33 | 34 | mutating func filterEdges(_ isIncluded: (Edge) throws -> Bool) rethrows 35 | { 36 | try edges.forEach 37 | { 38 | if try !isIncluded($0) 39 | { 40 | removeEdge(with: $0.id) 41 | } 42 | } 43 | } 44 | 45 | // MARK: - Value Filters 46 | 47 | func filtered(_ isIncluded: (NodeValue) throws -> Bool) rethrows -> Self 48 | { 49 | var result = self 50 | try result.filter(isIncluded) 51 | return result 52 | } 53 | 54 | mutating func filter(_ isIncluded: (NodeValue) throws -> Bool) rethrows 55 | { 56 | try filterNodes { try isIncluded($0.value) } 57 | } 58 | 59 | // MARK: - Node Filters 60 | 61 | func filteredNodes(_ isIncluded: NodeIDs) -> Self 62 | { 63 | var result = self 64 | result.filterNodes(isIncluded) 65 | return result 66 | } 67 | 68 | mutating func filterNodes(_ isIncluded: NodeIDs) 69 | { 70 | filterNodes { isIncluded.contains($0.id) } 71 | } 72 | 73 | func filteredNodes(_ isIncluded: (Node) throws -> Bool) rethrows -> Self 74 | { 75 | var result = self 76 | try result.filterNodes(isIncluded) 77 | return result 78 | } 79 | 80 | mutating func filterNodes(_ isIncluded: (Node) throws -> Bool) rethrows 81 | { 82 | try nodes.forEach 83 | { 84 | if try !isIncluded($0) 85 | { 86 | removeNode(with: $0.id) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Code/Graph+Algorithms/Graph+CondensationGraph.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension Graph.StronglyConnectedComponent: Sendable where NodeID: Sendable {} 4 | 5 | public extension Graph 6 | { 7 | /** 8 | Creates the acyclic [condensation graph](https://en.wikipedia.org/wiki/Strongly_connected_component) of the `Graph` 9 | 10 | The condensation graph is the graph in which the [strongly connected components](https://en.wikipedia.org/wiki/Strongly_connected_component) of the original graph have been collapsed into single nodes, so the resulting condensation graph is acyclic. 11 | */ 12 | func makeCondensationGraph() -> CondensationGraph 13 | { 14 | // get SCCs 15 | let sccs = findStronglyConnectedComponents().map 16 | { 17 | StronglyConnectedComponent(nodeIDs: $0) 18 | } 19 | 20 | // create hashmap from node IDs to the containing SCCs 21 | var sccByNodeID = [Node.ID: StronglyConnectedComponent]() 22 | 23 | for scc in sccs 24 | { 25 | for nodeID in scc.nodeIDs 26 | { 27 | sccByNodeID[nodeID] = scc 28 | } 29 | } 30 | 31 | // create condensation graph 32 | var condensationGraph = CondensationGraph(values: sccs) 33 | 34 | // add condensation edges 35 | for edgeID in edgeIDs 36 | { 37 | guard let originSCC = sccByNodeID[edgeID.originID], 38 | let destinationSCC = sccByNodeID[edgeID.destinationID] else 39 | { 40 | log(error: "mising scc in hash map") 41 | continue 42 | } 43 | 44 | if originSCC.id != destinationSCC.id 45 | { 46 | condensationGraph.insert(CondensationEdge(from: originSCC.id, 47 | to: destinationSCC.id)) 48 | } 49 | } 50 | 51 | // return graph 52 | return condensationGraph 53 | } 54 | 55 | typealias CondensationNode = CondensationGraph.Node 56 | typealias CondensationEdge = CondensationGraph.Edge 57 | 58 | typealias CondensationGraph = Graph 59 | 60 | struct StronglyConnectedComponent: Identifiable, Hashable 61 | { 62 | public var id: ID { nodeIDs } 63 | 64 | public typealias ID = NodeIDs 65 | 66 | init(nodeIDs: NodeIDs) 67 | { 68 | self.nodeIDs = nodeIDs 69 | } 70 | 71 | public let nodeIDs: NodeIDs 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/Algorithms/TransitiveReductionTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class MEGTests: XCTestCase { 5 | 6 | func testEmptyGraph() { 7 | XCTAssertEqual(TestGraph().filteredTransitiveReduction(), TestGraph()) 8 | } 9 | 10 | func testGraphWithoutEdges() { 11 | let graph = TestGraph(values: [1, 2, 3]) 12 | 13 | XCTAssertEqual(graph.filteredTransitiveReduction(), graph) 14 | } 15 | 16 | func testGraphWithoutTransitiveEdges() { 17 | let graph = TestGraph(values: [1, 2, 3], 18 | edges: [(1, 2), (2, 3)]) 19 | 20 | XCTAssertEqual(graph.filteredTransitiveReduction(), graph) 21 | } 22 | 23 | func testGraphWithOneTransitiveEdge() { 24 | let graph = TestGraph(values: [1, 2, 3], 25 | edges: [(1, 2), (2, 3), (1, 3)]) 26 | 27 | let expectedMEG = TestGraph(values: [1, 2, 3], 28 | edges: [(1, 2), (2, 3)]) 29 | 30 | XCTAssertEqual(graph.filteredTransitiveReduction(), expectedMEG) 31 | } 32 | 33 | func testGraphWithTwoComponentsEachWithOneTransitiveEdge() { 34 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 35 | edges: [(1, 2), (2, 3), (1, 3), (4, 5), (5, 6), (4, 6)]) 36 | 37 | let expectedMEG = TestGraph(values: [1, 2, 3, 4, 5, 6], 38 | edges: [(1, 2), (2, 3), (4, 5), (5, 6)]) 39 | 40 | XCTAssertEqual(graph.filteredTransitiveReduction(), expectedMEG) 41 | } 42 | 43 | func testGraphWithManyTransitiveEdges() { 44 | var graph = TestGraph() 45 | 46 | let numberOfNodes = 10 47 | 48 | for j in 0 ..< numberOfNodes 49 | { 50 | graph.insert(j) 51 | 52 | for i in 0 ..< j 53 | { 54 | graph.insertEdge(from: i, to: j) 55 | } 56 | } 57 | 58 | // sanity check 59 | let expectedNumberOfEdges = ((numberOfNodes * numberOfNodes) - numberOfNodes) / 2 60 | XCTAssertEqual(graph.edges.count, expectedNumberOfEdges) 61 | 62 | // only edges between neighbouring numbers should remain (0 -> 1 -> 2 ...) 63 | 64 | var expectedMEG = TestGraph() 65 | 66 | for i in 0 ..< numberOfNodes 67 | { 68 | expectedMEG.insert(i) 69 | 70 | if i > 0 71 | { 72 | expectedMEG.insertEdge(from: i - 1, to: i) 73 | } 74 | } 75 | 76 | XCTAssertEqual(graph.filteredTransitiveReduction(), expectedMEG) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Code/Graph/GraphEdge.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension GraphEdge: Sendable where NodeID: Sendable, Weight: Sendable {} 4 | extension GraphEdge.ID: Sendable where NodeID: Sendable {} 5 | 6 | extension GraphEdge: Codable where NodeID: Codable, Weight: Codable {} 7 | extension GraphEdge.ID: Codable where NodeID: Codable {} 8 | 9 | /** 10 | Directed connection of two ``GraphNode``s in a ``Graph`` 11 | 12 | A `GraphEdge` points from an origin- to a destination node and therefor has an ``GraphEdge/originID`` and a ``GraphEdge/destinationID``. 13 | 14 | A `GraphEdge` is `Identifiable` by its ``GraphEdge/id-swift.property``, which is a combination of ``GraphEdge/originID`` and ``GraphEdge/destinationID``. 15 | */ 16 | public struct GraphEdge: Identifiable, Equatable 17 | { 18 | /** 19 | A shorthand for the edge's full generic type name `GraphEdge` 20 | */ 21 | public typealias Edge = GraphEdge 22 | 23 | // MARK: - Identity 24 | 25 | /** 26 | The edge's `ID` combines the ``GraphNode/id``s of ``GraphEdge/originID`` and ``GraphEdge/destinationID`` 27 | */ 28 | public var id: ID { ID(originID, destinationID) } 29 | 30 | /** 31 | An edge's `ID` combines the ``GraphNode/id``s of its ``GraphEdge/originID`` and ``GraphEdge/destinationID`` 32 | */ 33 | public struct ID: Hashable 34 | { 35 | internal init(_ originID: NodeID, _ destinationID: NodeID) 36 | { 37 | self.originID = originID 38 | self.destinationID = destinationID 39 | } 40 | 41 | public let originID: NodeID 42 | public let destinationID: NodeID 43 | } 44 | 45 | // MARK: - Basics 46 | 47 | /// Create a ``GraphEdge``, for instance to pass it to a ``Graph`` initializer. 48 | public init(from originID: NodeID, 49 | to destinationID: NodeID, 50 | weight: Weight = 1) 51 | { 52 | self.originID = originID 53 | self.destinationID = destinationID 54 | 55 | self.weight = weight 56 | } 57 | 58 | /** 59 | The edge weight. 60 | 61 | If you don't need edge weights and want to save memory, you could specify `UInt8` (a.k.a. Byte) as the edge weight type, so each edge would require just one Byte instead of for example four or 8 Bytes for other numeric types. 62 | */ 63 | public internal(set) var weight: Weight 64 | 65 | /** 66 | The origin ``GraphNode/id`` at which the edge starts / from which it goes out 67 | */ 68 | public let originID: NodeID 69 | 70 | /** 71 | The destination ``GraphNode/id`` at which the edge ends / to which it goes in 72 | */ 73 | public let destinationID: NodeID 74 | } 75 | -------------------------------------------------------------------------------- /Code/Graph/GraphNode.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension GraphNode: Sendable where ID: Sendable, Value: Sendable {} 4 | extension GraphNode: Equatable where Value: Equatable {} 5 | extension GraphNode: Codable where ID: Codable, Value: Codable {} 6 | 7 | /** 8 | Unique node of a ``Graph``, holds a value, can be connected to other nodes of the graph 9 | 10 | A `GraphNode` is `Identifiable` by its ``GraphNode/id`` of generic type `ID`. 11 | 12 | A `GraphNode` has caches maintained by its ``Graph`` that enable quick access to the node's neighbours, see ``GraphNode/ancestorIDs``, ``GraphNode/descendantIDs`` and related properties. 13 | 14 | A `Graph` creates, owns and manages its `GraphNode`s. You can not create a `GraphNode` or mutate the `GraphNode`s contained in a `Graph`, since the contained neighbour caches must always be consistent with the `Graph`'s edges. 15 | 16 | When inserting a `NodeValue` into a ``Graph``, the ``Graph`` determines the ID of the node containing that value by use of the closure passed to- (``Graph/init(values:edges:determineNodeIDForNewValue:)``) or implied by its initializer. 17 | */ 18 | public struct GraphNode: Identifiable 19 | { 20 | // MARK: - Caches for Accessing Neighbours Quickly 21 | 22 | /** 23 | Indicates whether the node has no descendants (no outgoing edges) 24 | */ 25 | public var isSink: Bool { descendantIDs.isEmpty } 26 | 27 | /** 28 | Indicates whether the node has no ancestors (no ingoing edges) 29 | */ 30 | public var isSource: Bool { ancestorIDs.isEmpty } 31 | 32 | /** 33 | The node's neighbours (nodes connected via in- or outgoing edges) 34 | */ 35 | public var neighbourIDs: Set { ancestorIDs + descendantIDs } 36 | 37 | /** 38 | The node's ancestors (nodes connected via ingoing edges) 39 | */ 40 | public internal(set) var ancestorIDs = Set() 41 | 42 | /** 43 | The node's descendants (nodes connected via outgoing edges) 44 | */ 45 | public internal(set) var descendantIDs = Set() 46 | 47 | /** 48 | A shorthand for the node's full generic type name `GraphNode` 49 | */ 50 | public typealias Node = GraphNode 51 | 52 | // MARK: - Identity & Value 53 | 54 | /** 55 | Create a `GraphNode` as input for a new ``Graph``, see ``Graph/init(nodes:makeNodeIDForValue:)`` 56 | */ 57 | public init(id: ID, value: Value) 58 | { 59 | self.id = id 60 | self.value = value 61 | } 62 | 63 | /** 64 | The `Hashable` ID by which the node is `Identifiable` 65 | */ 66 | public let id: ID 67 | 68 | /** 69 | The actual stored `Value` 70 | */ 71 | public internal(set) var value: Value 72 | } 73 | -------------------------------------------------------------------------------- /Tests/Algorithms/EssentialEdgesTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class EssentialEdgesTests: XCTestCase { 5 | 6 | func testEmptyGraph() { 7 | XCTAssert(TestGraph().findEssentialEdges().isEmpty) 8 | } 9 | 10 | func testGraphWithoutEdges() { 11 | let graph = TestGraph(values: [1, 2, 3]) 12 | 13 | XCTAssert(graph.findEssentialEdges().isEmpty) 14 | } 15 | 16 | func testGraphWithoutTransitiveEdges() { 17 | let graph = TestGraph(values: [1, 2, 3], 18 | edges: [(1, 2), (2, 3)]) 19 | 20 | XCTAssertEqual(graph.findEssentialEdges(), 21 | [.init(1, 2), .init(2, 3)]) 22 | } 23 | 24 | func testGraphWithOneTransitiveEdge() { 25 | let graph = TestGraph(values: [1, 2, 3], 26 | edges: [(1, 2), (2, 3), (1, 3)]) 27 | 28 | XCTAssertEqual(graph.findEssentialEdges(), 29 | [.init(1, 2), .init(2, 3)]) 30 | } 31 | 32 | func testAcyclicGraphWithManyTransitiveEdges() { 33 | var graph = TestGraph() 34 | 35 | let numberOfNodes = 10 36 | 37 | for j in 0 ..< numberOfNodes 38 | { 39 | graph.insert(j) 40 | 41 | for i in 0 ..< j 42 | { 43 | graph.insertEdge(from: i, to: j) 44 | } 45 | } 46 | 47 | // sanity check 48 | let expectedNumberOfEdges = ((numberOfNodes * numberOfNodes) - numberOfNodes) / 2 49 | XCTAssertEqual(graph.edges.count, expectedNumberOfEdges) 50 | 51 | // only edges between neighbouring numbers are essential (0 -> 1 -> 2 ...) 52 | 53 | var expectedEssentialEdges = Set() 54 | 55 | for i in 1 ..< numberOfNodes 56 | { 57 | expectedEssentialEdges.insert(.init(i - 1, i)) 58 | } 59 | 60 | XCTAssertEqual(graph.findEssentialEdges(), expectedEssentialEdges) 61 | } 62 | 63 | func testGraphWithTwoCyclesAndOnlyEssentialEdges() { 64 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 65 | edges: [(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (6, 4)]) 66 | 67 | XCTAssertEqual(graph.findEssentialEdges(), Set(graph.edgeIDs)) 68 | } 69 | 70 | func testGraphWithTwoCyclesAndOneNonEssentialEdge() { 71 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6, 7], 72 | edges: [(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (6, 4), (6, 7), (3, 7)]) 73 | 74 | let allEdgesExceptNonEssentialOne = Set(graph.edgeIDs).subtracting([.init(3, 7)]) 75 | 76 | XCTAssertEqual(graph.findEssentialEdges(), allEdgesExceptNonEssentialOne) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/Algorithms/FilterAndMap/NodeFilterTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class NodeFilterTests: XCTestCase { 5 | 6 | func testGraphWithoutEdges() { 7 | let graph = TestGraph(values: [1, 2, 3]) 8 | 9 | XCTAssertEqual(graph.filteredNodes([]), 10 | TestGraph(values: [])) 11 | 12 | XCTAssertEqual(graph.filteredNodes([1, 2]), 13 | TestGraph(values: [1, 2])) 14 | 15 | XCTAssertEqual(graph.filteredNodes([1, 2, 4]), 16 | TestGraph(values: [1, 2])) 17 | 18 | XCTAssertEqual(graph.filteredNodes([1, 2, 3]), 19 | TestGraph(values: [1, 2, 3])) 20 | } 21 | 22 | func testSimpleTestGraph() { 23 | let graph = TestGraph(values: [1, 2, 3, 4], 24 | edges: [(1, 2), (2, 3), (3, 4)]) 25 | 26 | XCTAssertEqual(graph.filteredNodes([]), 27 | TestGraph(values: [])) 28 | 29 | XCTAssertEqual(graph.filteredNodes([1, 3]), 30 | TestGraph(values: [1, 3])) 31 | 32 | XCTAssertEqual(graph.filteredNodes([2, 4]), 33 | TestGraph(values: [2, 4])) 34 | 35 | XCTAssertEqual(graph.filteredNodes([2, 3]), 36 | TestGraph(values: [2, 3], edges: [(2, 3)])) 37 | 38 | XCTAssertEqual(graph.filteredNodes([2, 3, 4]), 39 | TestGraph(values: [2, 3, 4], edges: [(2, 3), (3, 4)])) 40 | } 41 | 42 | func testGraphWithTransitiveEdges() { 43 | let graph = TestGraph(values: [1, 2, 3, 4, 5], 44 | edges: [(1, 2), (2, 3), (3, 4), (4, 5), (1, 3), (2, 4), (3, 5)]) 45 | 46 | XCTAssertEqual(graph.filteredNodes([]), 47 | TestGraph(values: [])) 48 | 49 | XCTAssertEqual(graph.filteredNodes([2, 3]), 50 | TestGraph(values: [2, 3], edges: [(2, 3)])) 51 | 52 | XCTAssertEqual(graph.filteredNodes([2, 3, 4]), 53 | TestGraph(values: [2, 3, 4], edges: [(2, 3), (3, 4), (2, 4)])) 54 | 55 | XCTAssertEqual(graph.filteredNodes([1, 3, 5]), 56 | TestGraph(values: [1, 3, 5], edges: [(1, 3), (3, 5)])) 57 | } 58 | 59 | func testGraphWithCycles() { 60 | let graph = TestGraph(values: [1, 2, 3, 4, 5], 61 | edges: [(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 3)]) 62 | 63 | XCTAssertEqual(graph.filteredNodes([]), 64 | TestGraph(values: [])) 65 | 66 | XCTAssertEqual(graph.filteredNodes([1, 4]), 67 | TestGraph(values: [1, 4])) 68 | 69 | XCTAssertEqual(graph.filteredNodes([2, 3, 4]), 70 | TestGraph(values: [2, 3, 4], edges: [(2, 3), (3, 4)])) 71 | 72 | XCTAssertEqual(graph.filteredNodes([3, 4, 5]), 73 | TestGraph(values: [3, 4, 5], edges: [(3, 4), (4, 5), (5, 3)])) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Code/Graph/Graph.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public typealias HashableValuesGraph = Graph 4 | public typealias IdentifiableValuesGraph = Graph 5 | 6 | extension Graph: Sendable where NodeID: Sendable, NodeValue: Sendable, EdgeWeight: Sendable {} 7 | extension Graph: Equatable where NodeValue: Equatable {} 8 | extension Graph: Codable where NodeID: Codable, NodeValue: Codable, EdgeWeight: Codable {} 9 | 10 | /** 11 | Holds `Value`s in unique ``GraphNode``s which can be connected through ``GraphEdge``s 12 | 13 | The `Graph` creates `GraphNode`s when you insert `NodeValue`s into it, whereby the `Graph` determines the node IDs for new values according to the closure passed to- or implied by its initializer, see ``Graph/init(nodes:determineNodeIDForNewValue:)`` and other initializers. 14 | 15 | A `Graph` is Equatable if its `NodeValue` is. Equatability excludes the `determineNodeIDForNewValue` closure mentioned above. 16 | */ 17 | public struct Graph 18 | { 19 | // MARK: - Initialize 20 | 21 | /** 22 | Create a `Graph` that determines ``GraphNode/id``s for new `NodeValue`s via the given closure 23 | */ 24 | public init(valuesByID: Dictionary, 25 | edges: some Sequence) 26 | { 27 | // create nodes with their neighbour caches 28 | 29 | let idNodePairs = valuesByID.map { ($0.0 , Node(id: $0.0, value: $0.1)) } 30 | var nodesByIDTemporary = [NodeID: Node](uniqueKeysWithValues: idNodePairs) 31 | 32 | edges.forEach 33 | { 34 | nodesByIDTemporary[$0.originID]?.descendantIDs.insert($0.destinationID) 35 | nodesByIDTemporary[$0.destinationID]?.ancestorIDs.insert($0.originID) 36 | } 37 | 38 | // set nodes and edges 39 | 40 | nodesByID = nodesByIDTemporary 41 | edgesByID = .init(values: edges) 42 | } 43 | 44 | public init() 45 | { 46 | nodesByID = .init() 47 | edgesByID = .init() 48 | } 49 | 50 | // MARK: - Edges 51 | 52 | /** 53 | All ``GraphEdge``s of the `Graph` hashable by their ``GraphEdge/id-swift.property`` 54 | */ 55 | public internal(set) var edgesByID: [Edge.ID: Edge] 56 | 57 | /** 58 | Shorthand for `Set` 59 | */ 60 | public typealias EdgeIDs = Set 61 | 62 | /** 63 | Shorthand for the full generic type name `GraphEdge` 64 | */ 65 | public typealias Edge = GraphEdge 66 | 67 | // MARK: - Nodes 68 | 69 | /** 70 | All ``GraphNode``s of the `Graph` hashable by their ``GraphNode/id``s 71 | */ 72 | public internal(set) var nodesByID = [NodeID: Node]() 73 | 74 | /** 75 | Shorthand for the `Graph`'s full generic node type `GraphNode` 76 | */ 77 | public typealias Node = GraphNode 78 | 79 | /** 80 | Shorthand for `Set` 81 | */ 82 | public typealias NodeIDs = Set 83 | } 84 | -------------------------------------------------------------------------------- /Tests/ValueTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class ValueTests: XCTestCase { 5 | 6 | func testThatCodeFromREADMECompiles() 7 | { 8 | let graph = Graph(values: [1, 2, 3], 9 | edges: [(1, 2), (2, 3), (1, 3)]) 10 | 11 | let graph2: Graph = [1, 2, 3] 12 | 13 | typealias MyGraph = Graph 14 | 15 | let graph3 = MyGraph(valuesByID: ["a": 1, "b": 2, "c": 3], 16 | edges: [("a", "b"), ("b", "c"), ("a", "c")]) 17 | 18 | let graph4: MyGraph = ["a": 1, "b": 2, "c": 3] 19 | 20 | struct IdentifiableValue: Identifiable { let id = UUID() } 21 | typealias IVGraph = Graph 22 | 23 | let values = [IdentifiableValue(), IdentifiableValue(), IdentifiableValue()] 24 | let ids = values.map { $0.id } 25 | let graph5 = IVGraph(values: values, 26 | edges: [(ids[0], ids[1]), (ids[1], ids[2]), (ids[0], ids[2])]) 27 | 28 | var graph6 = Graph() 29 | 30 | graph6["a"] = 1 31 | let valueA = graph6["a"] 32 | graph6["a"] = nil 33 | 34 | graph6.update(2, for: "b") // returns the updated/created `Node` but is `@discardable` 35 | let valueB = graph6.value(for: "b") 36 | graph6.removeValue(for: "b") // returns the removed `NodeValue?` but is `@discardable` 37 | 38 | let allValues = graph6.values // returns `some Collection` 39 | 40 | var graph7 = Graph() 41 | 42 | graph7.insert(1) // returns the updated/created `Node` but is `@discardable` 43 | graph7.remove(1) // returns the removed `Node?` but is `@discardable` 44 | } 45 | 46 | func testInitializingWithIDValuePairs() { 47 | 48 | } 49 | 50 | func testAccessingMutatingAndDeletingValuesViaSubscript() 51 | { 52 | 53 | } 54 | 55 | func testThatProvidingMultipleEdgesWithTheSameIDAddsTheirWeights() 56 | { 57 | 58 | } 59 | 60 | func testThatProvidingOrInferingDuplicateNodeIDsWorks() 61 | { 62 | 63 | } 64 | 65 | func testInitializingWithValuesWhereValuesAreNodeIDs() { 66 | 67 | } 68 | 69 | func testInitializingWithValuesWhereValuesAreIdentifiable() { 70 | 71 | } 72 | 73 | func testGettingAllValues() { 74 | 75 | } 76 | 77 | func testGettingValues() { 78 | 79 | 80 | 81 | } 82 | 83 | func testUpdatingValues() { 84 | 85 | 86 | 87 | } 88 | 89 | func testSubscript() { 90 | 91 | 92 | 93 | } 94 | 95 | func testInsertingValuesWhereValuesAreNodeIDs() { 96 | 97 | 98 | 99 | } 100 | 101 | func testInsertingValuesWhereValuesAreIdentifiable() { 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Code/Graph+CreateAndAccess/Graph+ConvenientInitializers.swift: -------------------------------------------------------------------------------- 1 | //extension Graph: ExpressibleByArrayLiteral where NodeValue: Identifiable, NodeValue.ID == NodeID 2 | //{ 3 | // public init(arrayLiteral elements: NodeValue...) 4 | // { 5 | // self.init(values: elements) 6 | // } 7 | //} 8 | 9 | extension Graph: ExpressibleByArrayLiteral where NodeID == NodeValue 10 | { 11 | public init(arrayLiteral elements: NodeValue...) 12 | { 13 | self.init(values: elements) 14 | } 15 | } 16 | 17 | extension Graph: ExpressibleByDictionaryLiteral 18 | { 19 | public init(dictionaryLiteral elements: (NodeID, NodeValue)...) 20 | { 21 | self.init(valuesByID: .init(uniqueKeysWithValues: elements)) 22 | } 23 | } 24 | 25 | public extension Graph 26 | { 27 | init(values: some Sequence) 28 | where NodeValue: Identifiable, NodeValue.ID == NodeID 29 | { 30 | self.init(valuesByID: .init(values: values)) 31 | } 32 | 33 | /** 34 | Uses the `NodeValue.ID` of a value as the ``GraphNode/id`` for its corresponding node 35 | */ 36 | init(values: some Sequence, 37 | edges: some Sequence<(NodeID, NodeID)>) 38 | where NodeValue: Identifiable, NodeValue.ID == NodeID 39 | { 40 | self.init(valuesByID: .init(values: values), edges: edges) 41 | } 42 | 43 | init(values: some Sequence, 44 | edgeTuples: some Sequence<(NodeID, NodeID)>) 45 | where NodeValue: Identifiable, NodeValue.ID == NodeID 46 | { 47 | self.init(valuesByID: .init(values: values), edges: edgeTuples) 48 | } 49 | 50 | /** 51 | Uses the `NodeValue.ID` of a value as the ``GraphNode/id`` for its corresponding node 52 | */ 53 | init(values: some Sequence, 54 | edges: some Sequence) 55 | where NodeValue: Identifiable, NodeValue.ID == NodeID 56 | { 57 | self.init(valuesByID: .init(values: values), edges: edges) 58 | } 59 | 60 | init(values: some Sequence) 61 | where NodeID == NodeValue 62 | { 63 | self.init(valuesByID: .init(values: values)) 64 | } 65 | 66 | /** 67 | Uses a `NodeValue` itself as the ``GraphNode/id`` for its corresponding node 68 | */ 69 | init(values: some Sequence, 70 | edges: some Sequence<(NodeID, NodeID)>) 71 | where NodeID == NodeValue 72 | { 73 | self.init(valuesByID: .init(values: values), 74 | edges: edges) 75 | } 76 | 77 | /** 78 | Uses a `NodeValue` itself as the ``GraphNode/id`` for its corresponding node 79 | */ 80 | init(values: some Sequence, 81 | edges: some Sequence) 82 | where NodeID == NodeValue 83 | { 84 | self.init(valuesByID: .init(values: values), edges: edges) 85 | } 86 | 87 | init(valuesByID: Dictionary) 88 | { 89 | self.init(valuesByID: valuesByID, edges: [Edge]()) 90 | } 91 | 92 | /** 93 | Create a `Graph` that determines ``GraphNode/id``s for new `NodeValue`s via the given closure 94 | */ 95 | init(valuesByID: Dictionary, 96 | edges: some Sequence<(NodeID, NodeID)>) 97 | { 98 | let actualEdges = edges.map { Edge(from: $0.0, to: $0.1) } 99 | 100 | self.init(valuesByID: valuesByID, edges: actualEdges) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Code/Graph+Algorithms/Graph+TransitiveReduction.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension Graph 4 | { 5 | /** 6 | The [minumum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction) of an **acyclic** `Graph` 7 | 8 | 🛑 This only works on acyclic graphs and might even hang or crash on cyclic ones! 9 | */ 10 | func filteredTransitiveReduction() -> Self 11 | { 12 | var minimumEquivalentGraph = self 13 | minimumEquivalentGraph.filterTransitiveReduction() 14 | return minimumEquivalentGraph 15 | } 16 | 17 | /** 18 | Filter an **acyclic** `Graph` down to its [minumum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction) 19 | 20 | 🛑 This only works on acyclic graphs and might even hang or crash on cyclic ones! 21 | */ 22 | mutating func filterTransitiveReduction() 23 | { 24 | filterEdges(findTransitiveReductionEdges()) 25 | } 26 | 27 | /** 28 | Edges that are *not* in the [minumum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction) of an **acyclic** `Graph` 29 | 30 | 🛑 This only works on acyclic graphs and might even hang or crash on cyclic ones! 31 | */ 32 | func findTransitiveReductionEdges() -> EdgeIDs 33 | { 34 | var idsOfTransitiveEdges = EdgeIDs() // or "shortcuts" of longer paths; or "implied" edges 35 | 36 | var consideredAncestorsHash = [NodeID: NodeIDs]() 37 | 38 | for sourceNode in sources 39 | { 40 | idsOfTransitiveEdges += findTransitiveEdges(around: sourceNode, 41 | reachedAncestors: [], 42 | consideredAncestorsHash: &consideredAncestorsHash) 43 | } 44 | 45 | let idsOfAllEdges = EdgeIDs(edgeIDs) 46 | 47 | return idsOfAllEdges - idsOfTransitiveEdges 48 | } 49 | 50 | private func findTransitiveEdges(around node: Node, 51 | reachedAncestors: NodeIDs, 52 | consideredAncestorsHash: inout [NodeID: NodeIDs]) -> EdgeIDs 53 | { 54 | // to make this not hang in cycles it might be enough to just ensure that node is not in reachedAncestors ... 55 | 56 | let consideredAncestors = consideredAncestorsHash[node.id, default: NodeIDs()] 57 | let ancestorsToConsider = reachedAncestors - consideredAncestors 58 | 59 | if !reachedAncestors.isEmpty && ancestorsToConsider.isEmpty 60 | { 61 | // found shortcut edge on a path we've already traversed, so we reached no new ancestors 62 | return [] 63 | } 64 | 65 | consideredAncestorsHash[node.id, default: NodeIDs()] += ancestorsToConsider 66 | 67 | var idsOfTransitiveEdges = EdgeIDs() 68 | 69 | // base case: add edges from all reached ancestors to all reachable neighbours of node 70 | 71 | let descendants = node.descendantIDs 72 | 73 | for descendant in descendants 74 | { 75 | for ancestor in ancestorsToConsider 76 | { 77 | let edgeID = Edge.ID(ancestor, descendant) 78 | 79 | if contains(edgeID) 80 | { 81 | idsOfTransitiveEdges += edgeID 82 | } 83 | } 84 | } 85 | 86 | // recursive calls on descendants 87 | 88 | for descendantID in descendants 89 | { 90 | guard let descendant = self.node(with: descendantID) else { continue } 91 | 92 | idsOfTransitiveEdges += findTransitiveEdges(around: descendant, 93 | reachedAncestors: ancestorsToConsider + node.id, 94 | consideredAncestorsHash: &consideredAncestorsHash) 95 | } 96 | 97 | return idsOfTransitiveEdges 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Code/Graph+CreateAndAccess/Graph+EdgeAccess.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension Graph 4 | { 5 | /** 6 | Removes the corresponding ``GraphEdge``, see ``Graph/removeEdge(with:)`` 7 | */ 8 | @discardableResult 9 | mutating func removeEdge(from originID: NodeID, 10 | to destinationID: NodeID) -> Edge? 11 | { 12 | removeEdge(with: .init(originID, destinationID)) 13 | } 14 | 15 | /** 16 | Removes the ``GraphEdge`` with the given ID, also removing it from node neighbour caches 17 | */ 18 | @discardableResult 19 | mutating func removeEdge(with id: Edge.ID) -> Edge? 20 | { 21 | // remove from node caches 22 | nodesByID[id.originID]?.descendantIDs -= id.destinationID 23 | nodesByID[id.destinationID]?.ancestorIDs -= id.originID 24 | 25 | // remove edge itself 26 | return edgesByID.removeValue(forKey: id) 27 | } 28 | 29 | /** 30 | Add weight to the existing edge or create the edge with that weight 31 | 32 | - Returns: The new weight of the edge with the given ID 33 | */ 34 | @discardableResult 35 | mutating func add(_ weight: EdgeWeight, 36 | toEdgeFrom originID: NodeID, 37 | to destinationID: NodeID) -> EdgeWeight 38 | { 39 | add(weight, toEdgeWith: .init(originID, destinationID)) 40 | } 41 | 42 | /** 43 | Add weight to the existing edge or create the edge with that weight 44 | 45 | - Returns: The new weight of the edge with the given ID 46 | */ 47 | @discardableResult 48 | mutating func add(_ weight: EdgeWeight, toEdgeWith id: Edge.ID) -> EdgeWeight 49 | { 50 | if let existingWeight = edgesByID[id]?.weight 51 | { 52 | edgesByID[id]?.weight += weight 53 | 54 | return existingWeight + weight 55 | } 56 | else 57 | { 58 | insertEdge(from: id.originID, 59 | to: id.destinationID, 60 | weight: weight) 61 | 62 | return weight 63 | } 64 | } 65 | 66 | @discardableResult 67 | mutating func insertEdge(from originID: NodeID, 68 | to destinationID: NodeID, 69 | weight: EdgeWeight = 1) -> Edge 70 | { 71 | let edge = Edge(from: originID, to: destinationID, weight: weight) 72 | insert(edge) 73 | return edge 74 | } 75 | 76 | mutating func insert(_ edge: Edge) 77 | { 78 | if !contains(edge.id) 79 | { 80 | // update node caches because edge does not exist yet 81 | nodesByID[edge.originID]?.descendantIDs += edge.destinationID 82 | nodesByID[edge.destinationID]?.ancestorIDs += edge.originID 83 | } 84 | 85 | edgesByID.insert(edge) 86 | } 87 | 88 | /** 89 | The ``GraphEdge`` between the corresponding nodes if it exists, otherwise `nil` 90 | */ 91 | func edge(from originID: NodeID, to destinationID: NodeID) -> Edge? 92 | { 93 | edge(with: .init(originID, destinationID)) 94 | } 95 | 96 | /** 97 | The ``GraphEdge`` with the given ID if the edge exists, otherwise `nil` 98 | */ 99 | func edge(with id: Edge.ID) -> Edge? 100 | { 101 | edgesByID[id] 102 | } 103 | 104 | func containsEdge(from originID: NodeID, 105 | to destinationID: NodeID) -> Bool 106 | { 107 | contains(.init(originID, destinationID)) 108 | } 109 | 110 | /** 111 | Whether the `Graph` contains a ``GraphEdge`` with the given ``GraphEdge/id-swift.property`` 112 | */ 113 | func contains(_ edgeID: Edge.ID) -> Bool 114 | { 115 | edgesByID.keys.contains(edgeID) 116 | } 117 | 118 | /** 119 | All ``GraphEdge``s of the `Graph` 120 | */ 121 | var edges: some Collection 122 | { 123 | edgesByID.values 124 | } 125 | 126 | /** 127 | All ``GraphEdge/id-swift.property``s of the ``GraphEdge``s of the `Graph` 128 | */ 129 | var edgeIDs: some Collection 130 | { 131 | edgesByID.keys 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/Algorithms/CondensationGraphTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class CondensationGraphTests: XCTestCase { 5 | 6 | func testGraphWithoutEdges() { 7 | let graph = TestGraph(values: [1, 2, 3]) 8 | 9 | let condensationGraph = graph.makeCondensationGraph() 10 | 11 | let expectedCondensationGraph = TestGraph.CondensationGraph( 12 | values: [ 13 | .init(nodeIDs: [1]), 14 | .init(nodeIDs: [2]), 15 | .init(nodeIDs: [3]) 16 | ] 17 | ) 18 | 19 | XCTAssertEqual(condensationGraph, expectedCondensationGraph) 20 | } 21 | 22 | func testGraphWithoutCycles() { 23 | let graph = TestGraph(values: [1, 2, 3, 4], 24 | edges: [(1, 2), (2, 3), (3, 4), (1, 3), (2, 4)]) 25 | 26 | let condensationGraph = graph.makeCondensationGraph() 27 | 28 | let expectedEdges: [(Set, Set)] = 29 | [ 30 | ([1], [2]), ([2], [3]), ([3], [4]), ([1], [3]), ([2], [4]) 31 | ] 32 | 33 | let expectedCondensationGraph = TestGraph.CondensationGraph( 34 | values: [ 35 | .init(nodeIDs: [1]), 36 | .init(nodeIDs: [2]), 37 | .init(nodeIDs: [3]), 38 | .init(nodeIDs: [4]) 39 | ], 40 | edges: expectedEdges 41 | ) 42 | 43 | XCTAssertEqual(condensationGraph, expectedCondensationGraph) 44 | } 45 | 46 | func testGraphMadeOfOneCycle() { 47 | let graph = TestGraph(values: [1, 2, 3, 4], 48 | edges: [(1, 2), (2, 3), (3, 4), (4, 1)]) 49 | 50 | let condensationGraph = graph.makeCondensationGraph() 51 | 52 | let expectedCondensationGraph = TestGraph.CondensationGraph( 53 | values: [.init(nodeIDs: [1, 2, 3, 4])] 54 | ) 55 | 56 | XCTAssertEqual(condensationGraph, expectedCondensationGraph) 57 | } 58 | 59 | func testGraphContainingOneCycle() { 60 | let graph = TestGraph(values: [1, 2, 3, 4], 61 | edges: [(1, 2), (2, 3), (3, 1), (3, 4)]) 62 | 63 | let condensationGraph = graph.makeCondensationGraph() 64 | 65 | let expectedEdges: [(Set, Set)] = [([1, 2, 3], [4])] 66 | 67 | let expectedCondensationGraph = TestGraph.CondensationGraph( 68 | values: [.init(nodeIDs: [1, 2, 3]), .init(nodeIDs: [4])], 69 | edges: expectedEdges 70 | ) 71 | 72 | XCTAssertEqual(condensationGraph, expectedCondensationGraph) 73 | } 74 | 75 | func testGraphContainingTwoCycles() { 76 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 77 | edges: [(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (6, 4)]) 78 | 79 | let condensationGraph = graph.makeCondensationGraph() 80 | 81 | let expectedEdges: [(Set, Set)] = [([1, 2, 3], [4, 5, 6])] 82 | 83 | let expectedCondensationGraph = TestGraph.CondensationGraph( 84 | values: [.init(nodeIDs: [1, 2, 3]), .init(nodeIDs: [4, 5, 6])], 85 | edges: expectedEdges 86 | ) 87 | 88 | XCTAssertEqual(condensationGraph, expectedCondensationGraph) 89 | } 90 | 91 | func testGraphContainingTwoComponents() { 92 | let graph = TestGraph(values: [1, 2, 3, 4, 5, 6], 93 | edges: [(1, 2), (2, 3), (1, 3), (4, 5), (5, 6), (6, 4)]) 94 | 95 | let condensationGraph = graph.makeCondensationGraph() 96 | 97 | let expectedEdges: [(Set, Set)] = [([1], [2]), ([2], [3]), ([1], [3])] 98 | 99 | let expectedCondensationGraph = TestGraph.CondensationGraph( 100 | values: [ 101 | .init(nodeIDs: [1]), 102 | .init(nodeIDs: [2]), 103 | .init(nodeIDs: [3]), 104 | .init(nodeIDs: [4, 5, 6]) 105 | ], 106 | edges: expectedEdges 107 | ) 108 | 109 | XCTAssertEqual(condensationGraph, expectedCondensationGraph) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Code/Graph+Algorithms/Graph+EssentialEdges.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension Graph 4 | { 5 | /** 6 | Graph with only the edges of the transitive reduction of the condensation graph 7 | 8 | Note that this will not remove any edges that are part of cycles (i.e. part of strongly connected components), since only considers edges of the condensation graph can be "non-essential". This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are "non-essential". We recommend dealing with cycles independently of using this function. 9 | */ 10 | func filteredEssentialEdges() -> Self 11 | { 12 | var result = self 13 | result.filterEssentialEdges() 14 | return result 15 | } 16 | 17 | /** 18 | Remove edges that are not in the transitive reduction of the condensation graph 19 | 20 | Note that this will not remove any edges that are part of cycles (i.e. part of strongly connected components), since only considers edges of the condensation graph can be "non-essential". This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are "non-essential". We recommend dealing with cycles independently of using this function. 21 | */ 22 | mutating func filterEssentialEdges() 23 | { 24 | filterEdges(findEssentialEdges()) 25 | } 26 | 27 | /** 28 | Find edges that are in the minimum equivalent graph of the condensation graph 29 | 30 | Note that this includes all edges that are part of cycles (i.e. part of strongly connected components), since only edges of the condensation graph can be "non-essential". This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are "non-essential". We recommend dealing with cycles independently of using this function. 31 | */ 32 | func findEssentialEdges() -> EdgeIDs 33 | { 34 | var idsOfEssentialEdges = EdgeIDs() 35 | 36 | // make condensation graph 37 | let condensationGraph = makeCondensationGraph() 38 | 39 | // remember in which condensation node each original node is contained 40 | var condensationNodeIDByNodeID = [NodeID: StronglyConnectedComponent.ID]() 41 | 42 | for condensationNode in condensationGraph.nodes 43 | { 44 | for node in condensationNode.value.nodeIDs 45 | { 46 | condensationNodeIDByNodeID[node] = condensationNode.id 47 | } 48 | } 49 | 50 | // make minimum equivalent condensation graph 51 | let minimumCondensationGraph = condensationGraph.filteredTransitiveReduction() 52 | 53 | // for each original edge in the component graph ... 54 | for edge in edges 55 | { 56 | guard let originCondensationNodeID = condensationNodeIDByNodeID[edge.originID], 57 | let destinationCondensationNodeID = condensationNodeIDByNodeID[edge.destinationID] 58 | else 59 | { 60 | log(error: "Nodes don't have their condensation node IDs set (but must have at this point)") 61 | continue 62 | } 63 | 64 | // add this edge if it is within the same condensation node (within a strongly connected component and thereby within a cycle) 65 | 66 | if originCondensationNodeID == destinationCondensationNodeID 67 | { 68 | idsOfEssentialEdges += edge.id 69 | continue 70 | } 71 | 72 | // the non-cyclic edge is essential if its equivalent is in the minimum equivalent condensation graph 73 | 74 | let condensationEdgeID = CondensationEdge.ID(originCondensationNodeID, 75 | destinationCondensationNodeID) 76 | 77 | let edgeIsEssential = minimumCondensationGraph.contains(condensationEdgeID) 78 | 79 | if edgeIsEssential 80 | { 81 | idsOfEssentialEdges += edge.id 82 | } 83 | } 84 | 85 | return idsOfEssentialEdges 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Code/Graph+Algorithms/Graph+StronglyConnectedComponents.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension Graph 4 | { 5 | /** 6 | Find the [strongly connected components](https://en.wikipedia.org/wiki/Strongly_connected_component) of the `Graph` 7 | 8 | - Returns: Multiple sets of node IDs, each set representing a strongly connected components of the graph 9 | */ 10 | func findStronglyConnectedComponents() -> Set 11 | { 12 | var resultingSCCs = Set() 13 | 14 | var index = 0 15 | var stack = [NodeID]() 16 | var markings = [NodeID: Marking]() 17 | 18 | for node in nodeIDs 19 | { 20 | if markings[node] == nil 21 | { 22 | findSCCsRecursively(node: node, 23 | index: &index, 24 | stack: &stack, 25 | markings: &markings) { resultingSCCs += $0 } 26 | } 27 | } 28 | 29 | return resultingSCCs 30 | } 31 | 32 | @discardableResult 33 | private func findSCCsRecursively(node: NodeID, 34 | index: inout Int, 35 | stack: inout [NodeID], 36 | markings: inout [NodeID: Marking], 37 | handleNewSCC: (NodeIDs) -> Void) -> Marking 38 | { 39 | // Set the depth index for node to the smallest unused index 40 | assert(markings[node] == nil, "there shouldn't be a marking value for this node yet") 41 | let nodeMarking = Marking(index: index, lowLink: index, isOnStack: true) 42 | markings[node] = nodeMarking 43 | index += 1 44 | stack.append(node) 45 | 46 | // Consider descendants of node 47 | let nodeDescendants = self.node(with: node)?.descendantIDs ?? [] 48 | 49 | for descendant in nodeDescendants 50 | { 51 | if let descendantMarking = markings[descendant] 52 | { 53 | // If descendant is not on stack, then edge (from node to descendant) is pointing to an SCC already found and must be ignored 54 | if descendantMarking.isOnStack 55 | { 56 | // Successor "descendant" is in stack and hence in the current SCC 57 | nodeMarking.lowLink = min(nodeMarking.lowLink, descendantMarking.index) 58 | } 59 | } 60 | else // if descendant index is undefined then 61 | { 62 | // Successor "descendant" has not yet been visited; recurse on it 63 | let descendantMarking = findSCCsRecursively(node: descendant, 64 | index: &index, 65 | stack: &stack, 66 | markings: &markings, 67 | handleNewSCC: handleNewSCC) 68 | 69 | nodeMarking.lowLink = min(nodeMarking.lowLink, descendantMarking.lowLink) 70 | } 71 | } 72 | 73 | // If node is a root node, pop the stack and generate an SCC 74 | if nodeMarking.lowLink == nodeMarking.index 75 | { 76 | var newSCC = NodeIDs() 77 | 78 | while !stack.isEmpty 79 | { 80 | let sccNode = stack.removeLast() 81 | 82 | guard markings[sccNode] != nil else 83 | { 84 | fatalError("node that is on the stack should have a markings object") 85 | } 86 | 87 | markings[sccNode]?.isOnStack = false 88 | newSCC += sccNode 89 | 90 | if node == sccNode { break } 91 | } 92 | 93 | handleNewSCC(newSCC) 94 | } 95 | 96 | return nodeMarking 97 | } 98 | } 99 | 100 | private class Marking 101 | { 102 | init(index: Int, lowLink: Int, isOnStack: Bool) 103 | { 104 | self.index = index 105 | self.lowLink = lowLink 106 | self.isOnStack = isOnStack 107 | } 108 | 109 | var index: Int 110 | var lowLink: Int 111 | var isOnStack: Bool 112 | } 113 | -------------------------------------------------------------------------------- /Tests/EdgeTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftNodes 2 | import XCTest 3 | 4 | class EdgeTests: XCTestCase { 5 | 6 | func testThatCodeInREADMECompiles() { 7 | var graph: Graph = [1, 2, 3] 8 | 9 | graph.insertEdge(from: 1, to: 2) 10 | let hasEdgeFrom1To2 = graph.containsEdge(from: 1, to: 2) 11 | let edge = graph.edge(from: 1, to: 2) 12 | graph.removeEdge(from: 1, to: 2) 13 | } 14 | 15 | func testInitializeWithValuesAndEdges() throws { 16 | // with values as node IDs 17 | let graph = TestGraph(values: [-7, 0, 5, 42], edges: [(-7, 0)]) 18 | 19 | XCTAssertNotNil(graph.edge(from: -7, to: 0)) 20 | XCTAssertEqual(graph.node(with: -7)?.descendantIDs.contains(0), true) 21 | XCTAssertEqual(graph.node(with: 0)?.ancestorIDs.contains(-7), true) 22 | 23 | // with identifiable values 24 | struct IdentifiableValue: Identifiable { let id: Int } 25 | 26 | let values: [IdentifiableValue] = [.init(id: -7), .init(id: 0), .init(id: 5), .init(id: 42)] 27 | let graph2 = IdentifiableValuesGraph(values: values, edges: [(-7, 0)]) 28 | 29 | XCTAssertNotNil(graph2.edge(from: -7, to: 0)) 30 | XCTAssertEqual(graph2.node(with: -7)?.descendantIDs.contains(0), true) 31 | XCTAssertEqual(graph2.node(with: 0)?.ancestorIDs.contains(-7), true) 32 | } 33 | 34 | func testAddingEdges() throws { 35 | var graph = TestGraph(values: [1, 2]) 36 | graph.insertEdge(from: 1, to: 2) 37 | 38 | XCTAssertNil(graph.edge(from: 1, to: 3)) 39 | XCTAssertNil(graph.edge(from: 0, to: 2)) 40 | XCTAssertNotNil(graph.edge(from: 1, to: 2)) 41 | XCTAssertEqual(graph.edge(from: 1, to: 2)?.weight, 1) 42 | } 43 | 44 | func testAddingEdgeWithBigCount() throws { 45 | var graph = TestGraph(values: [1, 2]) 46 | 47 | graph.insertEdge(from: 1, to: 2, weight: 42) 48 | XCTAssertEqual(graph.edge(from: 1, to: 2)?.weight, 42) 49 | 50 | graph.add(58, toEdgeWith: .init(1, 2)) 51 | XCTAssertEqual(graph.edge(from: 1, to: 2)?.weight, 100) 52 | } 53 | 54 | func testEdgesAreDirected() throws { 55 | var graph = TestGraph(values: [1, 2]) 56 | graph.insertEdge(from: 1, to: 2) 57 | 58 | XCTAssertNotNil(graph.edge(from: 1, to: 2)) 59 | XCTAssertNil(graph.edge(from: 2, to: 1)) 60 | } 61 | 62 | func testThreeWaysToRemoveAnEdge() { 63 | var graph = TestGraph(values: [5, 3]) 64 | 65 | let edge = graph.insertEdge(from: 5, to: 3) 66 | XCTAssertNotNil(graph.edge(from: 5, to: 3)) 67 | graph.removeEdge(with: edge.id) 68 | XCTAssertNil(graph.edge(from: 5, to: 3)) 69 | 70 | graph.insertEdge(from: 5, to: 3) 71 | XCTAssertNotNil(graph.edge(from: 5, to: 3)) 72 | graph.removeEdge(with: .init(5, 3)) 73 | XCTAssertNil(graph.edge(from: 5, to: 3)) 74 | 75 | graph.insertEdge(from: 5, to: 3) 76 | XCTAssertNotNil(graph.edge(from: 5, to: 3)) 77 | graph.removeEdge(from: 5, to: 3) 78 | XCTAssertNil(graph.edge(from: 5, to: 3)) 79 | } 80 | 81 | func testInsertingConnectingAndDisconnectingValues() throws { 82 | var graph = TestGraph() 83 | XCTAssertNil(graph.node(with: 1)) 84 | XCTAssertNil(graph.value(for: 1)) 85 | 86 | let node1 = graph.insert(1) 87 | XCTAssertEqual(graph.value(for: 1), 1) 88 | XCTAssertEqual(graph.node(with: 1)?.id, node1.id) 89 | XCTAssertEqual(graph.insert(1).id, node1.id) 90 | XCTAssertNil(graph.edge(from: 1, to: 2)) 91 | 92 | XCTAssertEqual(node1.id, 1) 93 | XCTAssertEqual(node1.value, 1) 94 | 95 | let node2 = graph.insert(2) 96 | XCTAssertNil(graph.edge(from: 1, to: 2)) 97 | XCTAssertNil(graph.edge(from: node1.id, to: node2.id)) 98 | 99 | let edge12 = graph.insertEdge(from: node1.id, to: node2.id) 100 | XCTAssertNotNil(graph.edge(from: 1, to: 2)) 101 | XCTAssertNotNil(graph.edge(from: node1.id, to: node2.id)) 102 | 103 | XCTAssertEqual(edge12.weight, 1) 104 | XCTAssertEqual(2, graph.add(1, toEdgeWith: .init(1, 2))) 105 | XCTAssertEqual(graph.edge(from: node1.id, to: node2.id)?.weight, 2) 106 | XCTAssertEqual(edge12.originID, node1.id) 107 | XCTAssertEqual(edge12.destinationID, node2.id) 108 | 109 | guard let updatedNode1 = graph.node(with: node1.id) else 110 | { 111 | throw "There should still exist a node for the id of node 1" 112 | } 113 | 114 | XCTAssertFalse(updatedNode1.isSink) 115 | XCTAssert(updatedNode1.descendantIDs.contains(node2.id)) 116 | XCTAssert(updatedNode1.isSource) 117 | 118 | guard let updatedNode2 = graph.node(with: node2.id) else 119 | { 120 | throw "There should still exist a node for the id of node 2" 121 | } 122 | 123 | XCTAssertFalse(updatedNode2.isSource) 124 | XCTAssert(updatedNode2.ancestorIDs.contains(node1.id)) 125 | XCTAssert(updatedNode2.isSink) 126 | 127 | graph.removeEdge(with: edge12.id) 128 | XCTAssertNil(graph.edge(from: 1, to: 2)) 129 | XCTAssertNil(graph.edge(from: node1.id, to: node2.id)) 130 | 131 | guard let finalNode1 = graph.node(with: node1.id), 132 | let finalNode2 = graph.node(with: node2.id) else 133 | { 134 | throw "There should still exist node for the ids of nodes 1 and 2" 135 | } 136 | 137 | XCTAssert(finalNode1.ancestorIDs.isEmpty) 138 | XCTAssert(finalNode1.descendantIDs.isEmpty) 139 | XCTAssert(finalNode1.isSource) 140 | XCTAssert(finalNode1.isSink) 141 | XCTAssert(finalNode2.ancestorIDs.isEmpty) 142 | XCTAssert(finalNode2.descendantIDs.isEmpty) 143 | XCTAssert(finalNode2.isSource) 144 | XCTAssert(finalNode2.isSink) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftNodes 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftNodes%2Fbadge%3Ftype%3Dswift-versions&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftNodes)  [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftNodes%2Fbadge%3Ftype%3Dplatforms&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftNodes)  [![](https://img.shields.io/badge/Documentation-DocC-blue.svg?style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftNodes/documentation)  [![](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat-square)](LICENSE) 4 | 5 | 👩🏻‍🚀 *This project [is still a tad experimental](#development-status). Contributors and pioneers welcome!* 6 | 7 | ## What? 8 | 9 | SwiftNodes offers a concurrency safe [graph data structure](https://en.wikipedia.org/wiki/Graph_(abstract_data_type)) together with graph algorithms. A graph stores values in identifiable nodes which can be connected via edges. 10 | 11 | ### Contents 12 | 13 | * [Why?](#Why) 14 | * [How?](#How) 15 | * [Included Algorithms](#Included-Algorithms) 16 | * [Architecture](#Architecture) 17 | * [Development Status](#Development-Status) 18 | * [Roadmap](#Roadmap) 19 | 20 | ## Why? 21 | 22 | Graphs may be the most fundamental mathematical concept besides numbers. They have wide applications in problem solving, data analysis and visualization. And although such data structures fit well with the language, graph implementations in Swift are lacking – in particular, comprehensive graph algorithm libraries. 23 | 24 | SwiftNodes and its included algorithms were extracted from [Codeface](https://codeface.io). But SwiftNodes is general enough to serve other applications as well – and extensible enough for more algorithms to be added. 25 | 26 | ### Design Goals 27 | 28 | * Usability, safety, extensibility and maintainability – which also imply simplicity. 29 | * In particular, the API is supposed to feel familiar and fit well with official Swift data structures. So one question that guides its design is: What would Apple do? 30 | 31 | We put the above qualities over performance. But that doesn't mean we neccessarily end up with suboptimal performance. The main compromise SwiftNodes involves is that nodes are value types and can not be referenced, so they must be hashed. But that doesn't change the average case complexity and, in the future, we might even be able to avoid that hashing in essential use cases by exploiting array indices. 32 | 33 | ## How? 34 | 35 | This section is a tutorial and touches only parts of the SwiftNodes API. We recommend exploring the [DocC reference](https://swiftpackageindex.com/codeface-io/SwiftNodes/documentation), [unit tests](Tests) and [production code](Code). The code in particular is actually small, meaninfully organized and easy to grasp. 36 | 37 | ### Understand and Initialize Graphs 38 | 39 | Let's look at our first graph: 40 | 41 | ```swift 42 | let graph = Graph(values: [1, 2, 3], // values serve as node IDs 43 | edges: [(1, 2), (2, 3), (1, 3)]) 44 | ``` 45 | 46 | `Graph` is generic over three types: `Graph`. Much like a `Dictionary` stores values for unique keys, a `Graph` stores values for unique node IDs. Actually, the `Graph` stores the values *within* its nodes which we identify by their IDs. Unlike a `Dictionary`, a `Graph` also allows to connect its unique "value locations", which are its node IDs. Those connections are the graph's edges, and each of them has a numeric weight. 47 | 48 | So, in the above example, `Graph` stores `Int` values for `Int` node IDs and connects these node IDs (nodes) through edges that each have a `Double` weight. We provided the values and specified the edges. But where do the actual `Int` node IDs and `Double` edge weights come from? In both regards, the above initializer is a rather convenient one that infers things: 49 | 50 | 1. When values and node IDs are of the same (and thereby hashable) type, SwiftNodes infers that we actually don't need distinct node IDs, so each unique value also serves as the ID of its own node. 51 | 2. When we don't want to use or specify edge weights, we can specify edges by just the node IDs they connect, and SwiftNodes will create the corresponding edges with a default weight of 1. 52 | 53 | We could explicitly provide distinct node IDs, for example of type `String`: 54 | 55 | ```swift 56 | let graph = Graph(valuesByID: ["a": 1, "b": 2, "c": 3], 57 | edges: [("a", "b"), ("b", "c"), ("a", "c")]) 58 | ``` 59 | 60 | And if we want to add all edges later, we can create graphs without edges via array- and dictionary literals: 61 | 62 | ```swift 63 | let graph = Graph = [1, 2, 3] // values serve as node IDs 64 | _ = Graph = ["a": 1, "b": 2, "c": 3] 65 | ``` 66 | 67 | In two of the above examples (1st and 3rd graph), SwiftNodes can infer node IDs because node values are of the same type. There is one other type of value with which we don't need to provide node IDs: node values that are `Identifiable` by the same type of ID as nodes are, i.e. `NodeID == NodeValue.ID`. In that case, each value's unique ID also serves as the ID of the value's node. This does not work with array literals but with initializers: 68 | 69 | ```swift 70 | struct IdentifiableValue: Identifiable { let id = UUID() } 71 | typealias IVGraph = Graph 72 | 73 | let values = [IdentifiableValue(), IdentifiableValue(), IdentifiableValue()] 74 | let ids = values.map { $0.id } 75 | let graph = IVGraph(values: values, // value IDs serve as node IDs 76 | edges: [(ids[0], ids[1]), (ids[1], ids[2]), (ids[0], ids[2])]) 77 | ``` 78 | 79 | For all initializer variants see [Graph.swift](Code/Graph/Graph.swift) and [Graph+ConvenientInitializers.swift](Code/Graph+CreateAndAccess/Graph+ConvenientInitializers.swift). 80 | 81 | ### Values 82 | 83 | Just like with a `Dictionary`, you can read, write and delete values via subscripts and via functions: 84 | 85 | ```swift 86 | var graph = Graph() 87 | 88 | graph["a"] = 1 89 | let valueA = graph["a"] 90 | graph["a"] = nil 91 | 92 | graph.update(2, for: "b") // returns the updated/created `Node` as `@discardableResult` 93 | let valueB = graph.value(for: "b") 94 | graph.removeValue(for: "b") // returns the removed `NodeValue?` as `@discardableResult` 95 | 96 | let allValues = graph.values // returns `some Collection` 97 | ``` 98 | 99 | And just like with the graph initializers, you don't need to provide node IDs if either the values themselves or their IDs can serve as node IDs. Here, values are identical to their node IDs: 100 | 101 | ```swift 102 | var graph = Graph() 103 | 104 | graph.insert(1) // returns the updated/created `Node` as `@discardableResult` 105 | graph.remove(1) // returns the removed `Node?` as `@discardableResult` 106 | ``` 107 | 108 | ### Edges 109 | 110 | Each edge is identified by the two nodes it connects, thus an edge ID is a combination of two node IDs. Edges are also directed, which means they point in a direction, from one node to another, which we might call "origin-" and "destination node" (or similar). Directed edges are the most general form. If a client or algorithm works with "undirected" graphs, that simply means it doesn't care about edge direction. 111 | 112 | The three basic operations are inserting, reading and removing edges: 113 | 114 | ```swift 115 | var graph: Graph = [1, 2, 3] // values serve as node IDs 116 | 117 | graph.insertEdge(from: 1, to: 2) // returns the edge as `@discardableResult` 118 | let edge = graph.edge(from: 1, to: 2) // the optional edge itself 119 | let hasEdge = graph.containsEdge(from: 1, to: 2) // whether the edge exists 120 | graph.removeEdge(from: 1, to: 2) // returns the optional edge as `@discardableResult` 121 | ``` 122 | 123 | Of course, `Graph` also has properties providing all edges, all edges by their IDs and all edge IDs. And it has ways to initialize and mutate edge weights. For the whole edge API, see [Graph.swift](Code/Graph/Graph.swift) and [Graph+EdgeAccess.swift](Code/Graph+CreateAndAccess/Graph+EdgeAccess.swift). 124 | 125 | ### Nodes 126 | 127 | Nodes are basically identifiable value containers that can be connected by edges. But aside from values they also store the IDs of neighbouring nodes. This redundant storage (cache) is kept up to date by the `Graph` and makes graph traversal a bit more performant and convenient. Any given `node` has these cache-based properties: 128 | 129 | ```swift 130 | node.descendantIDs // IDs of all nodes to which there is an edge from node 131 | node.ancestorIDs // IDs of all nodes from which there is an edge to node 132 | node.neighbourIDs // all descendant- and ancestor IDs 133 | node.isSink // whether node has no descendants 134 | node.isSource // whether node has no ancestors 135 | ``` 136 | 137 | For the whole node API, see [Graph.swift](Code/Graph/Graph.swift) and [Graph+NodeAccess.swift](Code/Graph+CreateAndAccess/Graph+NodeAccess.swift). 138 | 139 | ### Marking Nodes 140 | 141 | Many graph algorithms do associate little intermediate results with individual nodes. The literature often refers to this as "marking" a node. The most prominent example is marking a node as visited while traversing a potentially cyclic graph. Some algorithms write multiple different markings to nodes. 142 | 143 | When we made SwiftNodes concurrency safe (to play well with the new Swift concurrency features), we removed the possibility to mark nodes directly, as that had lost its potential for performance optimization. See how the [included algorithms](Code/Graph+Algorithms) now use hashing to associate markings with nodes. 144 | 145 | ### Value Semantics and Concurrency 146 | 147 | Like official Swift data structures, `Graph` is a pure `struct` and inherits the benefits of value types: 148 | 149 | * You decide on mutability by using `var` or `let`. 150 | * You can use a `Graph` as a `@State` or `@Published` variable with SwiftUI. 151 | * You can use property observers like `didSet` to observe changes in a `Graph`. 152 | * You can easily copy a whole `Graph`. 153 | 154 | Many algorithms produce a variant of a given graph. Rather than modifying the original graph, SwiftNodes suggests to copy it, and you can copy a `Graph` like any other value. 155 | 156 | A `Graph` is also `Sendable` **if** its value- and id type are. SwiftNodes is thereby ready for the strict concurrency safety of Swift 6. You can safely share `Sendable` `Graph` values between actors. Remember that, to declare a `Graph` property on a `Sendable` reference type, you need to make that property constant (use `let`). 157 | 158 | ## Included Algorithms 159 | 160 | SwiftNodes has begun to accumulate [some graph algorithms](Code/Graph+Algorithms). The following overview also links to Wikipedia articles that explain what the algorithms do. We recommend also exploring them in code. 161 | 162 | ### Filter and Map 163 | 164 | You can map graph values and filter graphs by values, edges and nodes. Of course, the filters keep edges and node neighbour caches consistent and produce proper **subgraphs**. 165 | 166 | ```swift 167 | let graph: Graph = [1, 2, 10, 20] 168 | 169 | let oneDigitGraph = graph.filtered { $0 < 10 } 170 | let stringGraph = graph.map { "\($0)" } 171 | ``` 172 | 173 | See all filters in [Graph+FilterAndMap.swift](Code/Graph+Algorithms/Graph+FilterAndMap.swift). 174 | 175 | ### Components 176 | 177 | `graph.findComponents()` returns multiple sets of node IDs which represent the [components](https://en.wikipedia.org/wiki/Component_(graph_theory)) of the `graph`. 178 | 179 | ### Strongly Connected Components 180 | 181 | `graph.findStronglyConnectedComponents()` returns multiple sets of node IDs which represent the [strongly connected components](https://en.wikipedia.org/wiki/Strongly_connected_component) of the `graph`. 182 | 183 | ### Condensation Graph 184 | 185 | `graph.makeCondensationGraph()` creates the [condensation graph](https://en.wikipedia.org/wiki/Strongly_connected_component) of the `graph`, which is the graph in which all [strongly connected components](https://en.wikipedia.org/wiki/Strongly_connected_component) of the original `graph` have been collapsed into single nodes, so the resulting condensation graph is acyclic. 186 | 187 | ### Transitive Reduction 188 | 189 | `graph.findTransitiveReductionEdges()` finds all edges of the [transitive reduction (the minimum equivalent graph)](https://en.wikipedia.org/wiki/Transitive_reduction) of the `graph`. You can also use `filterTransitiveReduction()` and `filteredTransitiveReduction()` to create a graph's [minimum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction). 190 | 191 | Right now, all this only works on acyclic graphs and might even hang or crash on cyclic ones. 192 | 193 | ### Essential Edges 194 | 195 | `graph.findEssentialEdges()` returns the IDs of all "essential" edges. You can also use `graph.filterEssentialEdges()` and `graph.filteredEssentialEdges()` to remove all "non-essential" edges from a `graph`. 196 | 197 | Edges are essential when they correspond to edges of the [MEG](https://en.wikipedia.org/wiki/Transitive_reduction) (the transitive reduction) of the [condensation graph](https://en.wikipedia.org/wiki/Strongly_connected_component). In simpler terms: Essential edges are either in cycles or they are essential to the reachability described by the graph – i.e. they cannot be removed without destroying the only path between some nodes. 198 | 199 | Note that only edges of the condensation graph can be non-essential and so edges in cycles (i.e. in strongly connected components) are all considered essential. This is because it's [algorithmically](https://en.wikipedia.org/wiki/Feedback_arc_set#Hardness) as well as conceptually hard to decide which edges in cycles are "non-essential". We recommend dealing with cycles independently of using this function. 200 | 201 | ### Ancestor Counts 202 | 203 | `graph.findNumberOfNodeAncestors()` returns a `Dictionary` containing the ancestor count for each node ID of the `graph`. The ancestor count is the number of all (recursive) ancestors of the node. Basically, it's the number of other nodes from which the node can be reached. 204 | 205 | This only works on acyclic graphs right now and might return incorrect results for nodes in cycles. 206 | 207 | Ancestor counts can serve as a proxy for [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting). 208 | 209 | ## Architecture 210 | 211 | Here is the architecture (composition and [essential](https://en.wikipedia.org/wiki/Transitive_reduction) dependencies) of the SwiftNodes code folder: 212 | 213 | ![](Documentation/architecture.png) 214 | 215 | The above image was created with [Codeface](https://codeface.io). 216 | 217 | ## Development Status 218 | 219 | From version/tag 0.1.0 on, SwiftNodes adheres to [semantic versioning](https://semver.org). So until it has reached 1.0.0, its API may still break frequently, and we express those breaks with minor version bumps. 220 | 221 | SwiftNodes is already being used in production, but [Codeface](https://codeface.io) is still its primary client. SwiftNodes will move to version 1.0.0 as soon as **either one** of these conditions is met: 222 | 223 | * Basic practicality and conceptual soundness have been validated by serving multiple real-world clients. 224 | * We feel it's mature enough (well rounded and stable API, comprehensive tests, complete documentation and solid achievement of design goals). 225 | 226 | ## Roadmap 227 | 228 | 1. Review, update and complete all documentation, including API comments 229 | 2. Review tests again and add more to cover the API comprehensively 230 | 3. Round out and add algorithms (starting with the needs of Codeface): 231 | 1. Make existing algorithms compatible with cycles (two algorithms are still not). meaning: don't hang or crash, maybe throw an error! 232 | 2. Move to version 1.0.0 if possible 233 | 3. Add general purpose graph traversal algorithms (BFT, DFT, compatible with potentially cyclic graphs) 234 | 4. Add better ways of topological sorting 235 | 5. Approximate the [minimum feedback arc set](https://en.wikipedia.org/wiki/Feedback_arc_set), so Codeface can guess "faulty" or unintended dependencies, i.e. the fewest dependencies that need to be cut in order to break all cycles. 236 | 4. Make Graph conform to Collection, Sequence, RandomAccessCollection, SetAlgebra, etc. … Look into conformances of used collections OrderedSet and OrderedDictionary … 237 | 5. Possibly optimize performance – but only based on measurements and only if measurements show that the optimization yields significant acceleration. Optimizing the algorithms might be more effective than optimizing the data structure itself. 238 | * What role can `@inlinable` play here? 239 | * What role can [`lazy`](https://developer.apple.com/documentation/swift/sequence/lazy) play here? 240 | --------------------------------------------------------------------------------