pax_global_header00006660000000000000000000000064144512616760014526gustar00rootroot0000000000000052 comment=c61dfea98c04bdb3e321ae0858a3057280585f04 graph-0.23.0/000077500000000000000000000000001445126167600127115ustar00rootroot00000000000000graph-0.23.0/.github/000077500000000000000000000000001445126167600142515ustar00rootroot00000000000000graph-0.23.0/.github/.gitkeep000066400000000000000000000000001445126167600156700ustar00rootroot00000000000000graph-0.23.0/.github/FUNDING.yml000066400000000000000000000000271445126167600160650ustar00rootroot00000000000000github: [dominikbraun] graph-0.23.0/.github/workflows/000077500000000000000000000000001445126167600163065ustar00rootroot00000000000000graph-0.23.0/.github/workflows/go.yml000066400000000000000000000014011445126167600174320ustar00rootroot00000000000000name: Go on: push: pull_request: jobs: test: name: Unit Tests runs-on: ubuntu-latest strategy: matrix: go: [ '1.18.x', '1.19.x', '1.20.x' ] steps: - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Get dependencies run: | go get -v -t -d ./... - name: Lint uses: golangci/golangci-lint-action@v3 with: version: v1.51.2 - name: Test run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... - name: Codecov uses: codecov/codecov-action@v2 graph-0.23.0/.gitignore000066400000000000000000000004251445126167600147020ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ .idea/ graph-0.23.0/.golangci.yml000066400000000000000000000004161445126167600152760ustar00rootroot00000000000000run: timeout: 5m linters: disable-all: true enable: - govet - errcheck - gosimple - ineffassign - staticcheck - typecheck - unused linters-settings: govet: enable-all: true disable: - stdmethods - fieldalignment graph-0.23.0/CHANGELOG.md000066400000000000000000000267121445126167600145320ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.23.0] - 2023-07-05 **Are you using graph? [Check out the graph user survey](https://forms.gle/MLKUZKMeCRxTfj4v9)** ### Added * Added the `AllPathsBetween` function for computing all paths between two vertices. ## [0.22.3] - 2023-06-14 ### Changed * Changed `StableTopologicalSort` to invoke the `less` function as few as possible, reducing comparisons. * Changed `CreatesCycle` to use an optimized path if the default in-memory store is being used. * Changed map allocations to use pre-defined memory sizes. ## [0.22.2] - 2023-06-06 ### Fixed * Fixed the major performance issues of `StableTopologicalSort`. ## [0.22.1] - 2023-06-05 ### Fixed * Fixed `TopologicalSort` to retain its original performance. ## [0.22.0] - 2023-05-24 ### Added * Added the `StableTopologicalSort` function for deterministic topological orderings. * Added the `VertexAttributes` functional option for setting an entire vertex attributes map. ## [0.21.0] - 2023-05-18 ### Added * Added the `BFSWithDepth` function for performing a BFS with depth information. ### Fixed * Fixed false positives of `ErrVertexHasEdges` when removing a vertex. ## [0.20.0] - 2023-05-01 **Release post: [graph Version 0.20 Is Out](https://dominikbraun.io/blog/graph-version-0.20-is-out/)** ### Added * Added the `Graph.AddVerticesFrom` method for adding all vertices from another graph. * Added the `Graph.AddEdgesFrom` method for adding all edges from another graph. * Added the `Graph.Edges` method for obtaining all edges as a slice. * Added the `Graph.UpdateEdge` method for updating the properties of an edge. * Added the `Store.UpdateEdge` method for updating the properties of an edge. * Added the `NewLike` function for creating a new graph that is "like" the given graph. * Added the `EdgeAttributes` functional option for setting an entire edge attributes map. ### Changed * Changed `Graph.Clone` to use the built-in in-memory store for storing vertices and edges for cloned graphs. ## [0.19.0] - 2023-04-23 ### Added * Added the `MinimumSpanningTree` function for finding a minimum spanning tree. * Added the `MaximumSpanningTree` function for finding a maximum spanning tree. ## [0.18.0] - 2023-04-16 ### Added * Added the `Graph.RemoveVertex` method for removing a vertex. * Added the `Store.RemoveVertex` method for removing a vertex. * Added the `ErrVertexHasEdges` error instance. * Added the `Union` function for combining two graphs into one. ## [0.17.0] - 2023-04-12 ### Added * Added the `draw.GraphAttributes` functional option for `draw.DOT` for rendering graph attributes. ### Changed * Changed the library's GoDoc documentation. ## [0.16.2] - 2023-03-27 ### Fixed * Fixed `ShortestPath` for an edge case. ## [0.16.1] - 2023-03-06 ### Fixed * Fixed `TransitiveReduction` not to incorrectly report cycles. ## [0.16.0] - 2023-03-01 **This release contains breaking changes of the public API (see "Changed").** ### Added * Added the `Store` interface, introducing support for custom storage implementations. * Added the `NewWithStore` function for explicitly initializing a graph with a `Store` instance. * Added the `EdgeData` functional option that can be used with `AddEdge`, introducing support for arbitrary data. * Added the `Data` field to `EdgeProperties` for retrieving data added using `EdgeData`. ### Changed * Changed `Order` to additionally return an error instance (breaking change). * Changed `Size` to additionally return an error instance (breaking change). ## [0.15.1] - 2023-01-18 ### Changed * Changed `ShortestPath` to return `ErrTargetNotReachable` if the target vertex is not reachable. ### Fixed * Fixed `ShortestPath` to return correct results for large unweighted graphs. ## [0.15.0] - 2022-11-25 ### Added * Added the `ErrVertexAlreadyExists` error instance. Use `errors.Is` to check for this instance. * Added the `ErrEdgeAlreadyExists` error instance. Use `errors.Is` to check for this instance. * Added the `ErrEdgeCreatesCycle` error instance. Use `errors.Is` to check for this instance. ### Changed * Changed `AddVertex` to return `ErrVertexAlreadyExists` if the vertex already exists. * Changed `VertexWithProperties` to return `ErrVertexNotFound` if the vertex doesn't exist. * Changed `AddEdge` to return `ErrVertexNotFound` if either vertex doesn't exist. * Changed `AddEdge` to return `ErrEdgeAlreadyExists` if the edge already exists. * Changed `AddEdge` to return `ErrEdgeCreatesCycle` if cycle prevention is active and the edge would create a cycle. * Changed `Edge` to return `ErrEdgeNotFound` if the edge doesn't exist. * Changed `RemoveEdge` to return the error instances returned by `Edge`. ## [0.14.0] - 2022-11-01 ### Added * Added the `ErrVertexNotFound` error instance. ### Changed * Changed `TopologicalSort` to fail at runtime when a cycle is detected. * Changed `TransitiveReduction` to return the transitive reduction as a new graph and fail at runtime when a cycle is detected. * Changed `Vertex` to return `ErrVertexNotFound` if the desired vertex couldn't be found. ## [0.13.0] - 2022-10-15 ### Added * Added the `VertexProperties` type for storing vertex-related properties. * Added the `VertexWithProperties` method for retrieving a vertex and its properties. * Added the `VertexWeight` functional option that can be used for `AddVertex`. * Added the `VertexAttribute` functional option that can be used for `AddVertex`. * Added support for rendering vertices with attributes using `draw.DOT`. ### Changed * Changed `AddVertex` to accept functional options. * Renamed `PermitCycles` to `PreventCycles`. This seems to be the price to pay if English isn't a library author's native language. ### Fixed * Fixed the behavior of `ShortestPath` when the target vertex is not reachable from one of the visited vertices. ## [0.12.0] - 2022-09-19 ### Added * Added the `PermitCycles` option to explicitly prevent the creation of cycles. ### Changed * Changed the `Acyclic` option to not implicitly impose cycle checks for operations like `AddEdge`. To prevent the creation of cycles, use `PermitCycles`. * Changed `TopologicalSort` to only work for graphs created with `PermitCycles`. This is temporary. * Changed `TransitiveReduction` to only work for graphs created with `PermitCycles`. This is temporary. ## [0.11.0] - 2022-09-15 ### Added * Added the `Order` method for retrieving the number of vertices in the graph. * Added the `Size` method for retrieving the number of edges in the graph. ### Changed * Changed the `graph` logo. * Changed an internal operation of `ShortestPath` from O(n) to O(log(n)) by implementing the priority queue as a binary heap. Note that the actual complexity might still be defined by `ShortestPath` itself. ### Fixed * Fixed `draw.DOT` to work correctly with vertices that contain special characters and whitespaces. ## [0.10.0] - 2022-09-09 ### Added * Added the `PredecessorMap` method for obtaining a map with all predecessors of each vertex. * Added the `RemoveEdge` method for removing the edge between two vertices. * Added the `Clone` method for retrieving a deep copy of the graph. * Added the `TopologicalSort` function for obtaining the topological order of the vertices in the graph. * Added the `TransitiveReduction` function for transforming the graph into its transitive reduction. ### Changed * Changed the `visit` function of `DFS` to accept a vertex hash instead of the vertex value (i.e. `K` instead of `T`). * Changed the `visit` function of `BFS` to accept a vertex hash instead of the vertex value (i.e. `K` instead of `T`). ### Removed * Removed the `Predecessors` function. Use `PredecessorMap` instead and look up the respective vertex. ## [0.9.0] - 2022-08-17 ### Added * Added the `Graph.AddVertex` method for adding a vertex. This replaces `Graph.Vertex`. * Added the `Graph.AddEdge` method for creating an edge. This replaces `Graph.Edge`. * Added the `Graph.Vertex` method for retrieving a vertex by its hash. This is not to be confused with the old `Graph.Vertex` function for adding vertices that got replaced with `Graph.AddVertex`. * Added the `Graph.Edge` method for retrieving an edge. This is not to be confused with the old `Graph.Edge` function for creating an edge that got replaced with `Graph.AddEdge`. * Added the `Graph.Predecessors` function for retrieving a vertex' predecessors. * Added the `DFS` function. * Added the `BFS` function. * Added the `CreatesCycle` function. * Added the `StronglyConnectedComponents` function. * Added the `ShortestPath` function. * Added the `ErrEdgeNotFound` error indicating that a desired edge could not be found. ### Removed * Removed the `Graph.EdgeByHashes` method. Use `Graph.AddEdge` instead. * Removed the `Graph.GetEdgeByHashes` method. Use `Graph.Edge` instead. * Removed the `Graph.DegreeByHash` method. Use `Graph.Degree` instead. * Removed the `Graph.Degree` method. * Removed the `Graph.DFS` and `Graph.DFSByHash` methods. Use `DFS` instead. * Removed the `Graph.BFS` and `Graph.BFSByHash` methods. Use `BFS` instead. * Removed the `Graph.CreatesCycle` and `Graph.CreatesCycleByHashes` methods. Use `CreatesCycle` instead. * Removed the `Graph.StronglyConnectedComponents` method. Use `StronglyConnectedComponents` instead. * Removed the `Graph.ShortestPath` and `Graph.ShortestPathByHash` methods. Use `ShortestPath` instead. ## [0.8.0] - 2022-08-01 ### Added * Added the `EdgeWeight` and `EdgeAttribute` functional options. * Added the `Properties` field to `Edge`. ### Changed * Changed `Edge` to accept a variadic `options` parameter. * Changed `EdgeByHashes` to accept a variadic `options` parameter. * Renamed `draw.Graph` to `draw.DOT` for more clarity regarding the rendering format. ### Removed * Removed the `WeightedEdge` function. Use `Edge` with the `EdgeWeight` functional option instead. * Removed the `WeightedEdgeByHashes` function. Use `EdgeByHashes` with the `EdgeWeight` functional option instead. ### Fixed * Fixed missing edge attributes when drawing a graph using `draw.DOT`. ## [0.7.0] - 2022-07-26 ### Added * Added `draw` package for graph visualization using DOT-compatible renderers. * Added `Traits` function for retrieving the graph's traits. ## [0.6.0] - 2022-07-22 ### Added * Added `AdjacencyMap` function for retrieving an adjancency map for all vertices. ### Removed * Removed the `AdjacencyList` function. ## [0.5.0] - 2022-07-21 ### Added * Added `AdjacencyList` function for retrieving an adjacency list for all vertices. ### Changed * Updated the examples in the documentation. ## [0.4.0] - 2022-07-01 ### Added * Added `ShortestPath` function for computing shortest paths. ### Changed * Changed the term "properties" to "traits" in the code and documentation. * Don't traverse all vertices in disconnected graphs by design. ## [0.3.0] - 2022-06-27 ### Added * Added `StronglyConnectedComponents` function for detecting SCCs. * Added various images to usage examples. ## [0.2.0] - 2022-06-20 ### Added * Added `Degree` and `DegreeByHash` functions for determining vertex degrees. * Added cycle checks when adding an edge using the `Edge` functions. ## [0.1.0] - 2022-06-19 ### Added * Added `CreatesCycle` and `CreatesCycleByHashes` functions for predicting cycles. ## [0.1.0-beta] - 2022-06-17 ### Changed * Introduced dedicated types for directed and undirected graphs, making `Graph[K, T]` an interface. ## [0.1.0-alpha] - 2022-06-13 ### Added * Introduced core types and methods. graph-0.23.0/LICENSE000066400000000000000000000261351445126167600137250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. graph-0.23.0/README.md000066400000000000000000000234621445126167600141770ustar00rootroot00000000000000[中文版](README_CN.md) | [English Version](README.md) # A library for creating generic graph data structures and modifying, analyzing, and visualizing them. **Are you using graph? [Check out the graph user survey.](https://forms.gle/MLKUZKMeCRxTfj4v9)** # Features * Generic vertices of any type, such as `int` or `City`. * Graph traits with corresponding validations, such as cycle checks in acyclic graphs. * Algorithms for finding paths or components, such as shortest paths or strongly connected components. * Algorithms for transformations and representations, such as transitive reduction or topological order. * Algorithms for non-recursive graph traversal, such as DFS or BFS. * Vertices and edges with optional metadata, such as weights or custom attributes. * Visualization of graphs using the DOT language and Graphviz. * Integrate any storage backend by using your own `Store` implementation. * Extensive tests with ~90% coverage, and zero dependencies. > Status: Because `graph` is in version 0, the public API shouldn't be considered stable. > This README may contain unreleased changes. Check out the [latest documentation](https://pkg.go.dev/github.com/dominikbraun/graph). # Getting started ``` go get github.com/dominikbraun/graph ``` # Quick examples ## Create a graph of integers ![graph of integers](img/simple.svg) ```go g := graph.New(graph.IntHash) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddVertex(4) _ = g.AddVertex(5) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 4) _ = g.AddEdge(2, 3) _ = g.AddEdge(2, 4) _ = g.AddEdge(2, 5) _ = g.AddEdge(3, 5) ``` ## Create a directed acyclic graph of integers ![directed acyclic graph](img/dag.svg) ```go g := graph.New(graph.IntHash, graph.Directed(), graph.Acyclic()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddVertex(4) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) _ = g.AddEdge(2, 3) _ = g.AddEdge(2, 4) _ = g.AddEdge(3, 4) ``` ## Create a graph of a custom type To understand this example in detail, see the [concept of hashes](https://pkg.go.dev/github.com/dominikbraun/graph#hdr-Hashes). ```go type City struct { Name string } cityHash := func(c City) string { return c.Name } g := graph.New(cityHash) _ = g.AddVertex(london) ``` ## Create a weighted graph ![weighted graph](img/cities.svg) ```go g := graph.New(cityHash, graph.Weighted()) _ = g.AddVertex(london) _ = g.AddVertex(munich) _ = g.AddVertex(paris) _ = g.AddVertex(madrid) _ = g.AddEdge("london", "munich", graph.EdgeWeight(3)) _ = g.AddEdge("london", "paris", graph.EdgeWeight(2)) _ = g.AddEdge("london", "madrid", graph.EdgeWeight(5)) _ = g.AddEdge("munich", "madrid", graph.EdgeWeight(6)) _ = g.AddEdge("munich", "paris", graph.EdgeWeight(2)) _ = g.AddEdge("paris", "madrid", graph.EdgeWeight(4)) ``` ## Perform a Depth-First Search This example traverses and prints all vertices in the graph in DFS order. ![depth-first search](img/dfs.svg) ```go g := graph.New(graph.IntHash, graph.Directed()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddVertex(4) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) _ = g.AddEdge(3, 4) _ = graph.DFS(g, 1, func(value int) bool { fmt.Println(value) return false }) ``` ``` 1 3 4 2 ``` ## Find strongly connected components ![strongly connected components](img/scc.svg) ```go g := graph.New(graph.IntHash) // Add vertices and edges ... scc, _ := graph.StronglyConnectedComponents(g) fmt.Println(scc) ``` ``` [[1 2 5] [3 4 8] [6 7]] ``` ## Find the shortest path ![shortest path algorithm](img/dijkstra.svg) ```go g := graph.New(graph.StringHash, graph.Weighted()) // Add vertices and weighted edges ... path, _ := graph.ShortestPath(g, "A", "B") fmt.Println(path) ``` ``` [A C E B] ``` ## Find spanning trees ![minimum spanning tree](img/mst.svg) ```go g := graph.New(graph.StringHash, graph.Weighted()) // Add vertices and edges ... mst, _ := graph.MinimumSpanningTree(g) ``` ## Perform a topological sort ![topological sort](img/topological-sort.svg) ```go g := graph.New(graph.IntHash, graph.Directed(), graph.PreventCycles()) // Add vertices and edges ... // For a deterministic topological ordering, use StableTopologicalSort. order, _ := graph.TopologicalSort(g) fmt.Println(order) ``` ``` [1 2 3 4 5] ``` ## Perform a transitive reduction ![transitive reduction](img/transitive-reduction-before.svg) ```go g := graph.New(graph.StringHash, graph.Directed(), graph.PreventCycles()) // Add vertices and edges ... transitiveReduction, _ := graph.TransitiveReduction(g) ``` ![transitive reduction](img/transitive-reduction-after.svg) ## Prevent the creation of cycles ![cycle checks](img/cycles.svg) ```go g := graph.New(graph.IntHash, graph.PreventCycles()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) if err := g.AddEdge(2, 3); err != nil { panic(err) } ``` ``` panic: an edge between 2 and 3 would introduce a cycle ``` ## Visualize a graph using Graphviz The following example will generate a DOT description for `g` and write it into the given file. ```go g := graph.New(graph.IntHash, graph.Directed()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) file, _ := os.Create("./mygraph.gv") _ = draw.DOT(g, file) ``` To generate an SVG from the created file using Graphviz, use a command such as the following: ``` dot -Tsvg -O mygraph.gv ``` The `DOT` function also supports rendering graph attributes: ```go _ = draw.DOT(g, file, draw.GraphAttribute("label", "my-graph")) ``` ### Draw a graph as in this documentation ![simple graph](img/simple.svg) This graph has been rendered using the following program: ```go package main import ( "os" "github.com/dominikbraun/graph" "github.com/dominikbraun/graph/draw" ) func main() { g := graph.New(graph.IntHash) _ = g.AddVertex(1, graph.VertexAttribute("colorscheme", "blues3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(2, graph.VertexAttribute("colorscheme", "greens3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(3, graph.VertexAttribute("colorscheme", "purples3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(4, graph.VertexAttribute("colorscheme", "ylorbr3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(5, graph.VertexAttribute("colorscheme", "reds3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 4) _ = g.AddEdge(2, 3) _ = g.AddEdge(2, 4) _ = g.AddEdge(2, 5) _ = g.AddEdge(3, 5) file, _ := os.Create("./simple.gv") _ = draw.DOT(g, file) } ``` It has been rendered using the `neato` engine: ``` dot -Tsvg -Kneato -O simple.gv ``` The example uses the [Brewer color scheme](https://graphviz.org/doc/info/colors.html#brewer) supported by Graphviz. ## Storing edge attributes Edges may have one or more attributes which can be used to store metadata. Attributes will be taken into account when [visualizing a graph](#visualize-a-graph-using-graphviz). For example, this edge will be rendered in red color: ```go _ = g.AddEdge(1, 2, graph.EdgeAttribute("color", "red")) ``` To get an overview of all supported attributes, take a look at the [DOT documentation](https://graphviz.org/doc/info/attrs.html). The stored attributes can be retrieved by getting the edge and accessing the `Properties.Attributes` field. ```go edge, _ := g.Edge(1, 2) color := edge.Properties.Attributes["color"] ``` ## Storing edge data It is also possible to store arbitrary data inside edges, not just key-value string pairs. This data is of type `any`. ```go _ = g.AddEdge(1, 2, graph.EdgeData(myData)) ``` The stored data can be retrieved by getting the edge and accessing the `Properties.Data` field. ```go edge, _ := g.Edge(1, 2) myData := edge.Properties.Data ``` ### Updating edge data Edge properties can be updated using `Graph.UpdateEdge`. The following example adds a new `color` attribute to the edge (A,B) and sets the edge weight to 10. ```go _ = g.UpdateEdge("A", "B", graph.EdgeAttribute("color", "red"), graph.EdgeWeight(10)) ``` The method signature and the accepted functional options are exactly the same as for `Graph.AddEdge`. ## Storing vertex attributes Vertices may have one or more attributes which can be used to store metadata. Attributes will be taken into account when [visualizing a graph](#visualize-a-graph-using-graphviz). For example, this vertex will be rendered in red color: ```go _ = g.AddVertex(1, graph.VertexAttribute("style", "filled")) ``` The stored data can be retrieved by getting the vertex using `VertexWithProperties` and accessing the `Attributes` field. ```go vertex, properties, _ := g.VertexWithProperties(1) style := properties.Attributes["style"] ``` To get an overview of all supported attributes, take a look at the [DOT documentation](https://graphviz.org/doc/info/attrs.html). ## Store the graph in a custom storage You can integrate any storage backend by implementing the `Store` interface and initializing a new graph with it: ```go g := graph.NewWithStore(graph.IntHash, myStore) ``` To implement the `Store` interface appropriately, take a look at the [documentation](https://pkg.go.dev/github.com/dominikbraun/graph#Store). [`graph-sql`](https://github.com/dominikbraun/graph-sql) is a ready-to-use SQL store implementation. # Documentation The full documentation is available at [pkg.go.dev](https://pkg.go.dev/github.com/dominikbraun/graph). **Are you using graph? [Check out the graph user survey.](https://forms.gle/MLKUZKMeCRxTfj4v9)** graph-0.23.0/README_CN.md000066400000000000000000000224111445126167600145500ustar00rootroot00000000000000[中文版](README_CN.md) | [English Version](README.md) # 这是一款用于创建通用图数据结构、对其进行修改、分析和可视化的库。 # 特性 * 支持任意类型的通用顶点,例如 `int` 或 `City`。 * 图的特征和相应的验证,例如在无环图中进行循环检查。 * 寻找路径或连通图的算法,例如最短路径或强连通图。 * 转换和表示的算法,例如传递闭包或拓扑排序。 * 非递归图遍历的算法,例如 DFS 或 BFS。 * 顶点和边可以包含可选的元数据,例如权重或自定义属性。 * 使用 DOT 语言和 Graphviz 进行图形可视化。 * 通过使用自己的 `Store` 实现,可以集成任何存储后端。 * 包含广泛的测试,覆盖率约为 90%,且没有任何依赖项。 > 状态:由于 graph 版本处于 0.x 阶段,公共 API 不应被视为稳定的。 > README 可能包含未发布的更改。请查看 [latest documentation](https://pkg.go.dev/github.com/dominikbraun/graph). # 入门指南 ``` go get github.com/dominikbraun/graph ``` # 快速示例 ## 创建整数类型节点ID图 ![graph of integers](img/simple.svg) ```go g := graph.New(graph.IntHash) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddVertex(4) _ = g.AddVertex(5) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 4) _ = g.AddEdge(2, 3) _ = g.AddEdge(2, 4) _ = g.AddEdge(2, 5) _ = g.AddEdge(3, 5) ``` ## 创建整数类型节点ID有向无环图 ![directed acyclic graph](img/dag.svg) ```go g := graph.New(graph.IntHash, graph.Directed(), graph.Acyclic()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddVertex(4) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) _ = g.AddEdge(2, 3) _ = g.AddEdge(2, 4) _ = g.AddEdge(3, 4) ``` ## 创建自定义类型节点ID图 要详细了解此示例,请参见 [concept of hashes](https://pkg.go.dev/github.com/dominikbraun/graph@v0.17.0-rc4#hdr-Hashes). ```go type City struct { Name string } cityHash := func(c City) string { return c.Name } g := graph.New(cityHash) _ = g.AddVertex(london) ``` ## 创建边带权重的图 ![weighted graph](img/cities.svg) ```go g := graph.New(cityHash, graph.Weighted()) _ = g.AddVertex(london) _ = g.AddVertex(munich) _ = g.AddVertex(paris) _ = g.AddVertex(madrid) _ = g.AddEdge("london", "munich", graph.EdgeWeight(3)) _ = g.AddEdge("london", "paris", graph.EdgeWeight(2)) _ = g.AddEdge("london", "madrid", graph.EdgeWeight(5)) _ = g.AddEdge("munich", "madrid", graph.EdgeWeight(6)) _ = g.AddEdge("munich", "paris", graph.EdgeWeight(2)) _ = g.AddEdge("paris", "madrid", graph.EdgeWeight(4)) ``` ## 执行深度优先搜索 这个示例按 DFS 顺序遍历并打印图中的所有顶点。 ![depth-first search](img/dfs.svg) ```go g := graph.New(graph.IntHash, graph.Directed()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddVertex(4) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) _ = g.AddEdge(3, 4) _ = graph.DFS(g, 1, func(value int) bool { fmt.Println(value) return false }) ``` ``` 1 3 4 2 ``` ## 查找强联通分量 ![strongly connected components](img/scc.svg) ```go g := graph.New(graph.IntHash) // Add vertices and edges ... scc, _ := graph.StronglyConnectedComponents(g) fmt.Println(scc) ``` ``` [[1 2 5] [3 4 8] [6 7]] ``` ## 查找最短路径 ![shortest path algorithm](img/dijkstra.svg) ```go g := graph.New(graph.StringHash, graph.Weighted()) // Add vertices and weighted edges ... path, _ := graph.ShortestPath(g, "A", "B") fmt.Println(path) ``` ``` [A C E B] ``` ## 查找生成树 ![minimum spanning tree](img/mst.svg) ```go g := graph.New(graph.StringHash, graph.Weighted()) // Add vertices and edges ... mst, _ := graph.MinimumSpanningTree(g) ``` ## 执行拓扑排序 ![topological sort](img/topological-sort.svg) ```go g := graph.New(graph.IntHash, graph.Directed(), graph.PreventCycles()) // Add vertices and edges ... // For a deterministic topological ordering, use StableTopologicalSort. order, _ := graph.TopologicalSort(g) fmt.Println(order) ``` ``` [1 2 3 4 5] ``` ## 执行传递闭包削减 ![transitive reduction](img/transitive-reduction-before.svg) ```go g := graph.New(graph.StringHash, graph.Directed(), graph.PreventCycles()) // Add vertices and edges ... transitiveReduction, _ := graph.TransitiveReduction(g) ``` ![transitive reduction](img/transitive-reduction-after.svg) ## 禁止创建环路 ![cycle checks](img/cycles.svg) ```go g := graph.New(graph.IntHash, graph.PreventCycles()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) if err := g.AddEdge(2, 3); err != nil { panic(err) } ``` ``` panic: 在 2 和 3 之间创建的边将会引入一个环 ``` ## 使用 Graphviz 图可视化 以下示例将为 `g` 生成一个 DOT 描述,并将其写入给定的文件中。 ```go g := graph.New(graph.IntHash, graph.Directed()) _ = g.AddVertex(1) _ = g.AddVertex(2) _ = g.AddVertex(3) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 3) file, _ := os.Create("./mygraph.gv") _ = draw.DOT(g, file) ``` 要使用 Graphviz 从创建的文件生成 SVG,请使用如下命令: ``` dot -Tsvg -O mygraph.gv ``` `DOT` 函数还支持渲染图属性: ```go _ = draw.DOT(g, file, draw.GraphAttribute("label", "my-graph")) ``` ### 按照此文档绘制图 ![simple graph](img/simple.svg) 图使用以下程序进行渲染: ```go package main import ( "os" "github.com/dominikbraun/graph" "github.com/dominikbraun/graph/draw" ) func main() { g := graph.New(graph.IntHash) _ = g.AddVertex(1, graph.VertexAttribute("colorscheme", "blues3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(2, graph.VertexAttribute("colorscheme", "greens3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(3, graph.VertexAttribute("colorscheme", "purples3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(4, graph.VertexAttribute("colorscheme", "ylorbr3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddVertex(5, graph.VertexAttribute("colorscheme", "reds3"), graph.VertexAttribute("style", "filled"), graph.VertexAttribute("color", "2"), graph.VertexAttribute("fillcolor", "1")) _ = g.AddEdge(1, 2) _ = g.AddEdge(1, 4) _ = g.AddEdge(2, 3) _ = g.AddEdge(2, 4) _ = g.AddEdge(2, 5) _ = g.AddEdge(3, 5) file, _ := os.Create("./simple.gv") _ = draw.DOT(g, file) } ``` 使用 neato 引擎进行可视化: ``` dot -Tsvg -Kneato -O simple.gv ``` 这个例子使用Graphviz支持的 [Brewer color scheme](https://graphviz.org/doc/info/colors.html#brewer)。 ## 存储边属性 边可以具有一个或多个属性,用于存储元数据。在[visualizing a graph](#visualize-a-graph-using-graphviz) 时将考虑这些属性。 例如,此边将呈现为红色: ```go _ = g.AddEdge(1, 2, graph.EdgeAttribute("color", "red")) ``` 要获取所有支持的属性的概述,请查看 [DOT documentation](https://graphviz.org/doc/info/attrs.html). The stored attributes can be retrieved by getting the edge and accessing the `Properties.Attributes` field. 可以通过获取边并访问 `Properties.Attributes` 字段来检索存储的属性。 ```go edge, _ := g.Edge(1, 2) color := edge.Properties.Attributes["color"] ``` ## 存储边数据 还可以在边上存储任意类型属性数据,而不仅仅是键值字符串对。此数据类型为 `any`。 ```go _ = g.AddEdge(1, 2, graph.EdgeData(myData)) ``` 可以通过获取边并访问 `Properties.Data` 字段来检索存储的数据。 ```go edge, _ := g.Edge(1, 2) myData := edge.Properties.Data ``` ### 更新边数据 可以使用 `Graph.UpdateEdge` 更新边属性。以下示例向边 (A,B) 添加了一个新的 `color` 属性,并将边权重设置为 10。 ```go _ = g.UpdateEdge("A", "B", graph.EdgeAttribute("color", "red"), graph.EdgeWeight(10)) ``` `Graph.UpdateEdge` 的方法签名和接受的函数选项与 `Graph.AddEdge` 完全相同。 ## 存储点属性 顶点可能具有一个或多个属性,可用于存储元数据。在 [visualizing a graph](#visualize-a-graph-using-graphviz) 时将考虑这些属性。 例如,此顶点将以红色渲染: ```go _ = g.AddVertex(1, graph.VertexAttribute("style", "filled")) ``` 存储在顶点中的数据可以通过使用 `VertexWithProperties` 获取顶点,并访问 `Attributes` 字段来检索。 ```go vertex, properties, _ := g.VertexWithProperties(1) style := properties.Attributes["style"] ``` 要获取所有支持的属性的概述,请查看 [DOT documentation](https://graphviz.org/doc/info/attrs.html). ## 将图存储在自定义存储中 可以通过实现 `Store` 接口并使用它初始化一个新的图,来集成任何存储后端: ```go g := graph.NewWithStore(graph.IntHash, myStore) ``` 恰当实现 `Store` 接口,参考 [documentation](https://pkg.go.dev/github.com/dominikbraun/graph#Store)。 [`graph-sql`](https://github.com/dominikbraun/graph-sql) 是一个可直接使用的 SQL 存储实现。 # 文档 完整文档可在以下位置找到: [pkg.go.dev](https://pkg.go.dev/github.com/dominikbraun/graph). graph-0.23.0/collection.go000066400000000000000000000066461445126167600154070ustar00rootroot00000000000000package graph import ( "container/heap" "errors" ) // priorityQueue implements a minimum priority queue using a minimum binary heap // that prioritizes smaller values over larger values. type priorityQueue[T comparable] struct { items *minHeap[T] cache map[T]*priorityItem[T] } // priorityItem is an item on the binary heap consisting of a priority value and // an actual payload value. type priorityItem[T comparable] struct { value T priority float64 index int } func newPriorityQueue[T comparable]() *priorityQueue[T] { return &priorityQueue[T]{ items: &minHeap[T]{}, cache: map[T]*priorityItem[T]{}, } } // Len returns the total number of items in the priority queue. func (p *priorityQueue[T]) Len() int { return p.items.Len() } // Push pushes a new item with the given priority into the queue. This operation // may cause a re-balance of the heap and thus scales with O(log n). func (p *priorityQueue[T]) Push(item T, priority float64) { if _, ok := p.cache[item]; ok { return } newItem := &priorityItem[T]{ value: item, priority: priority, index: 0, } heap.Push(p.items, newItem) p.cache[item] = newItem } // Pop returns and removes the item with the lowest priority. This operation may // cause a re-balance of the heap and thus scales with O(log n). func (p *priorityQueue[T]) Pop() (T, error) { if len(*p.items) == 0 { var empty T return empty, errors.New("priority queue is empty") } item := heap.Pop(p.items).(*priorityItem[T]) delete(p.cache, item.value) return item.value, nil } // UpdatePriority updates the priority of a given item and sets it to the given // priority. If the item doesn't exist, nothing happens. This operation may // cause a re-balance of the heap and this scales with O(log n). func (p *priorityQueue[T]) UpdatePriority(item T, priority float64) { targetItem, ok := p.cache[item] if !ok { return } targetItem.priority = priority heap.Fix(p.items, targetItem.index) } // minHeap is a minimum binary heap that implements heap.Interface. type minHeap[T comparable] []*priorityItem[T] func (m *minHeap[T]) Len() int { return len(*m) } func (m *minHeap[T]) Less(i, j int) bool { return (*m)[i].priority < (*m)[j].priority } func (m *minHeap[T]) Swap(i, j int) { (*m)[i], (*m)[j] = (*m)[j], (*m)[i] (*m)[i].index = i (*m)[j].index = j } func (m *minHeap[T]) Push(item interface{}) { i := item.(*priorityItem[T]) i.index = len(*m) *m = append(*m, i) } func (m *minHeap[T]) Pop() interface{} { old := *m item := old[len(old)-1] *m = old[:len(old)-1] return item } type stack[T any] interface { push(T) pop() (T, error) top() (T, error) isEmpty() bool // forEach iterate the stack from bottom to top forEach(func(T)) } func newStack[T any]() stack[T] { return &stackImpl[T]{ elements: make([]T, 0), } } type stackImpl[T any] struct { elements []T } func (s *stackImpl[T]) push(t T) { s.elements = append(s.elements, t) } func (s *stackImpl[T]) pop() (T, error) { e, err := s.top() if err != nil { var defaultValue T return defaultValue, err } s.elements = s.elements[:len(s.elements)-1] return e, nil } func (s *stackImpl[T]) top() (T, error) { if s.isEmpty() { var defaultValue T return defaultValue, errors.New("no element in stack") } return s.elements[len(s.elements)-1], nil } func (s *stackImpl[T]) isEmpty() bool { return len(s.elements) == 0 } func (s *stackImpl[T]) forEach(f func(T)) { for _, e := range s.elements { f(e) } } graph-0.23.0/collection_test.go000066400000000000000000000177331445126167600164450ustar00rootroot00000000000000package graph import ( "reflect" "testing" ) func TestPriorityQueue_Push(t *testing.T) { tests := map[string]struct { items []int priorities []float64 expectedPriorityItems []*priorityItem[int] }{ "queue with 5 elements": { items: []int{10, 20, 30, 40, 50}, priorities: []float64{6, 8, 2, 7, 5}, expectedPriorityItems: []*priorityItem[int]{ {value: 20, priority: 8}, {value: 40, priority: 7}, {value: 10, priority: 6}, {value: 50, priority: 5}, {value: 30, priority: 2}, }, }, } for name, test := range tests { queue := newPriorityQueue[int]() for i, item := range test.items { queue.Push(item, test.priorities[i]) } if queue.Len() != len(test.expectedPriorityItems) { t.Fatalf("%s: item length expectancy doesn't match: expected %v, got %v", name, len(test.expectedPriorityItems), queue.Len()) } popped := make([]int, queue.Len()) for queue.Len() > 0 { item, _ := queue.Pop() popped = append(popped, item) } n := len(popped) for i, item := range test.expectedPriorityItems { poppedItem := popped[n-1-i] if item.value != poppedItem { t.Errorf("%s: item doesn't match: expected %v at index %d, got %v", name, item.value, i, poppedItem) } } } } func TestPriorityQueue_Pop(t *testing.T) { tests := map[string]struct { items []int priorities []float64 expectedItem int shouldFail bool }{ "queue with 5 item": { items: []int{10, 20, 30, 40, 50}, priorities: []float64{6, 8, 2, 7, 5}, expectedItem: 30, shouldFail: false, }, "queue with 1 item": { items: []int{10}, priorities: []float64{6}, expectedItem: 10, shouldFail: false, }, "empty queue": { items: []int{}, priorities: []float64{}, shouldFail: true, }, } for name, test := range tests { queue := newPriorityQueue[int]() for i, item := range test.items { queue.Push(item, test.priorities[i]) } item, err := queue.Pop() if test.shouldFail != (err != nil) { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, (err != nil), err) } if item != test.expectedItem { t.Errorf("%s: item expectancy doesn't match: expected %v, got %v", name, test.expectedItem, item) } } } func TestPriorityQueue_UpdatePriority(t *testing.T) { tests := map[string]struct { items []*priorityItem[int] expectedPriorityItems []*priorityItem[int] decreaseItem int decreasePriority float64 }{ "decrease 30 to priority 5": { items: []*priorityItem[int]{ {value: 40, priority: 40}, {value: 30, priority: 30}, {value: 20, priority: 20}, {value: 10, priority: 10}, }, decreaseItem: 30, decreasePriority: 5, expectedPriorityItems: []*priorityItem[int]{ {value: 40, priority: 40}, {value: 20, priority: 20}, {value: 10, priority: 10}, {value: 30, priority: 5}, }, }, "decrease a non-existent item": { items: []*priorityItem[int]{ {value: 40, priority: 40}, {value: 30, priority: 30}, {value: 20, priority: 20}, {value: 10, priority: 10}, }, decreaseItem: 50, decreasePriority: 10, expectedPriorityItems: []*priorityItem[int]{ {value: 40, priority: 40}, {value: 30, priority: 30}, {value: 20, priority: 20}, {value: 10, priority: 10}, }, }, "increase 10 to priority 100": { items: []*priorityItem[int]{ {value: 40, priority: 40}, {value: 30, priority: 30}, {value: 20, priority: 20}, {value: 10, priority: 10}, }, decreaseItem: 10, decreasePriority: 100, expectedPriorityItems: []*priorityItem[int]{ {value: 10, priority: 100}, {value: 40, priority: 40}, {value: 30, priority: 30}, {value: 20, priority: 20}, }, }, } for name, test := range tests { queue := newPriorityQueue[int]() for _, item := range test.items { queue.Push(item.value, item.priority) } queue.UpdatePriority(test.decreaseItem, test.decreasePriority) if queue.Len() != len(test.expectedPriorityItems) { t.Fatalf("%s: item length expectancy doesn't match: expected %v, got %v", name, len(test.expectedPriorityItems), queue.Len()) } popped := make([]int, queue.Len()) for queue.Len() > 0 { item, _ := queue.Pop() popped = append(popped, item) } n := len(popped) for i, item := range test.expectedPriorityItems { poppedItem := popped[n-1-i] if item.value != poppedItem { t.Errorf("%s: item doesn't match: expected %v at index %d, got %v", name, item.value, i, poppedItem) } } } } func TestPriorityQueue_Len(t *testing.T) { tests := map[string]struct { items []int priorities []float64 expectedLen int }{ "queue with 5 item": { items: []int{10, 20, 30, 40, 50}, priorities: []float64{6, 8, 2, 7, 5}, expectedLen: 5, }, "queue with 1 item": { items: []int{10}, priorities: []float64{6}, expectedLen: 1, }, "empty queue": { items: []int{}, priorities: []float64{}, expectedLen: 0, }, } for name, test := range tests { queue := newPriorityQueue[int]() for i, item := range test.items { queue.Push(item, test.priorities[i]) } n := queue.Len() if n != test.expectedLen { t.Errorf("%s: length expectancy doesn't match: expected %v, got %v", name, test.expectedLen, n) } } } func Test_stackImpl_push(t *testing.T) { type args[T any] struct { t T } type testCase[T any] struct { name string s stackImpl[T] args args[T] } tests := []testCase[int]{ { "push 1", stackImpl[int]{ elements: make([]int, 0), }, args[int]{ t: 1, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.s.push(tt.args.t) }) } } func Test_stackImpl_pop(t *testing.T) { type testCase[T any] struct { name string s stackImpl[T] want T wantErr bool } tests := []testCase[int]{ { "pop element", stackImpl[int]{ elements: []int{1}, }, 1, false, }, { "pop element from empty stack", stackImpl[int]{ elements: []int{}, }, 0, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.s.pop() if (err != nil) != tt.wantErr { t.Errorf("pop() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("pop() got = %v, want %v", got, tt.want) } }) } } func Test_stackImpl_top(t *testing.T) { type testCase[T any] struct { name string s stackImpl[T] want T wantErr bool } tests := []testCase[int]{ { "top element", stackImpl[int]{ elements: []int{1}, }, 1, false, }, { "top element of empty stack", stackImpl[int]{ elements: []int{}, }, 0, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.s.top() if (err != nil) != tt.wantErr { t.Errorf("top() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("top() got = %v, want %v", got, tt.want) } }) } } func Test_stackImpl_isEmpty(t *testing.T) { type testCase[T any] struct { name string s stackImpl[T] want bool } tests := []testCase[int]{ { "empty", stackImpl[int]{ elements: []int{}, }, true, }, { "not empty", stackImpl[int]{ elements: []int{1}, }, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.s.isEmpty(); got != tt.want { t.Errorf("isEmpty() = %v, want %v", got, tt.want) } }) } } func Test_stackImpl_forEach(t *testing.T) { type args[T any] struct { f func(T) } type testCase[T any] struct { name string s stackImpl[T] args args[T] } tests := []testCase[int]{ { name: "forEach", s: stackImpl[int]{ elements: []int{1, 2, 3, 4, 5, 6}, }, args: args[int]{ f: func(i int) { }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.s.forEach(tt.args.f) }) } } graph-0.23.0/dag.go000066400000000000000000000141151445126167600137750ustar00rootroot00000000000000package graph import ( "errors" "fmt" "sort" ) // TopologicalSort runs a topological sort on a given directed graph and returns // the vertex hashes in topological order. The topological order is a non-unique // order of vertices in a directed graph where an edge from vertex A to vertex B // implies that vertex A appears before vertex B. // // Note that TopologicalSort doesn't make any guarantees about the order. If there // are multiple valid topological orderings, an arbitrary one will be returned. // To make the output deterministic, use [StableTopologicalSort]. // // TopologicalSort only works for directed acyclic graphs. This implementation // works non-recursively and utilizes Kahn's algorithm. func TopologicalSort[K comparable, T any](g Graph[K, T]) ([]K, error) { if !g.Traits().IsDirected { return nil, fmt.Errorf("topological sort cannot be computed on undirected graph") } gOrder, err := g.Order() if err != nil { return nil, fmt.Errorf("failed to get graph order: %w", err) } predecessorMap, err := g.PredecessorMap() if err != nil { return nil, fmt.Errorf("failed to get predecessor map: %w", err) } queue := make([]K, 0) for vertex, predecessors := range predecessorMap { if len(predecessors) == 0 { queue = append(queue, vertex) } } order := make([]K, 0, gOrder) visited := make(map[K]struct{}, gOrder) for len(queue) > 0 { currentVertex := queue[0] queue = queue[1:] if _, ok := visited[currentVertex]; ok { continue } order = append(order, currentVertex) visited[currentVertex] = struct{}{} for vertex, predecessors := range predecessorMap { delete(predecessors, currentVertex) if len(predecessors) == 0 { queue = append(queue, vertex) } } } if len(order) != gOrder { return nil, errors.New("topological sort cannot be computed on graph with cycles") } return order, nil } // StableTopologicalSort does the same as [TopologicalSort], but takes a function // for comparing (and then ordering) two given vertices. This allows for a stable // and deterministic output even for graphs with multiple topological orderings. func StableTopologicalSort[K comparable, T any](g Graph[K, T], less func(K, K) bool) ([]K, error) { if !g.Traits().IsDirected { return nil, fmt.Errorf("topological sort cannot be computed on undirected graph") } predecessorMap, err := g.PredecessorMap() if err != nil { return nil, fmt.Errorf("failed to get predecessor map: %w", err) } queue := make([]K, 0) queued := make(map[K]struct{}) for vertex, predecessors := range predecessorMap { if len(predecessors) == 0 { queue = append(queue, vertex) queued[vertex] = struct{}{} } } order := make([]K, 0, len(predecessorMap)) visited := make(map[K]struct{}) sort.Slice(queue, func(i, j int) bool { return less(queue[i], queue[j]) }) for len(queue) > 0 { currentVertex := queue[0] queue = queue[1:] if _, ok := visited[currentVertex]; ok { continue } order = append(order, currentVertex) visited[currentVertex] = struct{}{} frontier := make([]K, 0) for vertex, predecessors := range predecessorMap { delete(predecessors, currentVertex) if len(predecessors) != 0 { continue } if _, ok := queued[vertex]; ok { continue } frontier = append(frontier, vertex) queued[vertex] = struct{}{} } sort.Slice(frontier, func(i, j int) bool { return less(frontier[i], frontier[j]) }) queue = append(queue, frontier...) } gOrder, err := g.Order() if err != nil { return nil, fmt.Errorf("failed to get graph order: %w", err) } if len(order) != gOrder { return nil, errors.New("topological sort cannot be computed on graph with cycles") } return order, nil } // TransitiveReduction returns a new graph with the same vertices and the same // reachability as the given graph, but with as few edges as possible. The graph // must be a directed acyclic graph. // // TransitiveReduction is a very expensive operation scaling with O(V(V+E)). func TransitiveReduction[K comparable, T any](g Graph[K, T]) (Graph[K, T], error) { if !g.Traits().IsDirected { return nil, fmt.Errorf("transitive reduction cannot be performed on undirected graph") } transitiveReduction, err := g.Clone() if err != nil { return nil, fmt.Errorf("failed to clone the graph: %w", err) } adjacencyMap, err := transitiveReduction.AdjacencyMap() if err != nil { return nil, fmt.Errorf("failed to get adajcency map: %w", err) } // For each vertex in the graph, run a depth-first search from each direct // successor of that vertex. Then, for each vertex visited within the DFS, // inspect all of its edges. Remove the edges that also appear in the edge // set of the top-level vertex and target the current vertex. These edges // are redundant because their targets apparently are not only reachable // from the top-level vertex, but also through a DFS. for vertex, successors := range adjacencyMap { tOrder, err := transitiveReduction.Order() if err != nil { return nil, fmt.Errorf("failed to get graph order: %w", err) } for successor := range successors { stack := make([]K, 0, tOrder) visited := make(map[K]struct{}, tOrder) onStack := make(map[K]bool, tOrder) stack = append(stack, successor) for len(stack) > 0 { current := stack[len(stack)-1] stack = stack[:len(stack)-1] if _, ok := visited[current]; ok { onStack[current] = false continue } visited[current] = struct{}{} onStack[current] = true stack = append(stack, current) if len(adjacencyMap[current]) == 0 { onStack[current] = false } for adjacency := range adjacencyMap[current] { if _, ok := visited[adjacency]; ok { if onStack[adjacency] { // If the current adjacency is both on the stack and // has already been visited, there is a cycle. return nil, fmt.Errorf("transitive reduction cannot be performed on graph with cycle") } continue } if _, ok := adjacencyMap[vertex][adjacency]; ok { _ = transitiveReduction.RemoveEdge(vertex, adjacency) } stack = append(stack, adjacency) } } } } return transitiveReduction, nil } graph-0.23.0/dag_test.go000066400000000000000000000200271445126167600150330ustar00rootroot00000000000000package graph import ( "fmt" "testing" ) func TestDirectedTopologicalSort(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expectedOrder []int shouldFail bool }{ "graph with 5 vertices": { vertices: []int{1, 2, 3, 4, 5}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 4}, {Source: 4, Target: 5}, }, expectedOrder: []int{1, 2, 3, 4, 5}, }, "graph with cycle": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, shouldFail: true, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } order, err := TopologicalSort(graph) if test.shouldFail != (err != nil) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, err != nil, err) } if test.shouldFail { continue } if len(order) != len(test.expectedOrder) { t.Errorf("%s: order length expectancy doesn't match: expected %v, got %v", name, len(test.expectedOrder), len(order)) } for i, expectedVertex := range test.expectedOrder { if expectedVertex != order[i] { t.Errorf("%s: order expectancy doesn't match: expected %v at %d, got %v", name, expectedVertex, i, order[i]) } } } } func TestUndirectedTopologicalSort(t *testing.T) { tests := map[string]struct { expectedOrder []int shouldFail bool }{ "return error": { expectedOrder: nil, shouldFail: true, }, } for name, test := range tests { graph := New(IntHash) order, err := TopologicalSort(graph) if test.shouldFail != (err != nil) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, err != nil, err) } if test.expectedOrder == nil && order != nil { t.Errorf("%s: order expectancy doesn't match: expcted %v, got %v", name, test.expectedOrder, order) } } } func TestDirectedStableTopologicalSort(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expectedOrder []int shouldFail bool }{ "graph with 5 vertices": { vertices: []int{1, 2, 3, 4, 5}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 4}, {Source: 4, Target: 5}, }, expectedOrder: []int{1, 2, 3, 4, 5}, }, "graph with many possible topological orders": { vertices: []int{1, 2, 3, 4, 5, 6, 10, 20, 30, 40, 50, 60}, edges: []Edge[int]{ {Source: 1, Target: 10}, {Source: 2, Target: 20}, {Source: 3, Target: 30}, {Source: 4, Target: 40}, {Source: 5, Target: 50}, {Source: 6, Target: 60}, }, expectedOrder: []int{1, 2, 3, 4, 5, 6, 10, 20, 30, 40, 50, 60}, }, "graph with cycle": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, shouldFail: true, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } order, err := StableTopologicalSort(graph, func(a, b int) bool { return a < b }) if test.shouldFail != (err != nil) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, err != nil, err) } if test.shouldFail { continue } if len(order) != len(test.expectedOrder) { t.Errorf("%s: order length expectancy doesn't match: expected %v, got %v", name, len(test.expectedOrder), len(order)) } fmt.Println("expected", test.expectedOrder) fmt.Println("actual", order) for i, expectedVertex := range test.expectedOrder { if expectedVertex != order[i] { t.Errorf("%s: order expectancy doesn't match: expected %v at %d, got %v", name, expectedVertex, i, order[i]) } } } } func TestDirectedTransitiveReduction(t *testing.T) { tests := map[string]struct { vertices []string edges []Edge[string] expectedEdges []Edge[string] shouldFail bool }{ "graph as on img/transitive-reduction-before.svg": { vertices: []string{"A", "B", "C", "D", "E"}, edges: []Edge[string]{ {Source: "A", Target: "B"}, {Source: "A", Target: "C"}, {Source: "A", Target: "D"}, {Source: "A", Target: "E"}, {Source: "B", Target: "D"}, {Source: "C", Target: "D"}, {Source: "C", Target: "E"}, {Source: "D", Target: "E"}, }, expectedEdges: []Edge[string]{ {Source: "A", Target: "B"}, {Source: "A", Target: "C"}, {Source: "B", Target: "D"}, {Source: "C", Target: "D"}, {Source: "D", Target: "E"}, }, }, "graph with cycle": { vertices: []string{"A", "B", "C"}, edges: []Edge[string]{ {Source: "A", Target: "B"}, {Source: "B", Target: "C"}, {Source: "C", Target: "A"}, }, shouldFail: true, }, "graph from issue 83": { vertices: []string{"_root", "A", "B", "C", "D", "E", "F"}, edges: []Edge[string]{ {Source: "_root", Target: "A"}, {Source: "_root", Target: "B"}, {Source: "_root", Target: "C"}, {Source: "_root", Target: "D"}, {Source: "_root", Target: "E"}, {Source: "_root", Target: "F"}, {Source: "E", Target: "C"}, {Source: "F", Target: "D"}, {Source: "F", Target: "C"}, {Source: "F", Target: "E"}, {Source: "C", Target: "A"}, {Source: "C", Target: "B"}, }, expectedEdges: []Edge[string]{ {Source: "_root", Target: "F"}, {Source: "F", Target: "D"}, {Source: "F", Target: "E"}, {Source: "E", Target: "C"}, {Source: "C", Target: "A"}, {Source: "C", Target: "B"}, }, }, } for name, test := range tests { graph := New(StringHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } reduction, err := TransitiveReduction(graph) if test.shouldFail != (err != nil) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, err != nil, err) } if test.shouldFail { continue } actualEdges := make([]Edge[string], 0) adjacencyMap, _ := reduction.AdjacencyMap() for _, adjacencies := range adjacencyMap { for _, edge := range adjacencies { actualEdges = append(actualEdges, edge) } } equalsFunc := reduction.(*directed[string, string]).edgesAreEqual if !slicesAreEqualWithFunc(actualEdges, test.expectedEdges, equalsFunc) { t.Errorf("%s: edge expectancy doesn't match: expected %v, got %v", name, test.expectedEdges, actualEdges) } } } func TestUndirectedTransitiveReduction(t *testing.T) { tests := map[string]struct { shouldFail bool }{ "return error": { shouldFail: true, }, } for name, test := range tests { graph := New(StringHash) _, err := TransitiveReduction(graph) if test.shouldFail != (err != nil) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, err != nil, err) } } } func slicesAreEqualWithFunc[T any](a, b []T, equals func(a, b T) bool) bool { if len(a) != len(b) { return false } for _, aValue := range a { found := false for _, bValue := range b { if equals(aValue, bValue) { found = true } } if !found { return false } } return true } graph-0.23.0/directed.go000066400000000000000000000171351445126167600150320ustar00rootroot00000000000000package graph import ( "errors" "fmt" ) type directed[K comparable, T any] struct { hash Hash[K, T] traits *Traits store Store[K, T] } func newDirected[K comparable, T any](hash Hash[K, T], traits *Traits, store Store[K, T]) *directed[K, T] { return &directed[K, T]{ hash: hash, traits: traits, store: store, } } func (d *directed[K, T]) Traits() *Traits { return d.traits } func (d *directed[K, T]) AddVertex(value T, options ...func(*VertexProperties)) error { hash := d.hash(value) properties := VertexProperties{ Weight: 0, Attributes: make(map[string]string), } for _, option := range options { option(&properties) } return d.store.AddVertex(hash, value, properties) } func (d *directed[K, T]) AddVerticesFrom(g Graph[K, T]) error { adjacencyMap, err := g.AdjacencyMap() if err != nil { return fmt.Errorf("failed to get adjacency map: %w", err) } for hash := range adjacencyMap { vertex, properties, err := g.VertexWithProperties(hash) if err != nil { return fmt.Errorf("failed to get vertex %v: %w", hash, err) } if err = d.AddVertex(vertex, copyVertexProperties(properties)); err != nil { return fmt.Errorf("failed to add vertex %v: %w", hash, err) } } return nil } func (d *directed[K, T]) Vertex(hash K) (T, error) { vertex, _, err := d.store.Vertex(hash) return vertex, err } func (d *directed[K, T]) VertexWithProperties(hash K) (T, VertexProperties, error) { vertex, properties, err := d.store.Vertex(hash) if err != nil { return vertex, VertexProperties{}, err } return vertex, properties, nil } func (d *directed[K, T]) RemoveVertex(hash K) error { return d.store.RemoveVertex(hash) } func (d *directed[K, T]) AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error { _, _, err := d.store.Vertex(sourceHash) if err != nil { return fmt.Errorf("source vertex %v: %w", sourceHash, err) } _, _, err = d.store.Vertex(targetHash) if err != nil { return fmt.Errorf("target vertex %v: %w", targetHash, err) } if _, err := d.Edge(sourceHash, targetHash); !errors.Is(err, ErrEdgeNotFound) { return ErrEdgeAlreadyExists } // If the user opted in to preventing cycles, run a cycle check. if d.traits.PreventCycles { createsCycle, err := d.createsCycle(sourceHash, targetHash) if err != nil { return fmt.Errorf("check for cycles: %w", err) } if createsCycle { return ErrEdgeCreatesCycle } } edge := Edge[K]{ Source: sourceHash, Target: targetHash, Properties: EdgeProperties{ Attributes: make(map[string]string), }, } for _, option := range options { option(&edge.Properties) } return d.addEdge(sourceHash, targetHash, edge) } func (d *directed[K, T]) AddEdgesFrom(g Graph[K, T]) error { edges, err := g.Edges() if err != nil { return fmt.Errorf("failed to get edges: %w", err) } for _, edge := range edges { if err := d.AddEdge(copyEdge(edge)); err != nil { return fmt.Errorf("failed to add (%v, %v): %w", edge.Source, edge.Target, err) } } return nil } func (d *directed[K, T]) Edge(sourceHash, targetHash K) (Edge[T], error) { edge, err := d.store.Edge(sourceHash, targetHash) if err != nil { return Edge[T]{}, err } sourceVertex, _, err := d.store.Vertex(sourceHash) if err != nil { return Edge[T]{}, err } targetVertex, _, err := d.store.Vertex(targetHash) if err != nil { return Edge[T]{}, err } return Edge[T]{ Source: sourceVertex, Target: targetVertex, Properties: EdgeProperties{ Weight: edge.Properties.Weight, Attributes: edge.Properties.Attributes, Data: edge.Properties.Data, }, }, nil } func (d *directed[K, T]) Edges() ([]Edge[K], error) { return d.store.ListEdges() } func (d *directed[K, T]) UpdateEdge(source, target K, options ...func(properties *EdgeProperties)) error { existingEdge, err := d.store.Edge(source, target) if err != nil { return err } for _, option := range options { option(&existingEdge.Properties) } return d.store.UpdateEdge(source, target, existingEdge) } func (d *directed[K, T]) RemoveEdge(source, target K) error { if _, err := d.Edge(source, target); err != nil { return err } if err := d.store.RemoveEdge(source, target); err != nil { return fmt.Errorf("failed to remove edge from %v to %v: %w", source, target, err) } return nil } func (d *directed[K, T]) AdjacencyMap() (map[K]map[K]Edge[K], error) { vertices, err := d.store.ListVertices() if err != nil { return nil, fmt.Errorf("failed to list vertices: %w", err) } edges, err := d.store.ListEdges() if err != nil { return nil, fmt.Errorf("failed to list edges: %w", err) } m := make(map[K]map[K]Edge[K], len(vertices)) for _, vertex := range vertices { m[vertex] = make(map[K]Edge[K]) } for _, edge := range edges { m[edge.Source][edge.Target] = edge } return m, nil } func (d *directed[K, T]) PredecessorMap() (map[K]map[K]Edge[K], error) { vertices, err := d.store.ListVertices() if err != nil { return nil, fmt.Errorf("failed to list vertices: %w", err) } edges, err := d.store.ListEdges() if err != nil { return nil, fmt.Errorf("failed to list edges: %w", err) } m := make(map[K]map[K]Edge[K], len(vertices)) for _, vertex := range vertices { m[vertex] = make(map[K]Edge[K]) } for _, edge := range edges { if _, ok := m[edge.Target]; !ok { m[edge.Target] = make(map[K]Edge[K]) } m[edge.Target][edge.Source] = edge } return m, nil } func (d *directed[K, T]) addEdge(sourceHash, targetHash K, edge Edge[K]) error { return d.store.AddEdge(sourceHash, targetHash, edge) } func (d *directed[K, T]) Clone() (Graph[K, T], error) { traits := &Traits{ IsDirected: d.traits.IsDirected, IsAcyclic: d.traits.IsAcyclic, IsWeighted: d.traits.IsWeighted, IsRooted: d.traits.IsRooted, PreventCycles: d.traits.PreventCycles, } clone := &directed[K, T]{ hash: d.hash, traits: traits, store: newMemoryStore[K, T](), } if err := clone.AddVerticesFrom(d); err != nil { return nil, fmt.Errorf("failed to add vertices: %w", err) } if err := clone.AddEdgesFrom(d); err != nil { return nil, fmt.Errorf("failed to add edges: %w", err) } return clone, nil } func (d *directed[K, T]) Order() (int, error) { return d.store.VertexCount() } func (d *directed[K, T]) Size() (int, error) { size := 0 outEdges, err := d.AdjacencyMap() if err != nil { return 0, fmt.Errorf("failed to get adjacency map: %w", err) } for _, outEdges := range outEdges { size += len(outEdges) } return size, nil } func (d *directed[K, T]) edgesAreEqual(a, b Edge[T]) bool { aSourceHash := d.hash(a.Source) aTargetHash := d.hash(a.Target) bSourceHash := d.hash(b.Source) bTargetHash := d.hash(b.Target) return aSourceHash == bSourceHash && aTargetHash == bTargetHash } func (d *directed[K, T]) createsCycle(source, target K) (bool, error) { // If the underlying store implements CreatesCycle, use that fast path. if cc, ok := d.store.(interface { CreatesCycle(source, target K) (bool, error) }); ok { return cc.CreatesCycle(source, target) } // Slow path. return CreatesCycle(Graph[K, T](d), source, target) } // copyEdge returns an argument list suitable for the Graph.AddEdge method. This // argument list is derived from the given edge, hence the name copyEdge. // // The last argument is a custom functional option that sets the edge properties // to the properties of the original edge. func copyEdge[K comparable](edge Edge[K]) (K, K, func(properties *EdgeProperties)) { copyProperties := func(p *EdgeProperties) { for k, v := range edge.Properties.Attributes { p.Attributes[k] = v } p.Weight = edge.Properties.Weight p.Data = edge.Properties.Data } return edge.Source, edge.Target, copyProperties } graph-0.23.0/directed_test.go000066400000000000000000001060331445126167600160650ustar00rootroot00000000000000package graph import ( "errors" "testing" ) func TestDirected_Traits(t *testing.T) { tests := map[string]struct { traits *Traits expected *Traits }{ "default traits": { traits: &Traits{}, expected: &Traits{}, }, "directed": { traits: &Traits{IsDirected: true}, expected: &Traits{IsDirected: true}, }, } for name, test := range tests { g := newDirected(IntHash, test.traits, newMemoryStore[int, int]()) traits := g.Traits() if !traitsAreEqual(traits, test.expected) { t.Errorf("%s: traits expectancy doesn't match: expected %v, got %v", name, test.expected, traits) } } } func TestDirected_AddVertex(t *testing.T) { tests := map[string]struct { vertices []int properties *VertexProperties expectedVertices []int expectedProperties *VertexProperties // Even though some AddVertex calls might work, at least one of them // could fail, e.g. if the last call would add an existing vertex. finallyExpectedError error }{ "graph with 3 vertices": { vertices: []int{1, 2, 3}, properties: &VertexProperties{ Attributes: map[string]string{"color": "red"}, Weight: 10, }, expectedVertices: []int{1, 2, 3}, expectedProperties: &VertexProperties{ Attributes: map[string]string{"color": "red"}, Weight: 10, }, }, "graph with duplicated vertex": { vertices: []int{1, 2, 2}, expectedVertices: []int{1, 2}, finallyExpectedError: ErrVertexAlreadyExists, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) var err error for _, vertex := range test.vertices { if test.properties == nil { err = graph.AddVertex(vertex) continue } // If there are vertex attributes, iterate over them and call the // VertexAttribute functional option for each entry. A vertex should // only have one attribute so that AddVertex is invoked once. for key, value := range test.properties.Attributes { err = graph.AddVertex(vertex, VertexWeight(test.properties.Weight), VertexAttribute(key, value)) } } if !errors.Is(err, test.finallyExpectedError) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.finallyExpectedError, err) } graphStore := graph.store.(*memoryStore[int, int]) for _, vertex := range test.vertices { if len(graphStore.vertices) != len(test.expectedVertices) { t.Errorf("%s: vertex count doesn't match: expected %v, got %v", name, len(test.expectedVertices), len(graphStore.vertices)) } hash := graph.hash(vertex) if _, _, err := graph.store.Vertex(hash); err != nil { vertices := graphStore.vertices t.Errorf("%s: vertex %v not found in graph: %v", name, vertex, vertices) } if test.properties == nil { continue } if graphStore.vertexProperties[hash].Weight != test.expectedProperties.Weight { t.Errorf("%s: edge weights don't match: expected weight %v, got %v", name, test.expectedProperties.Weight, graphStore.vertexProperties[hash].Weight) } if len(graphStore.vertexProperties[hash].Attributes) != len(test.expectedProperties.Attributes) { t.Fatalf("%s: attributes lengths don't match: expcted %v, got %v", name, len(test.expectedProperties.Attributes), len(graphStore.vertexProperties[hash].Attributes)) } for expectedKey, expectedValue := range test.expectedProperties.Attributes { value, ok := graphStore.vertexProperties[hash].Attributes[expectedKey] if !ok { t.Errorf("%s: attribute keys don't match: expected key %v not found", name, expectedKey) } if value != expectedValue { t.Errorf("%s: attribute values don't match: expected value %v for key %v, got %v", name, expectedValue, expectedKey, value) } } } } } func TestDirected_AddVerticesFrom(t *testing.T) { tests := map[string]struct { vertices []int properties map[int]VertexProperties existingVertices []int expectedVertices []int expectedProperties map[int]VertexProperties expectedError error }{ "graph with 3 vertices": { vertices: []int{1, 2, 3}, properties: map[int]VertexProperties{ 1: { Attributes: map[string]string{"color": "red"}, Weight: 10, }, 2: { Attributes: map[string]string{"color": "green"}, Weight: 20, }, 3: { Attributes: map[string]string{"color": "blue"}, Weight: 30, }, }, existingVertices: []int{}, expectedVertices: []int{1, 2, 3}, expectedProperties: map[int]VertexProperties{ 1: { Attributes: map[string]string{"color": "red"}, Weight: 10, }, 2: { Attributes: map[string]string{"color": "green"}, Weight: 20, }, 3: { Attributes: map[string]string{"color": "blue"}, Weight: 30, }, }, }, "graph with duplicated vertex": { vertices: []int{1, 2, 3}, properties: map[int]VertexProperties{}, existingVertices: []int{2}, expectedVertices: []int{1}, expectedProperties: map[int]VertexProperties{}, expectedError: ErrVertexAlreadyExists, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { source := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = source.AddVertex(vertex, copyVertexProperties(test.properties[vertex])) } g := New(IntHash, Directed()) for _, vertex := range test.existingVertices { _ = g.AddVertex(vertex) } err := g.AddVerticesFrom(source) if !errors.Is(err, test.expectedError) { t.Errorf("expected error %v, got %v", test.expectedError, err) } if err != nil { return } for _, vertex := range test.expectedVertices { _, actualProperties, err := g.VertexWithProperties(vertex) if err != nil { t.Errorf("failed to get vertex %v with properties: %v", vertex, err.Error()) return } if expectedProperties, ok := test.expectedProperties[vertex]; ok { if !vertexPropertiesAreEqual(expectedProperties, actualProperties) { t.Errorf("expected properties %v for %v, got %v", expectedProperties, vertex, actualProperties) } } } }) } } func TestDirected_Vertex(t *testing.T) { tests := map[string]struct { vertices []int vertex int expectedError error }{ "existing vertex": { vertices: []int{1, 2, 3}, vertex: 2, }, "non-existent vertex": { vertices: []int{1, 2, 3}, vertex: 4, expectedError: ErrVertexNotFound, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } vertex, err := graph.Vertex(test.vertex) if !errors.Is(err, test.expectedError) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } if test.expectedError != nil { continue } if vertex != test.vertex { t.Errorf("%s: vertex expectancy doesn't match: expected %v, got %v", name, test.vertex, vertex) } } } func TestDirected_RemoveVertex(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] vertex int expectedError error }{ "existing disconnected vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 2, Target: 3}, }, vertex: 1, }, "existing connected vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, }, vertex: 1, expectedError: ErrVertexHasEdges, }, "non-existent vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{}, vertex: 4, expectedError: ErrVertexNotFound, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { _ = graph.AddEdge(edge.Source, edge.Target) } err := graph.RemoveVertex(test.vertex) if !errors.Is(err, test.expectedError) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } } } func TestDirected_AddEdge(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] traits *Traits expectedEdges []Edge[int] // Even though some AddVertex calls might work, at least one of them // could fail, e.g. if the last call would introduce a cycle. finallyExpectedError error }{ "graph with 2 edges": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2, Properties: EdgeProperties{Weight: 10}}, {Source: 1, Target: 3, Properties: EdgeProperties{Weight: 20}}, }, traits: &Traits{}, expectedEdges: []Edge[int]{ {Source: 1, Target: 2, Properties: EdgeProperties{Weight: 10}}, {Source: 1, Target: 3, Properties: EdgeProperties{Weight: 20}}, }, }, "hashes for non-existent vertices": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 3, Properties: EdgeProperties{Weight: 20}}, }, traits: &Traits{}, finallyExpectedError: ErrVertexNotFound, }, "edge introducing a cycle in an acyclic graph": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, traits: &Traits{ PreventCycles: true, }, finallyExpectedError: ErrEdgeCreatesCycle, }, "edge already exists": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, {Source: 3, Target: 1}, }, traits: &Traits{}, finallyExpectedError: ErrEdgeAlreadyExists, }, "edge with attributes": { vertices: []int{1, 2}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Attributes: map[string]string{ "color": "red", }, }, }, }, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Attributes: map[string]string{ "color": "red", }, }, }, }, traits: &Traits{}, }, "edge with data": { vertices: []int{1, 2}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Data: "foo", }, }, }, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Data: "foo", }, }, }, traits: &Traits{}, }, } for name, test := range tests { graph := newDirected(IntHash, test.traits, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } var err error for _, edge := range test.edges { if len(edge.Properties.Attributes) == 0 { err = graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight), EdgeData(edge.Properties.Data)) } // If there are edge attributes, iterate over them and call the // EdgeAttribute functional option for each entry. An edge should // only have one attribute so that AddEdge is invoked once. for key, value := range edge.Properties.Attributes { err = graph.AddEdge( edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight), EdgeData(edge.Properties.Data), EdgeAttribute(key, value), ) } if err != nil { break } } if !errors.Is(err, test.finallyExpectedError) { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v", name, test.finallyExpectedError, err) } for _, expectedEdge := range test.expectedEdges { sourceHash := graph.hash(expectedEdge.Source) targetHash := graph.hash(expectedEdge.Target) edge, err := graph.Edge(sourceHash, targetHash) if err != nil { t.Fatalf("%s: edge with source %v and target %v not found", name, expectedEdge.Source, expectedEdge.Target) } if !edgesAreEqual(expectedEdge, edge, true) { t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, edge) } } } } func TestDirected_AddEdgesFrom(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] existingVertices []int existingEdges []Edge[int] expectedEdges []Edge[int] expectedError error }{ "graph with 3 edges": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, existingVertices: []int{1, 2, 3}, existingEdges: []Edge[int]{}, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, expectedError: nil, }, "edge with non-existing vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 3}, }, existingVertices: []int{1, 2}, existingEdges: []Edge[int]{}, expectedEdges: []Edge[int]{}, expectedError: ErrVertexNotFound, }, "graph with duplicated edge": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 2}, }, existingVertices: []int{1, 2}, existingEdges: []Edge[int]{ {Source: 1, Target: 2}, }, expectedEdges: []Edge[int]{}, expectedError: ErrEdgeAlreadyExists, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { source := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = source.AddVertex(vertex) } for _, edge := range test.edges { _ = source.AddEdge(copyEdge(edge)) } g := New(IntHash, Directed()) for _, vertex := range test.existingVertices { _ = g.AddVertex(vertex) } for _, edge := range test.existingEdges { _ = g.AddEdge(copyEdge(edge)) } err := g.AddEdgesFrom(source) if !errors.Is(err, test.expectedError) { t.Fatalf("expected error %v, got %v", test.expectedError, err) } for _, edge := range test.expectedEdges { actualEdge, err := g.Edge(edge.Source, edge.Target) if err != nil { t.Fatalf("failed to get edge: %v", err.Error()) } if !edgesAreEqual(edge, actualEdge, true) { t.Errorf("expected edge %v, got %v", edge, actualEdge) } } }) } } func TestDirected_Edge(t *testing.T) { tests := map[string]struct { vertices []int edge Edge[int] args [2]int expectedError error }{ "get edge of undirected graph": { vertices: []int{1, 2, 3}, edge: Edge[int]{Source: 1, Target: 2}, args: [2]int{1, 2}, }, "get edge of undirected graph with swapped source and target": { vertices: []int{1, 2, 3}, edge: Edge[int]{Source: 1, Target: 2}, args: [2]int{2, 1}, expectedError: ErrEdgeNotFound, }, "get non-existent edge of undirected graph": { vertices: []int{1, 2, 3}, edge: Edge[int]{Source: 1, Target: 2}, args: [2]int{2, 3}, expectedError: ErrEdgeNotFound, }, "get edge with properties": { vertices: []int{1, 2, 3}, edge: Edge[int]{ Source: 1, Target: 2, Properties: EdgeProperties{ // Attributes can't be tested at the moment, because there // is no way to add multiple attributes at once (using a // functional option like EdgeAttributes). // ToDo: Add Attributes once EdgeAttributes exists. Attributes: map[string]string{}, Weight: 10, Data: "this is an edge", }, }, args: [2]int{1, 2}, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } if err := graph.AddEdge(test.edge.Source, test.edge.Target, EdgeWeight(test.edge.Properties.Weight), EdgeData(test.edge.Properties.Data)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } edge, err := graph.Edge(test.args[0], test.args[1]) if !errors.Is(err, test.expectedError) { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } if test.expectedError != nil { continue } if edge.Source != test.args[0] { t.Errorf("%s: source expectancy doesn't match: expected %v, got %v", name, test.args[0], edge.Source) } if edge.Target != test.args[1] { t.Errorf("%s: target expectancy doesn't match: expected %v, got %v", name, test.args[1], edge.Target) } if !edgesAreEqual(test.edge, edge, true) { t.Errorf("%s: expected edge %v, got %v", name, test.edge, edge) } } } func TestDirected_Edges(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expectedEdges []Edge[int] }{ "graph with 3 edges": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { g := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = g.AddVertex(vertex) } for _, edge := range test.edges { _ = g.AddEdge(copyEdge(edge)) } edges, err := g.Edges() if err != nil { t.Fatalf("unexpected error: %v", err.Error()) } for _, expectedEdge := range test.expectedEdges { for _, actualEdge := range edges { if actualEdge.Source != expectedEdge.Source || actualEdge.Target != expectedEdge.Target { continue } if !edgesAreEqual(expectedEdge, actualEdge, true) { t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, actualEdge) } } } }) } } func TestDirected_UpdateEdge(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] updateEdge Edge[int] expectedErr error }{ "update an edge": { vertices: []int{1, 2}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, Data: "my-edge", }, }, }, updateEdge: Edge[int]{ Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "blue", "label": "a blue edge", }, Data: "my-updated-edge", }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { g := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = g.AddVertex(vertex) } for _, edge := range test.edges { _ = g.AddEdge(copyEdge(edge)) } err := g.UpdateEdge(copyEdge(test.updateEdge)) if !errors.Is(err, test.expectedErr) { t.Fatalf("expected error %v, got %v", test.expectedErr, err) } actualEdge, err := g.Edge(test.updateEdge.Source, test.updateEdge.Target) if err != nil { t.Fatalf("unexpected error: %v", err.Error()) } if !edgesAreEqual(test.updateEdge, actualEdge, true) { t.Errorf("expected edge %v, got %v", test.updateEdge, actualEdge) } }) } } func TestDirected_RemoveEdge(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] removeEdges []Edge[int] expectedError error }{ "two-vertices graph": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 2}, }, removeEdges: []Edge[int]{ {Source: 1, Target: 2}, }, }, "remove 2 edges from triangle": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 3}, }, removeEdges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, }, }, "remove non-existent edge": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, }, removeEdges: []Edge[int]{ {Source: 2, Target: 3}, }, expectedError: ErrEdgeNotFound, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } for _, removeEdge := range test.removeEdges { if err := graph.RemoveEdge(removeEdge.Source, removeEdge.Target); !errors.Is(err, test.expectedError) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } // After removing the edge, verify that it can't be retrieved using // Edge anymore. if _, err := graph.Edge(removeEdge.Source, removeEdge.Target); err != ErrEdgeNotFound { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v", name, ErrEdgeNotFound, err) } } } } func TestDirected_AdjacencyList(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expected map[int]map[int]Edge[int] }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: { 3: {Source: 1, Target: 3}, }, 2: { 3: {Source: 2, Target: 3}, }, 3: { 4: {Source: 3, Target: 4}, }, 4: {}, }, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: { 2: {Source: 1, Target: 2}, 3: {Source: 1, Target: 3}, }, 2: { 4: {Source: 2, Target: 4}, }, 3: { 4: {Source: 3, Target: 4}, }, 4: {}, }, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } adjacencyMap, _ := graph.AdjacencyMap() for expectedVertex, expectedAdjacencies := range test.expected { adjacencies, ok := adjacencyMap[expectedVertex] if !ok { t.Errorf("%s: expected vertex %v does not exist in adjacency map", name, expectedVertex) } for expectedAdjacency, expectedEdge := range expectedAdjacencies { edge, ok := adjacencies[expectedAdjacency] if !ok { t.Errorf("%s: expected adjacency %v does not exist in map of %v", name, expectedAdjacency, expectedVertex) } if edge.Source != expectedEdge.Source || edge.Target != expectedEdge.Target { t.Errorf("%s: edge expectancy doesn't match: expected %v, got %v", name, expectedEdge, edge) } } } } } func TestDirected_PredecessorMap(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expected map[int]map[int]Edge[int] }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: {}, 2: {}, 3: { 1: {Source: 1, Target: 3}, 2: {Source: 2, Target: 3}, }, 4: { 3: {Source: 3, Target: 4}, }, }, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: {}, 2: { 1: {Source: 1, Target: 2}, }, 3: { 1: {Source: 1, Target: 3}, }, 4: { 2: {Source: 2, Target: 4}, 3: {Source: 3, Target: 4}, }, }, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } predecessors, _ := graph.PredecessorMap() for expectedVertex, expectedPredecessors := range test.expected { predecessors, ok := predecessors[expectedVertex] if !ok { t.Errorf("%s: expected vertex %v does not exist in adjacency map", name, expectedVertex) } for expectedPredecessor, expectedEdge := range expectedPredecessors { edge, ok := predecessors[expectedPredecessor] if !ok { t.Errorf("%s: expected adjacency %v does not exist in map of %v", name, expectedPredecessor, expectedVertex) } if edge.Source != expectedEdge.Source || edge.Target != expectedEdge.Target { t.Errorf("%s: edge expectancy doesn't match: expected %v, got %v", name, expectedEdge, edge) } } } } } func TestDirected_Clone(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex, VertexWeight(vertex), VertexAttribute("color", "red")) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } clonedGraph, err := graph.Clone() if err != nil { t.Fatalf("%s: failed to clone graph: %s", name, err.Error()) } expected := graph.(*directed[int, int]) actual := clonedGraph.(*directed[int, int]) if actual.hash(5) != expected.hash(5) { t.Errorf("%s: hash expectancy doesn't match: expected %v, got %v", name, expected.hash, actual.hash) } if !traitsAreEqual(actual.traits, expected.traits) { t.Errorf("%s: traits expectancy doesn't match: expected %v, got %v", name, expected.traits, actual.traits) } expectedAdjacencyMap, _ := graph.AdjacencyMap() actualAdjacencyMap, _ := actual.AdjacencyMap() if !adjacencyMapsAreEqual(expectedAdjacencyMap, actualAdjacencyMap, expected.edgesAreEqual) { t.Errorf("%s: expected adjacency map %v, got %v", name, expectedAdjacencyMap, actualAdjacencyMap) } _ = clonedGraph.AddVertex(10) if _, err := graph.Vertex(10); err == nil { t.Errorf("%s: vertex 10 shouldn't exist in original graph", name) } } } func TestDirected_OrderAndSize(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expectedOrder int expectedSize int }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, expectedOrder: 4, expectedSize: 3, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, expectedOrder: 4, expectedSize: 4, }, "two-vertices two-edges graph": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 1}, }, expectedOrder: 2, expectedSize: 2, }, "edgeless graph": { vertices: []int{1, 2}, edges: []Edge[int]{}, expectedOrder: 2, expectedSize: 0, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } order, _ := graph.Order() size, _ := graph.Size() if order != test.expectedOrder { t.Errorf("%s: order expectancy doesn't match: expected %d, got %d", name, test.expectedOrder, order) } if size != test.expectedSize { t.Errorf("%s: size expectancy doesn't match: expected %d, got %d", name, test.expectedSize, size) } } } func TestDirected_edgesAreEqual(t *testing.T) { tests := map[string]struct { a Edge[int] b Edge[int] edgesAreEqual bool }{ "equal edges in directed graph": { a: Edge[int]{Source: 1, Target: 2}, b: Edge[int]{Source: 1, Target: 2}, edgesAreEqual: true, }, "swapped equal edges in directed graph": { a: Edge[int]{Source: 1, Target: 2}, b: Edge[int]{Source: 2, Target: 1}, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) actual := graph.edgesAreEqual(test.a, test.b) if actual != test.edgesAreEqual { t.Errorf("%s: equality expectations don't match: expected %v, got %v", name, test.edgesAreEqual, actual) } } } func TestDirected_addEdge(t *testing.T) { tests := map[string]struct { edges []Edge[int] }{ "add 3 edges": { edges: []Edge[int]{ {Source: 1, Target: 2, Properties: EdgeProperties{Weight: 1}}, {Source: 2, Target: 3, Properties: EdgeProperties{Weight: 2}}, {Source: 3, Target: 1, Properties: EdgeProperties{Weight: 3}}, }, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, edge := range test.edges { sourceHash := graph.hash(edge.Source) TargetHash := graph.hash(edge.Target) err := graph.addEdge(sourceHash, TargetHash, edge) if err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } outEdges := graph.store.(*memoryStore[int, int]).outEdges if len(outEdges) != len(test.edges) { t.Errorf("%s: number of outgoing edges doesn't match: expected %v, got %v", name, len(test.edges), len(outEdges)) } inEdges := graph.store.(*memoryStore[int, int]).inEdges if len(inEdges) != len(test.edges) { t.Errorf("%s: number of ingoing edges doesn't match: expected %v, got %v", name, len(test.edges), len(inEdges)) } } } func TestDirected_predecessors(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] vertex int expectedPredecessors []int }{ "graph with 3 vertices": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, }, vertex: 2, expectedPredecessors: []int{1}, }, "graph with 6 vertices": { vertices: []int{1, 2, 3, 4, 5, 6}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 6}, }, vertex: 5, expectedPredecessors: []int{2}, }, "graph with 4 vertices and 3 predecessors": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 4}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, vertex: 4, expectedPredecessors: []int{1, 2, 3}, }, } for name, test := range tests { graph := newDirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } predecessors, _ := predecessors(graph, graph.hash(test.vertex)) if !slicesAreEqual(predecessors, test.expectedPredecessors) { t.Errorf("%s: predecessors don't match: expected %v, got %v", name, test.expectedPredecessors, predecessors) } } } func slicesAreEqual[T comparable](a, b []T) bool { if len(a) != len(b) { return false } for _, aValue := range a { found := false for _, bValue := range b { if aValue == bValue { found = true } } if !found { return false } } return true } func vertexPropertiesAreEqual(a, b VertexProperties) bool { if a.Weight != b.Weight { return false } // A length check is required because in the iteration below, a.Attributes // could be empty and thus circumvent the comparison. if len(a.Attributes) != len(b.Attributes) { return false } for key, aValue := range a.Attributes { bValue, ok := b.Attributes[key] if !ok || aValue != bValue { return false } } return true } func edgesAreEqual[K comparable](a, b Edge[K], directed bool) bool { directedOk := a.Source == b.Source && a.Target == b.Target undirectedOk := directedOk || a.Source == b.Target && a.Target == b.Source if directed && !directedOk { return false } if !directed && !undirectedOk { return false } if a.Properties.Weight != b.Properties.Weight { return false } if len(a.Properties.Attributes) != len(b.Properties.Attributes) { return false } for aKey, aValue := range a.Properties.Attributes { bValue, ok := b.Properties.Attributes[aKey] if !ok { return false } if bValue != aValue { return false } } for bKey := range b.Properties.Attributes { if _, ok := a.Properties.Attributes[bKey]; !ok { return false } } return true } func predecessors[K comparable, T any](g *directed[K, T], vertexHash K) ([]K, error) { var predecessorHashes []K predecessorMap, err := g.PredecessorMap() if err != nil { return nil, err } for _, edge := range predecessorMap[vertexHash] { predecessorHashes = append(predecessorHashes, edge.Source) } return predecessorHashes, nil } graph-0.23.0/draw/000077500000000000000000000000001445126167600136465ustar00rootroot00000000000000graph-0.23.0/draw/draw.go000066400000000000000000000077241445126167600151440ustar00rootroot00000000000000// Package draw provides functions for visualizing graph structures. At this // time, draw supports the DOT language which can be interpreted by Graphviz, // Grappa, and others. package draw import ( "fmt" "io" "text/template" "github.com/dominikbraun/graph" ) // ToDo: This template should be simplified and split into multiple templates. const dotTemplate = `strict {{.GraphType}} { {{range $k, $v := .Attributes}} {{$k}}="{{$v}}"; {{end}} {{range $s := .Statements}} "{{.Source}}" {{if .Target}}{{$.EdgeOperator}} "{{.Target}}" [ {{range $k, $v := .EdgeAttributes}}{{$k}}="{{$v}}", {{end}} weight={{.EdgeWeight}} ]{{else}}[ {{range $k, $v := .SourceAttributes}}{{$k}}="{{$v}}", {{end}} weight={{.SourceWeight}} ]{{end}}; {{end}} } ` type description struct { GraphType string Attributes map[string]string EdgeOperator string Statements []statement } type statement struct { Source interface{} Target interface{} SourceWeight int SourceAttributes map[string]string EdgeWeight int EdgeAttributes map[string]string } // DOT renders the given graph structure in DOT language into an io.Writer, for // example a file. The generated output can be passed to Graphviz or other // visualization tools supporting DOT. // // The following example renders a directed graph into a file my-graph.gv: // // g := graph.New(graph.IntHash, graph.Directed()) // // _ = g.AddVertex(1) // _ = g.AddVertex(2) // _ = g.AddVertex(3, graph.VertexAttribute("style", "filled"), graph.VertexAttribute("fillcolor", "red")) // // _ = g.AddEdge(1, 2, graph.EdgeWeight(10), graph.EdgeAttribute("color", "red")) // _ = g.AddEdge(1, 3) // // file, _ := os.Create("./my-graph.gv") // _ = draw.DOT(g, file) // // To generate an SVG from the created file using Graphviz, use a command such // as the following: // // dot -Tsvg -O my-graph.gv // // Another possibility is to use os.Stdout as an io.Writer, print the DOT output // to stdout, and pipe it as follows: // // go run main.go | dot -Tsvg > output.svg // // DOT also accepts the [GraphAttribute] functional option, which can be used to // add global attributes when rendering the graph: // // _ = draw.DOT(g, file, draw.GraphAttribute("label", "my-graph")) func DOT[K comparable, T any](g graph.Graph[K, T], w io.Writer, options ...func(*description)) error { desc, err := generateDOT(g, options...) if err != nil { return fmt.Errorf("failed to generate DOT description: %w", err) } return renderDOT(w, desc) } // GraphAttribute is a functional option for the [DOT] method. func GraphAttribute(key, value string) func(*description) { return func(d *description) { d.Attributes[key] = value } } func generateDOT[K comparable, T any](g graph.Graph[K, T], options ...func(*description)) (description, error) { desc := description{ GraphType: "graph", Attributes: make(map[string]string), EdgeOperator: "--", Statements: make([]statement, 0), } for _, option := range options { option(&desc) } if g.Traits().IsDirected { desc.GraphType = "digraph" desc.EdgeOperator = "->" } adjacencyMap, err := g.AdjacencyMap() if err != nil { return desc, err } for vertex, adjacencies := range adjacencyMap { _, sourceProperties, err := g.VertexWithProperties(vertex) if err != nil { return desc, err } stmt := statement{ Source: vertex, SourceWeight: sourceProperties.Weight, SourceAttributes: sourceProperties.Attributes, } desc.Statements = append(desc.Statements, stmt) for adjacency, edge := range adjacencies { stmt := statement{ Source: vertex, Target: adjacency, EdgeWeight: edge.Properties.Weight, EdgeAttributes: edge.Properties.Attributes, } desc.Statements = append(desc.Statements, stmt) } } return desc, nil } func renderDOT(w io.Writer, d description) error { tpl, err := template.New("dotTemplate").Parse(dotTemplate) if err != nil { return fmt.Errorf("failed to parse template: %w", err) } return tpl.Execute(w, d) } graph-0.23.0/draw/draw_test.go000066400000000000000000000262071445126167600162000ustar00rootroot00000000000000package draw import ( "bytes" "strings" "testing" "github.com/dominikbraun/graph" ) func TestGenerateDOT(t *testing.T) { tests := map[string]struct { graph graph.Graph[string, string] attributes map[string]string vertices []string vertexProperties map[string]graph.VertexProperties edges []graph.Edge[string] expected description }{ "3-vertex directed graph": { graph: graph.New(graph.StringHash, graph.Directed()), attributes: map[string]string{}, vertices: []string{"1", "2", "3"}, edges: []graph.Edge[string]{ {Source: "1", Target: "2"}, {Source: "1", Target: "3"}, }, expected: description{ GraphType: "digraph", Attributes: map[string]string{}, EdgeOperator: "->", Statements: []statement{ {Source: "1", Target: "2"}, {Source: "1", Target: "3"}, {Source: "1"}, {Source: "2"}, {Source: "3"}, }, }, }, "3-vertex directed, weighted graph with weights and attributes": { graph: graph.New(graph.StringHash, graph.Directed(), graph.Weighted()), attributes: map[string]string{}, vertices: []string{"1", "2", "3"}, edges: []graph.Edge[string]{ { Source: "1", Target: "2", Properties: graph.EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, {Source: "1", Target: "3"}, }, expected: description{ GraphType: "digraph", Attributes: map[string]string{}, EdgeOperator: "->", Statements: []statement{ { Source: "1", Target: "2", EdgeWeight: 10, EdgeAttributes: map[string]string{ "color": "red", }, }, {Source: "1", Target: "3"}, {Source: "1"}, {Source: "2"}, {Source: "3"}, }, }, }, "vertices with attributes": { graph: graph.New(graph.StringHash, graph.Directed(), graph.Weighted()), attributes: map[string]string{}, vertices: []string{"1", "2"}, vertexProperties: map[string]graph.VertexProperties{ "1": { Attributes: map[string]string{ "color": "red", }, Weight: 10, }, "2": { Attributes: map[string]string{ "color": "blue", }, Weight: 20, }, }, edges: []graph.Edge[string]{ {Source: "1", Target: "2"}, }, expected: description{ GraphType: "digraph", Attributes: map[string]string{}, EdgeOperator: "->", Statements: []statement{ { Source: "1", SourceAttributes: map[string]string{ "color": "red", }, SourceWeight: 10, }, { Source: "2", SourceAttributes: map[string]string{ "color": "blue", }, SourceWeight: 20, }, { Source: "1", Target: "2", }, }, }, }, "directed graph with attributes": { graph: graph.New(graph.StringHash, graph.Directed()), attributes: map[string]string{ "label": "my-graph", "normalize": "true", "compound": "false", }, vertices: []string{"1", "2"}, edges: []graph.Edge[string]{ {Source: "1", Target: "2"}, }, expected: description{ GraphType: "digraph", Attributes: map[string]string{ "label": "my-graph", "normalize": "true", "compound": "false", }, EdgeOperator: "->", Statements: []statement{ { Source: "1", }, { Source: "2", }, { Source: "1", Target: "2", }, }, }, }, } for name, test := range tests { for _, vertex := range test.vertices { if test.vertexProperties == nil { _ = test.graph.AddVertex(vertex) continue } // If there are vertex attributes, iterate over them and call // VertexAttribute for each entry. A vertex should only have one // attribute so that AddVertex is invoked once. for key, value := range test.vertexProperties[vertex].Attributes { weight := test.vertexProperties[vertex].Weight // ToDo: Clarify how multiple functional options and attributes can be tested. _ = test.graph.AddVertex(vertex, graph.VertexWeight(weight), graph.VertexAttribute(key, value)) } } for _, edge := range test.edges { var err error if len(edge.Properties.Attributes) == 0 { err = test.graph.AddEdge(edge.Source, edge.Target, graph.EdgeWeight(edge.Properties.Weight)) } // If there are edge attributes, iterate over them and call // EdgeAttribute for each entry. An edge should only have one // attribute so that AddEdge is invoked once. for key, value := range edge.Properties.Attributes { err = test.graph.AddEdge(edge.Source, edge.Target, graph.EdgeWeight(edge.Properties.Weight), graph.EdgeAttribute(key, value)) } if err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } desc, _ := generateDOT(test.graph) // Add the graph attributes manually instead of using the functional // option. This is the reason why I dislike them more and more. desc.Attributes = test.attributes if desc.GraphType != test.expected.GraphType { t.Errorf("%s: graph type expectancy doesn't match: expected %v, got %v", name, test.expected.GraphType, desc.GraphType) } if desc.EdgeOperator != test.expected.EdgeOperator { t.Errorf("%s: edge operator expectancy doesn't match: expected %v, got %v", name, test.expected.EdgeOperator, desc.EdgeOperator) } if !slicesAreEqual(desc.Statements, test.expected.Statements, statementsAreEqual) { t.Errorf("%s: statements expectancy doesn't match: expected %v, got %v", name, test.expected.Statements, desc.Statements) } stringsAreEqual := func(a, b string) bool { return a == b } if !mapsAreEqual(desc.Attributes, test.expected.Attributes, stringsAreEqual) { t.Errorf("%s: attributes expectancy doesn't match: expected %v, got %v", name, test.expected.Attributes, desc.Attributes) } } } func TestRenderDOT(t *testing.T) { tests := map[string]struct { description description expected string }{ "3-vertex directed graph": { description: description{ GraphType: "digraph", Attributes: map[string]string{}, EdgeOperator: "->", Statements: []statement{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 1}, {Source: 2}, {Source: 3}, }, }, expected: `strict digraph { "1" -> "2" [ weight=0 ]; "1" -> "3" [ weight=0 ]; "1" [ weight=0 ]; "2" [ weight=0 ]; "3" [ weight=0 ]; }`, }, "custom edge attributes": { description: description{ GraphType: "digraph", Attributes: map[string]string{}, EdgeOperator: "->", Statements: []statement{ { Source: 1, Target: 2, EdgeAttributes: map[string]string{ "color": "red", }, }, { Source: 1, Target: 3, EdgeAttributes: map[string]string{ "color": "blue", }, }, {Source: 1}, {Source: 2}, {Source: 3}, }, }, expected: `strict digraph { "1" -> "2" [ color="red", weight=0 ]; "1" -> "3" [ color="blue", weight=0 ]; "1" [ weight=0 ]; "2" [ weight=0 ]; "3" [ weight=0 ]; }`, }, "vertices containing special characters": { description: description{ GraphType: "digraph", Attributes: map[string]string{}, EdgeOperator: "->", Statements: []statement{ {Source: "/home", Target: "projects/graph"}, {Source: "/home", Target: ".config"}, {Source: ".config", Target: "my file.txt"}, }, }, expected: `strict digraph { "/home" -> "projects/graph" [ weight=0 ]; "/home" -> ".config" [ weight=0 ]; ".config" -> "my file.txt" [ weight=0 ]; }`, }, "vertices with attributes": { description: description{ GraphType: "digraph", Attributes: map[string]string{}, EdgeOperator: "->", Statements: []statement{ { Source: "1", SourceAttributes: map[string]string{ "color": "red", }, SourceWeight: 10, }, { Source: "2", SourceAttributes: map[string]string{ "color": "blue", }, SourceWeight: 20, }, { Source: "1", Target: "2", }, }, }, expected: `strict digraph { "1" [ color="red", weight=10 ]; "2" [ color="blue", weight=20 ]; "1" -> "2" [ weight=0 ]; }`, }, "3-vertex directed graph with attributes": { description: description{ GraphType: "digraph", Attributes: map[string]string{ "label": "my-graph", }, EdgeOperator: "->", Statements: []statement{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 1}, {Source: 2}, {Source: 3}, }, }, expected: `strict digraph { label="my-graph"; "1" -> "2" [ weight=0 ]; "1" -> "3" [ weight=0 ]; "1" [ weight=0 ]; "2" [ weight=0 ]; "3" [ weight=0 ]; }`, }, } for name, test := range tests { buf := new(bytes.Buffer) _ = renderDOT(buf, test.description) output := normalizeOutput(buf.String()) expected := normalizeOutput(test.expected) if output != expected { t.Errorf("%s: DOT output expectancy doesn't match: expected %v, got %v", name, expected, output) } } } func TestGraphAttribute(t *testing.T) { tests := map[string]struct { attribute [2]string expected *description }{ "label attribute": { attribute: [2]string{"label", "my-graph"}, expected: &description{ Attributes: map[string]string{ "label": "my-graph", }, }, }, } for name, test := range tests { d := &description{ Attributes: make(map[string]string), } GraphAttribute(test.attribute[0], test.attribute[1])(d) stringsAreEqual := func(a, b string) bool { return a == b } if !mapsAreEqual(test.expected.Attributes, d.Attributes, stringsAreEqual) { t.Errorf("%s: graph attribute expectation doesn't match: expected %v, got %v", name, test.expected.Attributes, d.Attributes) } } } func slicesAreEqual[T any](a, b []T, equals func(a, b T) bool) bool { if len(a) != len(b) { return false } for _, aValue := range a { found := false for _, bValue := range b { if equals(aValue, bValue) { found = true } } if !found { return false } } return true } func mapsAreEqual[K comparable, V any](a, b map[K]V, equals func(a, b V) bool) bool { if len(a) != len(b) { return false } for aKey, aValue := range a { bValue, ok := b[aKey] if !ok { return false } if !equals(aValue, bValue) { return false } } return true } func normalizeOutput(output string) string { replacer := strings.NewReplacer(" ", "", "\n", "", "\t", "") return replacer.Replace(output) } func statementsAreEqual(a, b statement) bool { if len(a.EdgeAttributes) != len(b.EdgeAttributes) { return false } for aKey, aValue := range a.EdgeAttributes { bValue, ok := b.EdgeAttributes[aKey] if !ok { return false } if aValue != bValue { return false } } if len(a.SourceAttributes) != len(b.SourceAttributes) { return false } for aKey, aValue := range a.SourceAttributes { bValue, ok := b.SourceAttributes[aKey] if !ok { return false } if aValue != bValue { return false } } return a.Source == b.Source && a.Target == b.Target && a.EdgeWeight == b.EdgeWeight && a.SourceWeight == b.SourceWeight } graph-0.23.0/go.mod000066400000000000000000000000561445126167600140200ustar00rootroot00000000000000module github.com/dominikbraun/graph go 1.18 graph-0.23.0/graph.go000066400000000000000000000344061445126167600143500ustar00rootroot00000000000000// Package graph is a library for creating generic graph data structures and // modifying, analyzing, and visualizing them. // // # Hashes // // A graph consists of vertices of type T, which are identified by a hash value // of type K. The hash value for a given vertex is obtained using the hashing // function passed to [New]. A hashing function takes a T and returns a K. // // For primitive types like integers, you may use a predefined hashing function // such as [IntHash] – a function that takes an integer and uses that integer as // the hash value at the same time: // // g := graph.New(graph.IntHash) // // For storing custom data types, you need to provide your own hashing function. // This example takes a City instance and returns its name as the hash value: // // cityHash := func(c City) string { // return c.Name // } // // Creating a graph using this hashing function will yield a graph of vertices // of type City identified by hash values of type string. // // g := graph.New(cityHash) // // # Operations // // Adding vertices to a graph of integers is simple. [graph.Graph.AddVertex] // takes a vertex and adds it to the graph. // // g := graph.New(graph.IntHash) // // _ = g.AddVertex(1) // _ = g.AddVertex(2) // // Most functions accept and return only hash values instead of entire instances // of the vertex type T. For example, [graph.Graph.AddEdge] creates an edge // between two vertices and accepts the hash values of those vertices. Because // this graph uses the [IntHash] hashing function, the vertex values and hash // values are the same. // // _ = g.AddEdge(1, 2) // // All operations that modify the graph itself are methods of [Graph]. All other // operations are top-level functions of by this library. // // For detailed usage examples, take a look at the README. package graph import "errors" var ( ErrVertexNotFound = errors.New("vertex not found") ErrVertexAlreadyExists = errors.New("vertex already exists") ErrEdgeNotFound = errors.New("edge not found") ErrEdgeAlreadyExists = errors.New("edge already exists") ErrEdgeCreatesCycle = errors.New("edge would create a cycle") ErrVertexHasEdges = errors.New("vertex has edges") ) // Graph represents a generic graph data structure consisting of vertices of // type T identified by a hash of type K. type Graph[K comparable, T any] interface { // Traits returns the graph's traits. Those traits must be set when creating // a graph using New. Traits() *Traits // AddVertex creates a new vertex in the graph. If the vertex already exists // in the graph, ErrVertexAlreadyExists will be returned. // // AddVertex accepts a variety of functional options to set further edge // details such as the weight or an attribute: // // _ = graph.AddVertex("A", "B", graph.VertexWeight(4), graph.VertexAttribute("label", "my-label")) // AddVertex(value T, options ...func(*VertexProperties)) error // AddVerticesFrom adds all vertices along with their properties from the // given graph to the receiving graph. // // All vertices will be added until an error occurs. If one of the vertices // already exists, ErrVertexAlreadyExists will be returned. AddVerticesFrom(g Graph[K, T]) error // Vertex returns the vertex with the given hash or ErrVertexNotFound if it // doesn't exist. Vertex(hash K) (T, error) // VertexWithProperties returns the vertex with the given hash along with // its properties or ErrVertexNotFound if it doesn't exist. VertexWithProperties(hash K) (T, VertexProperties, error) // RemoveVertex removes the vertex with the given hash value from the graph. // // The vertex is not allowed to have edges and thus must be disconnected. // Potential edges must be removed first. Otherwise, ErrVertexHasEdges will // be returned. If the vertex doesn't exist, ErrVertexNotFound is returned. RemoveVertex(hash K) error // AddEdge creates an edge between the source and the target vertex. // // If either vertex cannot be found, ErrVertexNotFound will be returned. If // the edge already exists, ErrEdgeAlreadyExists will be returned. If cycle // prevention has been activated using PreventCycles and if adding the edge // would create a cycle, ErrEdgeCreatesCycle will be returned. // // AddEdge accepts functional options to set further edge properties such as // the weight or an attribute: // // _ = g.AddEdge("A", "B", graph.EdgeWeight(4), graph.EdgeAttribute("label", "my-label")) // AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error // AddEdgesFrom adds all edges along with their properties from the given // graph to the receiving graph. // // All vertices that the edges are joining have to exist already. If needed, // these vertices can be added using AddVerticesFrom first. Depending on the // situation, it also might make sense to clone the entire original graph. AddEdgesFrom(g Graph[K, T]) error // Edge returns the edge joining two given vertices or ErrEdgeNotFound if // the edge doesn't exist. In an undirected graph, an edge with swapped // source and target vertices does match. Edge(sourceHash, targetHash K) (Edge[T], error) // Edges returns a slice of all edges in the graph. These edges are of type // Edge[K] and hence will contain the vertex hashes, not the vertex values. Edges() ([]Edge[K], error) // UpdateEdge updates the edge joining the two given vertices with the data // provided in the given functional options. Valid functional options are: // - EdgeWeight: Sets a new weight for the edge properties. // - EdgeAttribute: Adds a new attribute to the edge properties. // - EdgeAttributes: Sets a new attributes map for the edge properties. // - EdgeData: Sets a new Data field for the edge properties. // // UpdateEdge accepts the same functional options as AddEdge. For example, // setting the weight of an edge (A,B) to 10 would look as follows: // // _ = g.UpdateEdge("A", "B", graph.EdgeWeight(10)) // // Removing a particular edge attribute is not possible at the moment. A // workaround is to create a new map without the respective element and // overwrite the existing attributes using the EdgeAttributes option. UpdateEdge(source, target K, options ...func(properties *EdgeProperties)) error // RemoveEdge removes the edge between the given source and target vertices. // If the edge cannot be found, ErrEdgeNotFound will be returned. RemoveEdge(source, target K) error // AdjacencyMap computes an adjacency map with all vertices in the graph. // // There is an entry for each vertex. Each of those entries is another map // whose keys are the hash values of the adjacent vertices. The value is an // Edge instance that stores the source and target hash values along with // the edge metadata. // // For a directed graph with two edges AB and AC, AdjacencyMap would return // the following map: // // map[string]map[string]Edge[string]{ // "A": map[string]Edge[string]{ // "B": {Source: "A", Target: "B"}, // "C": {Source: "A", Target: "C"}, // }, // "B": map[string]Edge[string]{}, // "C": map[string]Edge[string]{}, // } // // This design makes AdjacencyMap suitable for a wide variety of algorithms. AdjacencyMap() (map[K]map[K]Edge[K], error) // PredecessorMap computes a predecessor map with all vertices in the graph. // // It has the same map layout and does the same thing as AdjacencyMap, but // for ingoing instead of outgoing edges of each vertex. // // For a directed graph with two edges AB and AC, PredecessorMap would // return the following map: // // map[string]map[string]Edge[string]{ // "A": map[string]Edge[string]{}, // "B": map[string]Edge[string]{ // "A": {Source: "A", Target: "B"}, // }, // "C": map[string]Edge[string]{ // "A": {Source: "A", Target: "C"}, // }, // } // // For an undirected graph, PredecessorMap is the same as AdjacencyMap. This // is because there is no distinction between "outgoing" and "ingoing" edges // in an undirected graph. PredecessorMap() (map[K]map[K]Edge[K], error) // Clone creates a deep copy of the graph and returns that cloned graph. // // The cloned graph will use the default in-memory store for storing the // vertices and edges. If you want to utilize a custom store instead, create // a new graph using NewWithStore and use AddVerticesFrom and AddEdgesFrom. Clone() (Graph[K, T], error) // Order returns the number of vertices in the graph. Order() (int, error) // Size returns the number of edges in the graph. Size() (int, error) } // Edge represents an edge that joins two vertices. Even though these edges are // always referred to as source and target, whether the graph is directed or not // is determined by its traits. type Edge[T any] struct { Source T Target T Properties EdgeProperties } // EdgeProperties represents a set of properties that each edge possesses. They // can be set when adding a new edge using the corresponding functional options: // // g.AddEdge("A", "B", graph.EdgeWeight(2), graph.EdgeAttribute("color", "red")) // // The example above will create an edge with a weight of 2 and an attribute // "color" with value "red". type EdgeProperties struct { Attributes map[string]string Weight int Data any } // Hash is a hashing function that takes a vertex of type T and returns a hash // value of type K. // // Every graph has a hashing function and uses that function to retrieve the // hash values of its vertices. You can either use one of the predefined hashing // functions or provide your own one for custom data types: // // cityHash := func(c City) string { // return c.Name // } // // The cityHash function returns the city name as a hash value. The types of T // and K, in this case City and string, also define the types of the graph. type Hash[K comparable, T any] func(T) K // New creates a new graph with vertices of type T, identified by hash values of // type K. These hash values will be obtained using the provided hash function. // // The graph will use the default in-memory store for persisting vertices and // edges. To use a different [Store], use [NewWithStore]. func New[K comparable, T any](hash Hash[K, T], options ...func(*Traits)) Graph[K, T] { return NewWithStore(hash, newMemoryStore[K, T](), options...) } // NewWithStore creates a new graph same as [New] but uses the provided store // instead of the default memory store. func NewWithStore[K comparable, T any](hash Hash[K, T], store Store[K, T], options ...func(*Traits)) Graph[K, T] { var p Traits for _, option := range options { option(&p) } if p.IsDirected { return newDirected(hash, &p, store) } return newUndirected(hash, &p, store) } // NewLike creates a graph that is "like" the given graph: It has the same type, // the same hashing function, and the same traits. The new graph is independent // of the original graph and uses the default in-memory storage. // // g := graph.New(graph.IntHash, graph.Directed()) // h := graph.NewLike(g) // // In the example above, h is a new directed graph of integers derived from g. func NewLike[K comparable, T any](g Graph[K, T]) Graph[K, T] { copyTraits := func(t *Traits) { t.IsDirected = g.Traits().IsDirected t.IsAcyclic = g.Traits().IsAcyclic t.IsWeighted = g.Traits().IsWeighted t.IsRooted = g.Traits().IsRooted t.PreventCycles = g.Traits().PreventCycles } var hash Hash[K, T] if g.Traits().IsDirected { hash = g.(*directed[K, T]).hash } else { hash = g.(*undirected[K, T]).hash } return New(hash, copyTraits) } // StringHash is a hashing function that accepts a string and uses that exact // string as a hash value. Using it as Hash will yield a Graph[string, string]. func StringHash(v string) string { return v } // IntHash is a hashing function that accepts an integer and uses that exact // integer as a hash value. Using it as Hash will yield a Graph[int, int]. func IntHash(v int) int { return v } // EdgeWeight returns a function that sets the weight of an edge to the given // weight. This is a functional option for the [graph.Graph.Edge] and // [graph.Graph.AddEdge] methods. func EdgeWeight(weight int) func(*EdgeProperties) { return func(e *EdgeProperties) { e.Weight = weight } } // EdgeAttribute returns a function that adds the given key-value pair to the // attributes of an edge. This is a functional option for the [graph.Graph.Edge] // and [graph.Graph.AddEdge] methods. func EdgeAttribute(key, value string) func(*EdgeProperties) { return func(e *EdgeProperties) { e.Attributes[key] = value } } // EdgeAttributes returns a function that sets the given map as the attributes // of an edge. This is a functional option for the [graph.Graph.AddEdge] and // [graph.Graph.UpdateEdge] methods. func EdgeAttributes(attributes map[string]string) func(*EdgeProperties) { return func(e *EdgeProperties) { e.Attributes = attributes } } // EdgeData returns a function that sets the data of an edge to the given value. // This is a functional option for the [graph.Graph.Edge] and // [graph.Graph.AddEdge] methods. func EdgeData(data any) func(*EdgeProperties) { return func(e *EdgeProperties) { e.Data = data } } // VertexProperties represents a set of properties that each vertex has. They // can be set when adding a vertex using the corresponding functional options: // // _ = g.AddVertex("A", "B", graph.VertexWeight(2), graph.VertexAttribute("color", "red")) // // The example above will create a vertex with a weight of 2 and an attribute // "color" with value "red". type VertexProperties struct { Attributes map[string]string Weight int } // VertexWeight returns a function that sets the weight of a vertex to the given // weight. This is a functional option for the [graph.Graph.Vertex] and // [graph.Graph.AddVertex] methods. func VertexWeight(weight int) func(*VertexProperties) { return func(e *VertexProperties) { e.Weight = weight } } // VertexAttribute returns a function that adds the given key-value pair to the // vertex attributes. This is a functional option for the [graph.Graph.Vertex] // and [graph.Graph.AddVertex] methods. func VertexAttribute(key, value string) func(*VertexProperties) { return func(e *VertexProperties) { e.Attributes[key] = value } } // VertexAttributes returns a function that sets the given map as the attributes // of a vertex. This is a functional option for the [graph.Graph.AddVertex] methods. func VertexAttributes(attributes map[string]string) func(*VertexProperties) { return func(e *VertexProperties) { e.Attributes = attributes } } graph-0.23.0/graph_test.go000066400000000000000000000146171445126167600154110ustar00rootroot00000000000000package graph import ( "reflect" "testing" ) func TestNew(t *testing.T) { directedType := reflect.TypeOf(&directed[int, int]{}) undirectedType := reflect.TypeOf(&undirected[int, int]{}) tests := map[string]struct { expectedType reflect.Type options []func(*Traits) }{ "no options": { options: []func(*Traits){}, expectedType: undirectedType, }, "directed option": { options: []func(*Traits){Directed()}, expectedType: directedType, }, } for name, test := range tests { graph := New(IntHash, test.options...) actualType := reflect.TypeOf(graph) if actualType != test.expectedType { t.Errorf("%s: graph type expectancy doesn't match: expected %v, got %v", name, test.expectedType, actualType) } } } func TestNewLike(t *testing.T) { tests := map[string]struct { g Graph[int, int] vertices []int }{ "new directed graph of integers": { g: New(IntHash, Directed()), vertices: []int{1, 2, 3}, }, "new undirected weighted graph of integers": { g: New(IntHash, Weighted()), vertices: []int{1, 2, 3}, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { for _, vertex := range test.vertices { _ = test.g.AddVertex(vertex) } h := NewLike(test.g) if len(test.vertices) > 0 { if _, err := h.Vertex(test.vertices[0]); err == nil { t.Errorf("expected vertex %v not to exist in h", test.vertices[0]) } } if test.g.Traits().IsDirected { actual, ok := h.(*directed[int, int]) if !ok { t.Fatalf("type assertion to *directed failed") } expected := test.g.(*directed[int, int]) if actual.hash(42) != expected.hash(42) { t.Errorf("expected hash %v, got %v", expected.hash, actual.hash) } } else { actual, ok := h.(*undirected[int, int]) if !ok { t.Fatalf("type assertion to *directed failed") } expected := test.g.(*undirected[int, int]) if actual.hash(42) != expected.hash(42) { t.Errorf("expected hash %v, got %v", expected.hash, actual.hash) } } if !traitsAreEqual(h.Traits(), test.g.Traits()) { t.Errorf("expected traits %+v, got %+v", test.g.Traits(), h.Traits()) } }) } } func TestStringHash(t *testing.T) { tests := map[string]struct { value string expectedHash string }{ "string value": { value: "London", expectedHash: "London", }, } for name, test := range tests { hash := StringHash(test.value) if hash != test.expectedHash { t.Errorf("%s: hash expectancy doesn't match: expected %v, got %v", name, test.expectedHash, hash) } } } func TestIntHash(t *testing.T) { tests := map[string]struct { value int expectedHash int }{ "int value": { value: 3, expectedHash: 3, }, } for name, test := range tests { hash := IntHash(test.value) if hash != test.expectedHash { t.Errorf("%s: hash expectancy doesn't match: expected %v, got %v", name, test.expectedHash, hash) } } } func TestEdgeWeight(t *testing.T) { tests := map[string]struct { expected EdgeProperties weight int }{ "weight 4": { weight: 4, expected: EdgeProperties{ Weight: 4, }, }, } for name, test := range tests { properties := EdgeProperties{} EdgeWeight(test.weight)(&properties) if properties.Weight != test.expected.Weight { t.Errorf("%s: weight expectation doesn't match: expected %v, got %v", name, test.expected.Weight, properties.Weight) } } } func TestEdgeAttribute(t *testing.T) { tests := map[string]struct { key string value string expected EdgeProperties }{ "attribute label=my-label": { key: "label", value: "my-label", expected: EdgeProperties{ Attributes: map[string]string{ "label": "my-label", }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { properties := EdgeProperties{ Attributes: make(map[string]string), } EdgeAttribute(test.key, test.value)(&properties) value, ok := properties.Attributes[test.key] if !ok { t.Errorf("attribute expectaton doesn't match: key %v doesn't exist", test.key) } expectedValue := test.expected.Attributes[test.key] if value != expectedValue { t.Errorf("value expectation doesn't match: expected %v, got %v", expectedValue, value) } }) } } func TestEdgeAttributes(t *testing.T) { tests := map[string]struct { attributes map[string]string expected map[string]string }{ "attribute label=my-label": { attributes: map[string]string{ "label": "my-label", }, expected: map[string]string{ "label": "my-label", }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { properties := EdgeProperties{ Attributes: make(map[string]string), } EdgeAttributes(test.attributes)(&properties) if !mapsAreEqual(test.expected, properties.Attributes) { t.Errorf("expected %v, got %v", test.expected, properties.Attributes) } }) } } func TestVertexAttribute(t *testing.T) { tests := map[string]struct { key string value string expected VertexProperties }{ "attribute label=my-label": { key: "label", value: "my-label", expected: VertexProperties{ Attributes: map[string]string{ "label": "my-label", }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { properties := VertexProperties{ Attributes: make(map[string]string), } VertexAttribute(test.key, test.value)(&properties) value, ok := properties.Attributes[test.key] if !ok { t.Errorf("attribute expectaton doesn't match: key %v doesn't exist", test.key) } expectedValue := test.expected.Attributes[test.key] if value != expectedValue { t.Errorf("value expectation doesn't match: expected %v, got %v", expectedValue, value) } }) } } func TestVertexAttributes(t *testing.T) { tests := map[string]struct { attributes map[string]string expected map[string]string }{ "attribute label=my-label": { attributes: map[string]string{ "label": "my-label", }, expected: map[string]string{ "label": "my-label", }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { properties := VertexProperties{ Attributes: make(map[string]string), } VertexAttributes(test.attributes)(&properties) if !mapsAreEqual(test.expected, properties.Attributes) { t.Errorf("expected %v, got %v", test.expected, properties.Attributes) } }) } } graph-0.23.0/img/000077500000000000000000000000001445126167600134655ustar00rootroot00000000000000graph-0.23.0/img/banner.png000066400000000000000000004223621445126167600154510ustar00rootroot00000000000000PNG  IHDR Ԫ: iCCPICC ProfileHTS{CBKЛ@J-t%@P!kAE+\" Q,,`]EDY .p;oΙ3_&3×[p7P%P( @EPbzT>UA5PwQ}'4MAѦh':EףKzt+.=!c0&G Ǭa01un a Xl$6{[`p8  qp<> E #^{B|_O zGBGXMN8Fh"& & D31HH,&h8,d˜./H2&H$ itL&Q 6r% ),EL-˓ [*[/{GANO)\.KHmyMKmݧ}>9?o˼yw+WvS++*w+Vx$TiPyV5V]RUՑN矞P V3V R[vT]mL]C[]OMM#QcaM@sE͗t%:L/GԴ|$ZG:& Csku:q:uZtFu5uխ}Gc%k7߬ߠ@ـmePmؐljfXfxk0J2:`i '6ML&L`8,.([cJ2efVr^G4o3faklq⑥ee[+c+U=kF76&6|6Em[lۉjuc0@VufOvr2uJrrz`!᱅#}.t.}Z2gn:n%&PQ$lMH]%2ӡeTd0jJ~gdf~\*UUWoY=Zk7_\wd=>v} +67&m-"0MMٹ?xP''Џ?vl޲o˷|^/[[odSOul~pvp;+  v-U;=+():Wؿq$tWۿeށ;R?Tpa#Gˊbf}~,Xό+/8\XWTZi_YYVT>ydci͑ZZm)pJr/1?w LY(uPцƮsZ~5Yr/L^̺8vItir偖-D\׺׼]ic]|s77nݪom;:v]z]^u?~oOtO_/Ńof>x8jO~7Ϯ|GgH`s!͡V/;_.}9Jjb$O?6|}/G#F߈LN]{-ccO?|SC+5ɔIG̙Pqq-GH(tfh`♙{ZAXĺ)D%e05Z>%Xu+?dfZ0 \o\eXIfMM*>F(iNx ASCIIScreenshotYf pHYs%%IR$iTXtXML:com.adobe.xmp 1282 2560 Screenshot ʳDiDOT(!.R>@IDATxMfUꮶݒP x- ]bP1( 6 |30YYXXVeda"""DVxm~V9W}7鵞N=?K{ ԛ @ @ @ @8S /%@ @ @ @ @ @ @ @ @ @ 4G&@ @ @ @ @  @ @ @ @ p@^# @ @ @ @ @@ @ @ @ @8/͑  @ @ @ @ @w @ @ @ @P@ @ @ @ @ @@;@ @ @ @ @(xKsd @ @ @ @  @ @ @ @ @P<92 @ @ @ @P @ @ @ @ @ ( @ @ @ @(z @ @ @ @ @xiL @ @ @ @ @ @ @ @ @ 4G&@ @ @ @ @  @ @ @ @ p@^# @ @ @ @ @@ @ @ @ @8/͑  @ @ @ @ @w @ @ @ @P@ @ @ @ @ @@;@ @ @ @ @(xKsd @ @ @ @  @ @ @ @ @P<92 @ @ @ @P @ @ @ @ @ ( @ @ @ @(z @ @ @ @ @xiL @ @ @ @ @ @ @ @ @ 4G&@ @ @ @ @  @ @ @ @ p@^# @ @ @ @ @@ @ @ @ @8/͑  @ @ @ @ @w @ @ @ @P@ @ @ @ @ @@;@ @ @ @ @(xKsd @ @ @ @ pr{ok}ggBk}ggBk}ggBk}ggBk}ggBk}ggBk}ggBk}ggBk}ggBk}ggBk}ggBk}ggBk}gY,WXJW?τjτjτjτjτjτjτjτjτjτjτjτj3W|ܗBSɿu5TTR5sk\WSJq]M*U3ǿu5TTR5sk\WSJq]M*U3ǿu5TTR5sk\WSJq]M*U3ǿu55_pU#kL+{h,+{h,+{h,+{h,+{h,+{h,+{h,+{h,+{h,+{h,+{h,+{h,/_0/ك''''''''''''p/{F?X<\hX<\hX<\hX<\hX<\hX<\hX<\hX<\hX<\hX<\hX<\hX<\hZ pqq4CG'b-;@K|"(;ៈ#G'b-;@K|"(;ៈ#G'b-;@K|"(;ៈ#G2Y|hyA/ [⿀T8¿w!RB܅h H# q/ /]激T8¿w!RB܅h H# q/ /]激T8¿w!RB܅h H# q/ /]激T8`h @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(pm...?jτjτjτjτjτjτjτjτjτjτjτjτjҳY寰$j g/x%Qj*U95Wj׸__㺚Ufj*U95Wj׸__㺚Ufj*U95Wj׸__㺚Ufjj઼G/ ֘W4lXW4lXW4lXW4lXW4lXW4lXW4lXW4lXW4lXW4lXW4lXW4lX^`짳_ %O %O %O %O %O %O %O %O %O %O %O %O e+/dz_s~Qy4Qy4Qy4Qy4Qy4Qy4Qy4Qy4Qy4Qy4Qy4QyѴl="i~hOZ#1wD߁DQw%>?sGhOZ#1wD߁DQw%>?sGhOZ#1wD߁DQw%>?sGhd&_p!B4  G.D_@*_p!B4  G.D_@*_p!B4  G.D_@*_p$B[ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q\\\o gx_a)^I_;K?_;K?_;K?_;K?_;K?_;K?_;K?_;K?_;K?_;K?_;K?_;K?_;K?\q_ M J'TR5sk\WSJq]M*U3ǿu5TTR5sk\WSJq]M*U3ǿu5TTR5sk\WSJq]M*U3ǿu5TlUys_@11i٢<1i٢<1i٢<1i٢<1i٢<1i٢<1i٢<1i٢<1i٢<1i٢<1i٢<1i٢lOggK K K K K K K K K K K K W ^gbs=ibs=ibs=ibs=ibs=ibs=ibs=ibs=ibs=ibs=ibs=ibs=i+>{E;ៈ#G'b-;@K|"(;ៈ#G'b-;@K|"(;ៈ#G'b-;@K|"(;?dM7l!RB܅h H# q/ /]激T8¿w!RB܅h H# q/ /]激T8¿w!RB܅h H# q/ /]激T8¿w!RHa  @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @>eR@IDATh/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @hb @ @ @ @PlEH @ @ @ @F  @ @ @ @h/ @ @ @ @  @ @ @ @ ^@9  @ @ @ @G+ @ @ @ @ @`+r@ @ @ @ @ 0 (&V @ @ @ @ @@{W @ @ @ @ @`PM @ @ @ @ @   @ @ @ @ @(8X!@ @ @ @ @_ @ @ @ @ @Q@p4B @ @ @ @ ("$@ @ @ @ @a .w=* >|+?$j g&kKw3M7?M7?M׾}3ϯlW{_}O~l>v޿mOԓv?O]t?O]t?O]t?O]t?Oz[]~Nxl|Ok?m?̽v߷ݹ,޽?|H( @ @ @((!@@ >#Gg/>=sϿs @ @ @ @}w @}_.K7_}ϔye𣏊؇3e @ @ @ @@܆7mMo8?QُL  @ @ @ (>a @^^?}ӽgv_~|C @ @ @ @P|r7#@ (Ov_||{ @ @ @ @cKc  ?*>#Z{ @ @ @l ^ @@7? g?O~b{#?}< @ @ @N@&@*p{?~~{A*p_8Ǟ.K??vVۗ%@ @ @ @L@p&d K +3a9Sl{/r=83V @ @ @+xثsp @6^ھ~6p};OY~_ @ @ @k 0x|Of{}y2iRݿmڶ @ @ @h-z$oaƯ|f{ㅗ:l>=K'@ @ @ @}{ @`A͗^޾}{R~Q XE @ @ @ -! @n_w^v=s] >=o @ @ @J@ۗ%@]IUmwHJI( ,@ J؊XWn*W%Z (Jҍ%; *gN~9y~9,y.My]Jfs$w~{Pz[@@@@@@@xƛ# @/mP)%] E#S@@@@@@p@GN+B@`MW? 5Q "-E}DH3 F@@@@@@B/@0 6`dFSjyU0y6<       `{B z x OWscꟼ(QM[J@@@@@@@ )-" <"~݆Sre%@@@@@@@ tڌ2@[d: "%IjL.a1Ѷb@@@@@@@V&v@p?GO9T؟@@@@@@p@'"c@@<J:'Ыg b@@@@@@@4 8[hV9tKRr0etG $D^{s<@@@@@@@*@Ю3G  @iv\ph_[A@@@@@@C  N 7}̀_O       `C64JF@G t@D4#dGA       ;s5 L-ύvN4      U]g@@/_7Jiǯ.:{@@@@@@+@бS@@(?S|Wʣ$wW4@@@@@@@ ݔQ0 +DK⼗n%#f       NIƁ '(XJ $Gbڵ#={@@@@@@@m1M Jꍁ1.H2P\Q3\@@@@@@@;# @Y>S+܇HM'y{@@@@@@@-==  h@8 _r>9إg       `7v1E@2%yi_vBUG.@@@@@@@KN E! @0K?Lb&'$,! @@@@@@ @D  @ƣoHw?u ;#w{Kҝ)G@@@@@@,'@rSBA  @/+=/"鋢 @@@@@@@ u! @@*t,!wJ#       h@F 294`XHMjji>7       Zi6@H0^T_#7S|      XFeB@@ kgic8N Ij$,*m<      -@@E 7}.m[%#       hi@Fpz9p4+ӦTdـ      %@,yE@2-g !p@Ilv>c@@@@@@@T9 +Pcjhp< w$]>lD@@@@@@ h8! yKƫ&!p@x|T5^M  䎝&>2GBhj=-ѭ$@@@@@ t @L h ;" @0*"@]%ẞA     :mF a@sRws<@HسJk%}!     h)@@ t2$@,Psbl  fkJ)@@@@0Nq G$G@[Y,zRD5mA2:@@@@@~`+ :  f@@w'Jύ^@%s*؃M     k- 5Zc@|H♹ǽ $[_h@@@@@M"8B#A  }m3`FjYKY> C@@@@ h8! B@l#PVT,;_kz)Q Hq8{@@@@CX Nu@@ͲGC= @XX^0Ƈ=@@@@p@w3D@k  @ $V0VƄ$^mc;7@@@@@-:1 h^ rNa9kPi< i1P<     @$i@nŞ  `@J'SLK p]Ͽ;@@@@@]< @41 +؛7'cB&t4ꐵO     i@)2@))X F"RIw]ʥN@@@@@ CK P Yx@(!RsK& $^u$&a#    G <ڃG !@e@@]vwVʊuiFC gIy6= @@@@@m? @m8i [v֡3n w,Oe( @@@@0V &@@l#@6SBo/U^2P@@@@@" @ +" @&=PPmp0     NIƁ `'v-jE@]~Xp[IG-X%!     4ޜ@ 9  hrM]3 @@@@|   @Ai@B'@0t̍@@@@@9 @=?T  7 @gID-    -@@7 t3f@l*MRiDgKSw:qh @@@@[d @&@0J`_{x~*HUR3@@@@p@72cD@  @l@h>K7-!     hi(@ t3<@$p%*' \ iet@@@@@}H+ #`ܹs{cVV: v;u+ v}C<A:~$x$׏#5P_)oL⏿ #`nz^`nz^-!!#?eBݎh}+kʄB /'@i?ReV ?[sssss9K<=5o <=5oKOFBG$Bsh\}m_BqU} ~V5N?*4Uy{/iwS_kNsg0w0w0w0w0wgo>9{o={o= >]'~;:~鼻U=S'G<:C p-_//xYU^*-ԡ;o⏿,'^zޢ^zޢ?@]'AO0w_gP?O0wm:L4.F6?su`~e_u~;ososososos;˟{㯷{㯷O? zP?LL-_Z&:yt4@` jw]A00>;kG͝O7W97W97W97W97W9OпK[ԿK[Կ]P*W;@9}m@9}m*yP608۟*͆?[{4TU^ʯ[7w~\s{\s{\s{\s{\s{w?@3=_OM3=_OMߖ@XH0z0z`ޔ?3 E@{HtiF*OU:w͝[7W97W97W97W97W9O0yMK2vMK2vm006B@ odà~ {Hb;uH     @}i@ž  `@er辗M=} K@@@@@  /@xszD@ J3sdE"@tRgo@@@@@ td2@TQ( ~)ںC0\ +$鎾K     Zyv @: @ d>;Zr24 T}pˤ>@@@@@Pr ^  `;ϴyMZ߿#a q@@@@@t $  @?@!P#{.Pbij@n     G <ڃG !@e@@v]Je. "P@-WIV@@@@@ tݔ3`@ $P /ɛ>9 b*@@@@@ x@0B  ~d5^JJtmL q]>vc;     Jv `@'@@w3%[J2|?=Q +$:HS     s:g.  }g@ xEI=5`$E#kC     ZlB(@]1 @{ ,X dPbڵ#11@@@@@@Ozj  ͉@@@_V+]!P@K+މ     x: iMc@8V.n1 DK_ZL@@@@@+ P S:uf 6(̑Q$w6R$qGI}7 "    ~ @Z`Є4  Iqᤀ@H{>Έ@@@@@N#X'@@Ǔ%RVTlaP bNo&Uy#e     @ph@   @fy%2n0R [$ 쒾@@@@@[ Q4 |)@xf._Ѷ]v):z/JXL4      @`Pi@(XZUpՆ;l"|[_I TK     w@w t3j@B.Po/!@xRZ%U-&     8R `6$JD@N%I7cʦVH]y@@@@@O³ R`(ui@ Iq3[:U ,&ZD5k!2.@@@@@@w   @@2mſl7#`䛯ئ^ E@@@@ @ @  n g (78)+(ұUB\ǚ[#    6 h \@Gt42@|=G'%20@ʓ%s .@@@@@g t|2@{K     n(@0 @#9|!YL8?⺶IdZǙO>yp@x|f%9k`@@@@@ M Fk"8Y 8)w0[̙-薍+A HV %{8){-CiQySͲG}ޟ#P%UL     Mt(@m=} VyK JpqGHK\'^ٓ+Ɂ[l7Hg'NQŔ    W}@+@оsG .(\^>p9<6Z/Ww+''{/fU(kQUK.@@@@@q7 @m0I @񖝒=3co$z1@)ڶ+8(_)O     [u7 ԧo@*(=)#J*}vIXLtH_+C>F SI}. OJM     Pri؀ @@\XSoġG8[/ի|eEgE ĝNR+d+@@@@@=@9/T bw'J],pvn#IwuOC1atUIC7m      @04A@ HAr8 z 䍟!YoҜ>)Cg @FcA USזZ /D"n~@~P/53#dWzfϓ ܡJ0}]׮ۥ3ez??pH{u[?zOPסzF'kTIUfd^5Ij_{Uf|R4iT3~h#OZxyYrïw-'G`_p_S$~Ԟ*Iq %eR4S48Ԯ~7T]b . Ig  fdKc$wc;Ao ''ڠK̛8ӻ`CĖ[5Jg"nB*S;]e XDW $iָzJmiڊ'#t[/u@ ZLnq:?T5;l6YbߴGߖd΃bLPWN6P6o/[ɺ{^ٟaU髥&x߻r}U:{-h%+ʌ9k~GkmޘNjEs;5;Zr,XYCR\n[5 6S4.M֔NdᆀUZe&CzN٤~\elP_ԇHtKMzH:UU7ضu}9U=VDQ XR%@YRmފPK>֞0)^}>'5$aT_USJ䮺KK%{Xdh _j{%/$o>/U%qe*wD47@KdpY{Ѫ5<,"`,XY˶Ȓ5^i"C) q-آ Z5Rm߼K&2uPSkpS* ;=lڐMzi[cmuE*X_>0)lTIZE묶]꾡ުDGEZDjqgC/p `Tb;e{Ky W\.{B单@@_[^~y?/hS>H.Z­7y嫶lphRW=]vt,Ta?mܳ|MΩNJsd+rfu{wy^m(; 3*~ s~Ygծ|E`{uZpF#i9TKUXS(3X8uJ|J^%~a\iá[UO'jn q3$Pf)ZQ *HnI۵Ībڷb,&(%kgRkB2I}IV+t<ݺE\统Aܲ;L4g! h+͚V_?.`+=~4v^KjufZsT{no;e<X6Hfn~p x4@1-ԶVNfʳHe斑-m)U%@s73z'hzVGaQgW ִtL.8T9&^w! P*7i2^*Ԝﺪs^r3ejJ9E+ `@!Pr0C f-e*j=h2<> W@n^*Ws_:aˑ5zWنSg\U+!Qh܅$WW! \)q>)sT8`/%#J@%]j]QH ֩]%fls @L. L~^I|B&B68|!jKf3reZQr<7KoTUDDVqi6Rn F.lY fN'H\)&/sJAQqp傣##\pͥkr D{冀\Xſw KƩULdzV@ zsZxÀ)!N el @O+Tox,,g3{ӮĜ}DR'8`J[Tx\$_^^cF}[). ]w4iT38@ O!qwKZ/B)Tu-QԔW%(k"@qݱ?ʖ2ىGdgkTDFf@ Pq8B L :bL "* IY**(}Zoxf.|gxMI|Fl d~,ڴ#FlvTYIyQٗaP.(`fyӹŌ_e WWtk+_E:@]9-ʧH b^1cLWy Ϸ{dfyPq~qMJ.@Ϸe@kN?JO ǿFʴkXUo 綔'BE/JGh\0} :&dN"7,7;ǖ?K @m;uxE<_ϖ_Vӌcjp7uk9f\zDt1,=uD[%\F"j9b4k[ bi$_~OA߇j%>JXzDzu}:IJrmӘ6E2E2Z]wZw,{i?K:4w׹ @1i /6OT`ZkBaQ߳$D"<2JG_ygU+u1IL?_ jK$#[m&I=iZ۷N9!ذyh. %:UR>kϓ:b-@Q;Ox{پ'l @kM24c-dFL|\ԥ5Z~t/{eko}+':/zۯjеuQ yG_,*ޙ i~/!zC6@I%W Wm|V(0<%Q˹jAX+dJ ~-Y>2SjѭH$SZ#*ΕRvQ\yqErѥss @M[VG~%3r6th;1?OʞYK*oV}KYuo=k"S 57unvۺɓ4ZfZN;,SΑ{F|0K|~QdGC,,/*3HIPx]!ְ@5&@7'B Y$Lm:sˎjt$`\bpy_Βc4ә< 3~mu6qA (k6JܸM햲 [pA[/~1ZJXL q p嫶þ?]OuSjC{Jnm,;F0뫣fKoM8[QāC92-[3U^U\vV'Д .߯vnkt׆癹@Gگ;HMK\.V/Օ9 l-HRz(`-ʑ\.q1Q#M«IlXwI@[ij7 cG#W9g5x ZnJBZf UDvN  pC۫vi/ z'W5 z8tu eյ0o~#jU_n~pJ{Թ[KByE}`gn?h e蠋%>l<:~ (+,܏H' K8G^V>`jopf Wi|ia*OewN 7VnUvy@ ߓNbſ N3].k!TR[Kf!DK#ܣsץ!Zb hi0V[p@m : S_!th(񄟽#} ˒sDQ LKLnws `@|ZoŘ]ڋJ/ \nd+ Vt6v%i"aV[@8,g_rO-Sdl7O^,~ ٩۹9WZpE]Z:H97=;u~SN:;>8}I&WBF 9Ӣ3˲_wN h!_ 2ht6 hL(_4;W+_,'.pEt5hJvYw7p*5%C~@B._P$η) yt`My@]NKtTiE4ޔ\l\{ϻ}O)VK~6l+RvSjک@/\JS* N.4 /ؘ(ۧAk ;vG^B&XfI*]X_O']ۥ1@ q,.tZW.jdD8E'1Ĵc/H֛c$w;W)Ⱥ*ڍm  ) G 4_CF0P:k|4"ݸ!XZZ&/&ύ*%%7 sçjuI(sr;{=\$ᆖJPnKtવˍCߗXb.(xʥf|hj/*?%ņ[ӡ_x|ZIMb);B`(Ti xd_YY!ɉ%bU0ẞ'"f'"Xv薧X(*A@oUr/ VnpfMWu}$'Z@CM@[tk+@040yry mt@hޠ9qA~:Et)֍¢byvnr9ngן'ع9W-kvwϓ=}ISWos;6ivBB%@0T!(s@]u}[C@qvYFGOHֈϤ$tذf ۰zJF@xO~ʼn֘|x~h/ 4 `',,}n%2r2r,26Lkx@䃗KxxXC0ĖmeU>Yn.(\(y_Wɭ5z?A.[~v屡= _U}@##-,W}-u[jK#H̙-L9*7VJҳLJD֪ jud@K h]{eAKIq~{2䗿5Ʃqʭ$cEur pEy~f2vm )%wSQ3'%DuAfZ5fdE%89}@t/f,ŊF%Х] * *@0P9C<  SL7?-em |8e=a>N j~ yYPM0xa=];Ǵ|!@zwKF5}=}fȍ/殱\QqU)=\M[շ%k7 !Wy2!rRT@m9mr?F2_}S/ 4lKתKC .%Wo.R' Xjwտ [@b|2!Za'seϿY E nV! yKPʣc޵uHO0$n5v?o)vpI2qmwZXn]4s&.gWؘ(k37v{r03@,$+J @]~0|*ג.9N7 j@9o.YM 'sZSI|Ĝqǘ@د~~ $E7ɅO0`:[hFd7 #֖}Nn\?}.AiٳGѩq雷H׳ϑO7u]5cZ:7;mzB푫n)nҥ=ADg+,kh3YX`'Z䏆T×Ӗp憀7^IF8P&i 8JQ<@+xf-YjY {gI}.[3UT c|@TԊHlvN&cC@ dڊ<9N|~a*.oW=\Xn>m#cvkpCҭk =7܈].6[岋NV ϖ|pP7yAiox|_j%g#olc;roIIIE4)Iw 8O^}j' Zh2(D w=._=S)9n)J$Ozuqː'  dfyʛG7޶LK8IV&JVndx$+;_g丑į1|yu̱;,VZf{;tKݚҺҪyISdϵe+i(tugzo޶O֬-l)GmѽrnǦ`@l>%+Jk뺟;i$-$O'RO:NмBkT?jak}ez_#|}#wy?^87޴6% cb"%:J}EGJ(ڶB/(=]{ [:(9*r\ݻ7, 8 (ZYx՗ҭ>|_yQ5j!c`H Kaap  @kvgaEZҼIm~{k 9]d*HKm-mOgy! Ͷ-x}O'/م< Q!K.n!߮U}i[z7צU]orV[!lWNYjo2!ҮmCGo2`; dSo&KVT!jn?(kk mUe򙖙 [p:g)jHդaj`sF5}0H^C鹢]6~΃MWk;T0pÖ}‡rE&s\ԥe9z</@3@6nIv~a3K{>N7.YJPD?@'0oF3h+WRhVj}UȯZ i?v͔**To-THZ1)(#_)#\6TXkT_[E#F*1 +JǪ/꾤TVQY(Cj.JpžvKTecn'UO*rzKLQGBv0׮)KW +ժN%ʌO65~q9bg]KYK&3s Ӛ RnVZ-7烕kvxW +R쯻s,`rpvQAόNo g/rT|٪mBR茟 }>H iu  h\'P.up׍qMc. Eֵ @Ӗʀ{޵D-FPz_FL޵]j ,/.M;Z @=ӆ]_}`uURYTkxWҨQ-ُV5jUK}]ܲoSunҷW;VhqM |5c@~\CKKcNy}r̎fu{wy~kE׼&3rl?]j*}kJjJsdZfw+d2ǵGvOF r8:@pW۾=m*eS)+ɌYofboUSd.7@ X r<%hzvPmp0NmJՊ-;iX@S/e/x{RR!緶/|u>=>a\,;fۜi?rGy쾞>Og*GhFuYz>'"ۛ7Q:KѺfV=VhqdOݚK ܑ6vE2^}-]n{Q%Qf@NiPçhx N]5cy=FS>ړ!g_9UNr h!>=I)63rEec3?z3[ϒ^`j1!ְi>+/~gr_Vzwo+6,wvߠ}euHvSq!`V @cJ?v2 D7o]/o@X`Z >v;Zһ[ILCɆӢ22ab[R'[@z>/ݖeL;?$'rN+Z^ґo r}TKΐs:4u}meZpEq~Ž*3= UVq;|ns>'vVNw{ʩW[ջsw78~bYΞ?&7k X}@m̳.}V+̝R".>]ڪ%6Իz]?,Rќo@L|]؆T*`ܹsܹ AYGu|m%H RPX4wNq+-V܂Hdޙw}>;s{~g!wgatL4N~8W B&  3t"?0. !!lTwF;Aĥ kA'VTq w=f69s0U F7DŒYlVQ_lT7-NV)vzQtd[W|eg#=FvV圈l9\m,^wp,GYw.APSG /I8U_yrw1wNf;(3̋,G/Y{qȀIM> I՛LĚS dB:';OذsobMXሩ׫Mtm]c.UQHhߐȐ~$WחoX{v0^_6#[OxU^ ; _ `R0P^2*{ϡb x@ZJ0M^ RDw0uW1Ӷ~crȗ;u0Ш?6J*k4đsVuʖ]ҥNQw GѰ _wYج8Yv@pu,| ީh˺ vL ;U J/_5ЁF1k;uBS?˭f RWXO5=n=UVrt kU6N@Dސ(7=$ĬGՏ3DXO0#e88" kcWr]ZPn? ]h8)Kx2$Y QWC8Hr8 ?N/}*m\rqTgAg$w͕ JF8?kհ4eNLkZxb108*潦I٘=-oZ.;7-(# g?}Ey aݕbL]J+ۤ3=cFuGiiR4c1@in\(괛4SQ[ork~l?JSr1Xg.ݴs{-.\MGWq:IyeM R#yE\M{~sḢjISZ~qJ N&Mך? riZ/@,__{{]K$\:WXlFP/ o_ OIFp~?"0Alo4) #gȟ?-U;t?FO݂oiS@ e͂ I`XL{QEubc7ʏ&uzJWv/,V7;Gx1V̉N]T7 J2{/1s^YGWOk"~S;оoP~AO)i"l`f%лC`:7r=.ZGܢ)W_Kkq]km14hTT]iICzOUR[^]c_v=Nؼfz+#aZjuT^h͟@4\vHlt{{$SdzG3_Vx5)KS&EXX:D,a_`5q̵`k1Ŗ Ovy3UE{y!XG7BhCb\r[Io8Hw1p)Qm;FTD)5,;EʒTu,M $K@nv#EuZOMr"{TazHw_D1/ɿ~ ~H-Sip|rs/ f3Z_VmXO@Sk׏&aնwzErib_xuF|xE#Y8F/E_ ś7po RjUj";1Jj or`@J+D-Z1 ;}z _6X87R&kC>7gv {< &G{s, E |hnQ 5IHH4&xa0SbYwn9GLӃ-ƌE?:v w`V3}3\ ;"w^S L-U D oEXvl2cs1w̎/7qARe\ y* 7oհ3yXoVF 1n )a5`!*@{Wx0q %A_[NnRcLX &@ 7o߻OEcJ {x.#4/ 3בp|/:& N >{Zv'*[i1=Y4*a"T1ZVzKԦn!tkSѼ=k!`5Z<'/ciPbH" ^_J]h8LJ&݁] e> /h/SKmҬzw(ybǕ[&9s@IDATGD Qk)C!ku7Y2|,W2ںǧ* @ePԘ1У<|>3qYÐ8 \4$`zيm8A+ ^![=&!M1Sq1}n*Ѻ%%cvB|rU 6[O`ɘ0N4,2$I% s-9Z wsy7o?BS1[#೬sƚ9m?"6vjR};@2igOi1C!crLYߋEuG  JSܡQ:γ~u1v3L| H$@%@i[H@>ߺwP hD J"ڢܢE("Ð 8BnX#S̉3*Mm%EjN௿bwMIi9vz_Tm<׼N[ZwQ% Id CiXp oˎ#     :7rWo)?IbFŲ-hT}`HHk4~MI"[3}TN&TW#/9s =޼y\e̥L>6CʄX95ċ&<7E (2 (~0Nt/lߞdz_^g:`)ޮ{gXʜ31wr揜Wjt b;pathT+X&{eOi%umY3pi[x;^X9%gIn/7ږݡo:ҀQDfeQ <4c$`JAkvAIEWڼ:yF2_qHHLD`u0NOZQ:,A&ZwZ2`Z ^PJǠP|{n㉇ˌ8{rMk_ ,>\ 7_] Z-cd G|8w5e!(yCl[|:cH|E3TB9Scܶ\ kH@SDP!Mc2 hM r|ڬNjuh#  И7@XwTع bƈDHPa+$*?5 3: i&u|ny+ 3)[7coRTY?{ϡJI8Y4:KоdX 3~$㗕.odjhZ(ά@LlR07{]Ls<־H$H&@8H $oo*>xmHCx̓^"X&'  uQm aTQgeW| Limz,{"n4ڍ f5zE]K;8͈#0k^1_=a)׾:(~vǘ~'@ŮpW1Zw۶V5Wv-+=pvG?Ċ$C2pX*hYQBv8r*W9S^UO $@"@E$6 7lERKSjxYv5KϢIHH& DǁKo5.t*!PӲЪm'5ic(Oi3HÌDgH_> RVv0sTeQx@~'@-\01ʗ,.)K}WoWVc1տ(l4hٯ߷FyÜ \ѳ+WuF(HP.f'Sj ^:mX DH( z  pAO@< %CɕGQx J''IE4: Ёf4)͋;@S@UytRK-; e*>Hs{?diNW0n[]Z ٔ'p[(Pe[.5\IfD޴X:%iH@ip{ɝ0UE9]D$ 33 o@Xmϳ|AA$@$@$z-[>Ll]ڢ>z_-Nu4 Lf3V)s6Ҁ CH`}Д 7vHoߡ\1xVh [If2fN=-H4a #П(V_9S& =EA$@(=3<'H S5̤ńm/J2&e k}.lN\0 Xw> 'j C vh Y%ƖEFkC0yUO;tB4GQjx8*6J@kܙcb2YͺSVѺaaeIYG7BьZu ЦL[G΅<7~HϹ M$`4,۰^(W4u#j`wuIHA~XJ%b8Qg@C,'{,1p`mr}bT !%.Mn}C_䵘={u?PljL],] ѣE I1}Dٝ%r$%IYv\69J`$Ԫg_"C޸uN ;sEk\ԱɜE$`:4,x{_.F|bf$ (X=*y 3ב4TREPxzxRLW@<5pìKjĘB+d5oбEP& |N` (]W~i6h{VvEq('{To2 kwRV~OX\LQW b *7kJlZc ,Hy4:ϐH ۍ}'<O |舷~H$@$@$BU+w4H+݂ O '- ѫƃW΍k; D`v+όy+&0k^1_mvX 50|x7ysp%A/^x 9sMG^3:Ǐz"$8L޼y{ʔe;ͣmyHh pSlv0 8B )#S9HHH@1J*3RTNwQP>_Zڌ50^̨8ߤf9(S޸trK-; RDwQ1t40T o8ٯTɥ1%sX o -^!þ|eG(UFY$@$@$@{H[޽S؟~uPJ.[Jpۏx_k6GVS웤h6@ww7 ) \@#YXRL',W[?H}fts {#e2iqWo*!yX8_ -A$ 33 uS"K zF\q r  paw?ykuM;2o1\3,] cî3;@95Ɉ}'{Jd 4l?k+%4AloQ"GRJŘ@iYCDضسuitkSZ,4fs$gZP J"Hڊݘ$Z ;GŴ$@$@$@Q]v,_8#&5S%@a0l&%.R k5ΪIy,n|F xˊ;jaHHH&PP:uU46_=Okx$QT$yH5?{*lN? I;b2zbzGŽ~􈠌& q-uG Q4 bd4ؾ5Wx&v0G߿Gbp폗~ ..Đ(ɍ`\75 Icԩ0.xQJ(ޢ|ۗQB E '@xH S*s<,fbX W c>T9ػ2z(>'a)Tn:K}O8uoU)x8(@Rp yCJسuitkS:ۼN8s\vyH@ma$`^w"Fp@$ Z-z8uv:fnL8Nlp7 r6KOZ8HI@cI@:7mH \H{ǧJ$@$@$0k7q2 y<2 !4N˪xFG#RDt\J>w&ꯄȖ]"+B\@:y勧PldӮ,:,)W^~yz'_ݑ4ӏoMw9 UunUЪA!Q\ Tk<kw.1~,o\>0X C@9ܙ OwI1|#MV@=s.bUrHHH|rVt)ƞJh-'PnKNnW)& # 3 'g* IxސSC{U 60[wEFs4 slQ 4S;_҇MJD=/'Lך!k"k5J`ho9l AH?9HDh4Q3Y $Yk'E\e*ܢy+HHHp#$Em%23$4#TR' ۢp|ɞR<fN u~%tww ^n 'h_z[x^{h-ئn! |@x/^kݚDO2y 8&M+wXiͷ~V`" h:ЍGp30 8B ޚq/#S9HHH@ vuABOC88… @%t[MQ@ɷhWT6&Mgw+@(XcJ'V_O=B VDγAH35l>hFf`G%v9ël8Je|Ȭ@9iuM:UrIA$@ (93)ۍ}'aq{҄) Tm<vVUѼnA%P E `p㰆 OpPػmtiUJ<fNא>m!%<R&m^'޼yt|%384GEkF0L_q7n=ď}a}]Uqx/ҥBU`d}û&H! DrR6eе5'ג)cQhNQ' (FMx8bb( ę3~LX? (K7^Zo0VA 5M:]v4V E_.U U/qyU!>cpTX:E0wx=m+ Y.WD(i `<iSؤCIM;U!]+u3iU UJdLJe&?i|G- ~ hϜIN_Ǔa'F? VB$@$@&#a)Tn:QrdLeB$`+}D_m.l PJIsEO:y ܽsst|ɞҼYa <})rwǓ/@mQX"oZ,J,f# \|?kh}ϓ+f(j6.~#4Gx?=QIh4| Y !lZ<;_Nrf%ę=$./ &й([Ƈ:WDEB$`+Uj7NU`%˧v2>zf*/[$صS%A[['" b;pvț3X(FvhpӾʡSKdKeQ`/? e d`FpYKKTIpLH wi T6r7/Z' X69+]aaߚu*@Eݑ>M"UQIvI:_MP K@LCթ۰ia!';gV@$@$@&%0lFZNmI*"HL#]tnUѼnA lCP ]b%C;GzcAؖj]|.fsyжBgk;ҿs7'H@SB>_VMmiZ O@{D$$7WFQI |ܘvkϪIHH4;OKW.ES g\HcitUx|c/J߱8Pj[+躞H*Z(B#;/^6L="WXt6@, f# ܼ)tspo2i$T3#Vic,Ce{@cK0UE K@LCpt BO B'` a$@$@$`R~/W׽E)/#]3T;@g\ qcx!>X77+u~tɾ3;8X: 0ZOҍ+@P8ۿ&~)c\w"6S;7->kdQT3W&[` DžZZ(‹H@G4I\"k3H?F} rIHH5xYJ㗕ݑ>M"*MMK%;'Pi JeŬ Ń`F=xw7m`*5P@ʃhiRh ûƑ!^9AU.dX51ъϝ pfD cИ}jPͼuZV("ȓ1xB &%   ,\u :Β0'n!]6l?M'j4:vM]'J=C\]'AH#;Pl'T4ʙk HI'N_G~NFq~:2C v@1C;$!iZ՛F?TJ0aH'r hQ< %pvWpMf''dDےKό[8Z("xa0SpǡatOTHm4čPDN;WtHQ:<9)0f\5%Hn U = /H-nA*OmI㪈LJ`u0ngH;K$@ (93ib\R|8C[ l~ 7 dMWv51iT9F@O% bYUy#^\0, KN5u ]t-Ecfs%i{Ig1&!]CC+t* PߍV&ww7P =ZezK$htdВRC-C2 MrQxwnp@$@$@g*1ί3]zV\`2 /d<R&:\{-]ҵ ZHQ[vA<5f0 Q<#ߓ$2ѪWt;uD,ɥPI*cUAI`¬@tDdorJǷ:/ /# k/=@hbo C{$@$@$@^I F&uK$ɫ%!2 6@x*pq~-^Q(o:(%!_Gk:@MqL%`LɱmI05s LK(TstӇE 9PI*@Ҫp-3A˞ ̿H$ 31 ixO1$p@(3!8HHH@'[MMtn{3[ 'p$ @[L][ZaY<~U ̘@O\L r_IA$ ?/ \5hs ~l΁$"g8Sُ-J}2ʯbL?Rl(VDN;zuxT LN$ 3# pI_SbE=q]o,TK$@$@.Dh%;5К ^uWÎ0GƤ\9laxo߾C[I#FTO~(\u8,Ŷ@%UѢ^At :Ow\Pȶ^:fj`Uګ g*Xwj3M(QR509 x4gΌ$`*߼ŭSbC JֳqS) {ϿZu$qq|k"pU2T6ť&LRXX&54m.\L%IW?[Jl𑮃HY{ |FhW fE_RۥI@EH-]ZEՒ-.kpJhӨtZ6B&(H@,f60%mEO z׆\)*$ pU*oV$׏X=u INo6(~b{/+P7}k$ q9@G96O%}/cp (De5sTEѢxqRyTpFX2= N`s(UoyJ@`XxG@pOg㉋eB3g"0HHH$ڲ[t4+T<3k,]޽{-msLmF@U :WDE5%0eN'o?c@ZEe$`#UƆi4L`hQp0bK`hm\췏#tPsq'.\Eǁ#P(@ (9̸4Hg7@ղ4#0h: Nm+[ :(%@g;i|>^M5<Ė9/'k#W{?n4#V8y:rV^ [@$ ‘3! /pt {dX"̋}[(HHH T@ptaNgD:( NAǩ;>NR/.YKסUv|pf "EtW u3o2WKP '@pLH$pwܜű*e r(HHH lqOg֦t@zV^W0&"b<&y} -`fkz낊#P?vӹ NΦIvNWl+O ]=:Oh2EAiRiU]׬T#@j0(˷iUOF$]\Yp(IHH%t[M^Uв~!:("Pp?~Yaƥ)'LDPqpwwӼ>T@#KRʔۖtI@K|faCZ; v#sj*@T9YAO*WԯG`NV ],$@M$0"wN5{܈ـjw:TN$@$@$::gI/x_ԩK Tn8v+|qi T0F8V>\@"}pŖțgI@K>b҂]Z; v#sj*?Gc9U 'J Jݶ *%UJU0􎌿W ],$@M$0*}':2*;궟@=)kZ'r 0C )ѢqMPxn: @3piu( ѣόR$I1iRY1ktÏO OU:y:h_CC+♍t$k E anQ@0ahsg*PnP ]߬t%bak'+R A$@$@$8)sw]EUnI-]^Z>E^ 9 @=7o-Mke5c*]"g IDATVFNތWjΡ84:IHI@5Co@QkʪZ2+f>6A`Dp:GIJ( 5 |E) ޿ǝ毛NTwjP!A$@$@$ i[ur.HA$;WgC0U8O"y4nzyKCâߵt@Z`7Z*CqhtÓT0I@Ef%RϞ 淓A*&#zZ\ .hI PP 1dZ#@ '`("J)HH\ ·vԖ>H4k7՛)[c i5( Áu=ό q&{JvЩe 3! E`ɚ#3C6ť&L R;zuxf51 @`Β}h}T)|IՠRr J+µZf$U :qw1A%,AU3!.ʣ.   _7~vEη AXQ$gV@`=hsL0x.z^U#cR.g#WE?jF&uKA$MQ9@9WaMOxƭG;Q!p!48K ţWX2_:,R|(P(yNlrs,|N%04]>*#؞3^F$ ! ' ;ΕkegLg@fP jjH4O(YvHHH]~cJ{q@| t@z2w'[W0ͮ0i>@`b0o|kc@lue.l|ɞR }>R*CqhtÓT0țgrN$ ` W UM4JΤP5Ɋ@Ee@4 T$J6Ãr\w%ޮPg$Ğ9BHHLA`1|ZET Ћ kj‡0i>@ec=kc@vUM.H Ќ'Af D#J,3Ep& (H௿ǂ=*7dM# ~uE4bY3 A@5@$`JMYO oUM񉙑HHHagÀ%jbŽȑ5V#{* "m/  ~ό x 5[Ow 2 DT4ůAW[lF:x9fs{F{G>ȅ-Z1ߥpJ΀;Jop4 G΄$:ă~?!:[i1gpyE6oHHLH`h;@ze+@Ћ@[L][aƥ0LDPHmU6`ʃhit}7 GhFPdg/4iO0X +!=:_ #B޽{-+pX;#]笘T!@*0)|%VDzDn^^+J<$@$@$@Pb-eR&FU1 G~X4a4G+ .+{Ѣ|Y?}z~<}zoH RJ+@U0vk^=}ʊ-H@ū721=.t@j6K  AhiHU  ăS\|֭IZvA$   M`ɚ#3CtoW j:/YTn8vV (+gTaYʫм+W'@ŢWعiqX^lFJA/_ r cC,zΊI@4 /4q,MO1g)a'c&  |[0NѾIQd\NHw44G!;ﻬohޞ00/ Blp3]b@[(i7F`Z1Oub$PV _I+ |RhtvXP JbH^Z,&@>H^ӧD$@$@$@صP@iܕ3>xk`V(?t@Zt2U86'`̚.1vkԟ[3OIi]uLfIOR|`5ܛj9?9-&ǥ+='%@5UAuO)~y <`OJ[t3"R1^o WXd)7+,TGP٢-P$nn%pI"ZY]c[ʛZc `8_w#% iyv.r.=Е{sf4#XLT~>?O;dk}()~xu :ޣ䟰,b8˯KHlCr7onNz80+d)ذ8I3inƟ@oN Kxq'\a "o?xm"qv/`$Q P֏vVv|$ zP q=(ã^[:bpßH*g} @2;ӣL;%F/YYGG6^pMDP..[b:ݼ9G0?մzgJ3aJg0܌?/,5Ɲp%@oN?KEo]Fy!_Q;_Z]x]-cLM3DI`A 4=@x zPڬ~90݃NoqjuM38v0qGUz6~Kو6JxU! ټA}T9T7.;?r:Z~st ?YEwݢ@x="_%Ӧm8 3^3m?/,k޾eu9 ̡B@ J/qtT05XFmea1% @A+Rw|7M/*@'ҟF(e .dx5mU?bp c*@@oЅ BPCkzlG7؜yNLTy{S=w"h ! Ei@7c'd=/D_ 2}IV(^@-7V> YuR͖<\*W\Ґ*+1@l)k>fW> r@kק.M@7h YO=/ٸa}:sUp:EC ip,``|(9 ~Vmmziu: "[@T X% lR*޵_91GP@=T |y8bC**CJ*?񿅕O@H45mZ]9Ŝ(290|Y\C+gt 6kҐN}֙qtXMR>ⶵS;)C40le P%`C \.+s3R{'97& 8D23/XM@@U#(LAq,(aRG*ꎶ0/pV|!E s?}:op+/'|;ڵmiT|hT@hG !`MKol z||5;u1` /wQ /D(gN궠n)(njۙq UQOfږ>EW&es!5@PCp=xlv<]X 3>vO~Ӟ @p `Y5*u/XB`G8rv"(Ho t9RӰs`x+pq=h @aº|=leΊE.a! X|̉Ӽmu;#S@?,U) ީ@IDATU45t()%!!Jؠ(Ht()e!) ݡt 3L0Lq}~|{{߾>֜A$@]Ӗ ."R*(hMjoTw2 <5ڭ!8 jB E-~3f-mPC釳{Փ-wtT ﷭i6UOsr?MX<3g'0u;m0BX+ u! V,TAYD1$` >×`= u/;VUt!ciscJ_8,c&wM'cM/> 5P h48 C Y^C'IRCOE?YS7 HI@$Ska7HѮG@"Շ݇O-x76cؗZ(!Px9}͞)PWh85\?V  o't6@B9M!EI@#i3 h| HO 5+bBE< =t^k %$TuyW),ӆ:HHH!yh C>($#5C:fp<4 st& A(ifһR%WXx#1f^x4Ϝ BֲblmyWF`΢3gdel;D4M}GQ ߬Ÿ z/úEkjAhZGY=hǒ hI@-i2 P_ 01$ V*bkT@כ>x+R^eue?aP!  p@gbNFfzf勮s{! -о,Q@[C̙ JFON,_z\98nnn&)`Z@aSMy4 :~&@U/;C*ϨK@p쿦 ACt" PĮPQh44 l&>;&gjxc#h'9D5z{ѯND,f ^*3; Y"EI@#i3 h| X@UOlp뵷@ʥ۸&|Zֵwjƛc兛{Z\7i2gE2u t:t5;X7أt)D_O ߇4Ϝ>䄉 )_|΃D'е+g>͊i\TZyPfI!0 8IuؼﴓQJ! )Ukeu$ 2E F CcGnibkR^Ui&9ϓ91k@Jimy hO`ͦsUiبd_U̲hމ|&J'/I~ ϲEK$@$@$Zeb'Я+. ,YVShC̙1!h •G}ˉ H ;P0DDJq %k M IA/BƣnnQbalYlBPPh0Ó $Kd <"yC9o;[ajǚ}LI9Tn6C˱(,D@ 5 C@].iD=Pc=_nx,2E"ƛQM|35*4HHHR'0wlI Ѭn,Õ/ DTOxƭAO) @3cblI_0_b&egZHL ,< Ek 0GghL>e0߶:[Jm5U~H\0uFa]\кee$ :E O =D_e[u͵ɘý`x(d!Ņ#`tK"i4e4Ϝ&Pu/|V"Զȟ7Cҿ_ 6CJdh޷p! jV'pdiC'.H@l:K&A(H#(H\@v xIIHH@(Xm0"?V"uZԭ5{!K-x̘4?là $}ф]^c0!3S@Bk6E 6E`Lpn4@,ڍ>~N96:5\4J,J՜ #e@  h#(# Ї@Ĺe> HױHHMg?c֒9NxUtQm~zLh4m4Ϝ&pC>qqqI0;.Lgf:HHA/ȥ'|EMR2u|w?1ag /ʾ,j9d'pgkS.C@i5 %(\K(H\ɋ:i ^d^ZN$@$@"zJ7 vi" XnXW5's-@[G̙1yMޚ]%?+z4Űuؙ)_E&>uFܧ4 fmht2ag٬CT{w|}F%_&EU::I@<4*" $4s)*m;4F~O$@$@$ _T~/E눵ܹrǝ1 i453N`}>|q %/o22 $$v9XH“dT,`,$f#4se*oI(I(S4ꂕAIl @ 8HHsWwˌICt-"ӧ]u̐$@$@$D^-/:/F#X^<v44hԚhhLGG 0BdtqISgeОZ'W) ɑl c~`THhPW<}t*y1+ΚI@ 4  ?E8e<|TEq}O$@$@$`66Mæ̖( >8irfϘO~ ?̣Vh= p@=26 @JhL F lN<;˴&*_Ygp6 KoC*s#m4B(9\zuN݀9Tp@2ST~:Nr;lyjH IqqqjuQOhlgd4:3,ȨhTh8 Azetv7a @DzP/K$S#$@$@۴#g .&4 $,VY7/Ogp. (USDFU0N. BPx\q_D&h^ȶx"y F`[#4/lkz\OKBh'  xMPW8-^oUjTwceyBݷ%iW(DC AJQ1h.^I<=RV777=3#&Pi#nTȬ:ά2ư9y[eh~<4ǞI w' i='(m^mL;KIHH@oc^37F^*F|tϠ dE}7@Z-M$i>E`׾hۧΈ_3x¨pwk] v *Qj}%z_Ƅ9N?ӴG4j$#@d \-LB$@$4_ `ܤ/JpַIM!RJ$   _ʾ,~dMpOADКKa8x֡ǷD!V:<@qD_H3{S dRLo&u[>#\ٲ vXmFU\t~72f1*%H`ܷ0v:=TB!l] >ӄ `dI (i(,@@ 4% N l.<]S22OHKc-,   G~/p̿&p.uB0ˎP6s8[3ʗ㧬"RٺRe D"@gΌy;e@hx6uf*&  a.ߏn[~ 1 cEfL(S1A\~Fe.ơŘ~$/S#C놣h6N-HAP¦Q2 c¶@/^,,_/rֱ;HHL@5l=w- ]ߨ[#}:oi밂/|p|&}?]"|L0 f"|p'>LچlR4oe^A`7k1nz(UhlEfl@lqq_ob(}66>i R!Fjc` v`b e9$ %j 8O |_ p:bÜfB҅H9 ٙHH&pkf"mP~Iֻs|f-ahEȉMXV!5SZqY'ALH0AK yA!DG-16/Us{p Kmp אa>5!@=&Smɫ_z^TH$,!htPVș89{>..k7÷V\r6n ={L rE2 Zb  $:PWhCu̢Mhna GQDDFnXJ4A5"|nK^W=ݱtzW4[ZDnkb4ؚ= ,u I+ -E+Z,"@TX  D;Ϧ#HfYePϓe#  &0ǝ;Zmm;^o^| β £6_RWVܯ̑2H0Q[t?AVB~ | na[q> Cz7K"JG`3h{)/eRyG@2hGTHV%@U;˺HHl"}z 0Ƌ6-mZd=e&zHHHt㧬ØLסu^/ޒ1_ɳ`/og:jdmnh -L a*5 ;gv<ݲ]Ѽ L p- 0Uˈ>1gS3@S;a\r>Ȝ^:v50u[_QR? \ z_oOSϛUͲ >o;[aՊc>j`r$p?*7kzgH%@HH@@1w@Y 09&)\ڶE$@$@.D`zVV~׼~9hTukekugc͖cXHS 6OQ_$btH.X$vBLnZuŠ_gZƛhfdLXp3et8vUi4&}! Tlhg˕ &wo|g/rPe2 cИqF܂; \0uFa]\кee$ :E B ~ |:NO$Y͘$@$@$ 5#aR`^hl+ mf1qErk-%/^+m}TI寧TYr lK٤_R1(/;~=T._@*ݮ*ƭ@t[1Aڴi`-|MoaD19m3z{`tݺܒ59H߲$[YWEآ^Y,M0֕Cu{R'@`8H@4ÕQIH$$ç"| ?yB_\EZN$@$@zoCtOzUoQnS3{F4j%("2[գ)+~w mZW_S>KPMki4?@@5p4Us=q5@{h7vT~b6?"ȨhTk6/;W )|IՅϊkɥT/#\ ·IhlkY CcMa5d>Mj:T>' U [}2b)f-m U_(olJy[]869dB%[ڢ]/m1P"f ٠Ʉ!jk|2_(cC$(經gZ񈎎>W IXsOyaE>!(R02-@˵A@;`q( h' X@ݰWr2>_V? ^ێjt#g (],JɍŔyPJy)pH#pM3|բvԯVʏJ/2eA G弲r`Nyp TVQsyupxeejaPƄYg _Gnw HvN v` f-X/'U{6Fl3] _NCtHVǴo'9$X ɂS&ǯ/jT,ڢbq WaՖRܦay,Ed0` SKPW_5 :[kQ@@+t5 J hOq9 öHkz`l  \↲R 0@7jQnO!ysq 8N"U~ xAi4'%T]GhL 碔.ۏfmD>D¹V"XȟՊj:BoX$/V((>+:O]e~v}w;\*(F\ uL$@$`7+_'ʄ Do":HHH@poG. B\@/cN,N)0Ejtl=pFVT@Q2RA#1vZlZ9iȜ[rk'@8S~4CV@PQ7 iBWmG9w6oZ0O$@$@$pmtN]iXɍ_!RfvWVivsۡ#UVI-2`ݚ%ln#&&8_UVo9;5?ݳt>퇞bO ha MΤ$@ y ":O"n̙Bx9$@$@$ +_ 0 8L(߫qC_]_$˕g|{L OXks/-;Nű҅s+[W@IYXpo3R֐h~ȓ/)0E<(綝Πi)E_oiTC:᭬-eIQ +7vFG}Pl>GCpێV%>AF8u5:m|g$:$@&@HH "x`e6GjHHHBc)e/Cu$_$͗Ƕz"t'@@ꖱ:M.KV{jVVV͍Ŕ"yg͠}2"FDF8uA93q}[TZț/ECؤ$6~=wv?6]fH=wr{*+/h(U4J(gb`{8V %8{z\=3KlXq| J'gΌ$@;HH4"ux`%9W PW<7ȝ3SyUX6PIIH4&    ܾo8& i_\e^UiLHHHzo`XcEȤ|7_Kt}nc ɵ*?ѫDM I@)'0gns cR]37 *9{,AyYJ{ hyW֓^ij%)J4JYgRhL ϑ A@#(3  VL"֞SIZN$@$@FϋO[ٮkhפ&}9g47hajhX$~[T @W8뵕?㭞q7 )Gv(_y^[miNO?zz?}I@s:7ĨuzYs O[FpeؕI\4˟IH,L .<*&2Ӱ)HH$kĤpM#S3W+ċ 5TFن#`bndbh|L$j~8q?E!' P-QNo: +fmۓGPyM(Wَ~ޗ+MΔY/3ehTO$@$ w &-1g/I"ϒ $"Wm8sEDt=Qba*~ܶqE,P\.@w9K ,,߶w6lQxiӦOhԂ1fTF}99@R(' ~%B/E&&@`b&<:D=J; ׁJI HH 0rB1 >)QIHH~xc'V"ֻjW/.dGN\EW'V/a֗Mjit^G ̘?_t#L*zZ>PKb?w=VsP=,5ˀG@]qǩ/oێVZz/YT LNGf[k/M%$ ?!+  @j$j;Hf,z&.$ (ϓ;<=6{͑H0' paqqqX(M]箻0 yJWze.Gh]@76Ucv50u[jp4ZY3^:7ktX1^TH74V"ٽtaͬoP K{5C߮cpp-  p@a3 @4&$@$@$/_/DMc: {u`Lh}&b-ܸ_\Lbf7USs9QI#ʿ-܉O,KN!秎z|2V eUơ^bro|._Į?/M%PtUiMCoxbW5>}܍:S|tˊڢyEch_۴o_v֚? SZ/;Gi4eɂ?I@5|:Mڭ|ߨnpSğ_lX.9D)BJJ?Dg>;JNy GG䴙bx-{_&R.ʟ_'1*Q0u=FOYX)WV4,5-ڎ`4K¡k5*/0D0!ǨЩC C򥔄Xm\WYҵu]k' H7p]:n?wpu0KAB9Q\1+S14VdjçZmeLqr5p$B0iLY=Q$2cvx^!ehD`a Zyt8q?<'ubV(U KP%I[ -E[[,"@TX  W!uR 0[RQ$_Jۤ_X5~ܦ4ڕ|xxHH!p^.^ }7w@ܾB+1aQL/2 S_eE댾ϓG?嵺^_e UH!h~q$jb]L>;=b涑@ɂ0GcthU fdX O5֌mʹ|͟6w}զd>F5JbDPֶ @CT4"=f$@$@|~(>Me&" v';'A1F4~$@$@$@C=>u+7~Y]6/ .z.(,\mEeu]xƿ֍+@ݎR@;> ysqS?طELLNYQ^h۸"zeK=hγ0nl沴T p Tq Nh ,Ò b(&ɈTӘ: tVV]a,l$g_bCv+ \Y3&ѶyeT3W}IW9 Ky70}v "R̫6Ȕm^[UAͪŐF}.p P*d$  8O"t~%]<2z:HHH!0hrL]T5&/5U+&Κe%VuUl7o?gStg֍*(JS%(4rk@}F`+n}k;1{yZAYirRVAY/3ehTO$@$BȚ.Z=v@ڬ~m4l('  0@'cLTX5/&|$nqx;~?U@抹\]QT)V>hrwF`BzRW`ݼb^Btt^n^fIԭ^|$@}*ch gf!HLLxHH p<,V@nb*   T %e dݪRBp>$hQȤPv _pӎIYUgfJzՊ)<$$`"@jH@&4-j%  gMYEk9˗$`;t#l$@$@$@BPW*TcZB0]+ Κ]7qu y[luJ᥊P"RQ^}@KI%0_8$c__p:eu@KE W\JmRYێVbz]5 :ƚ^<_cz ( '@șHH%y\?JnM4 A@"E@)BPi RP("HXEAAD FflI2ew{yp_gNuԳ{ @r# ( @ 4LJ ̹vb+,F`6DDZ/K @@`a?Ok6'@I@0OW @y-0wǴo t*0tmc=z @@.8;k'[lZ_k6'@I@0OW @yT$y C ͨ9 )0lbA @@v;յC2N֧ڃ  @yi @@u^%@ ̻јZx'/OY'bAd@`X=c֜yuܓ=>Q]{9 @ Oy:- @ȗ`K @*GAŪo|!% @c&չۯF uC @I @ ;3 @z!0/ kkQ=nvZO$(IIg]`K/hkmaO @T@I @j Xd[ @~ p~,/ `˸>j= w_CǻWw\&R qΦ$@ PzۓQ{yuy{cԤھ  @@^1'ƭXuݶ?i`s @@vb%@ N  @@b=s;j*<Cw6Oܳy6)Ȉ3xaݻѻľO{ @I@0OW @y$'PxޏsTi:;MkP =tn\{c{ @I@0OW @y$.0;gnI/0漣c7&$@7A]Yuűk6'@Q@0g @9*0c7'0hƨI7`&"@8Îqџ{ @M@0o'_ @Y*ˮKuO_;mxdL`>Bݻc-]>4@ț`NL @# )H]`Ō~>6hַ- 2q2 Ș?}ĺw?^4@ȣ`OM @! s%H]gca_I}/4 ׉Q?F f" Pgk;v9:w_Xg2ы& @yۉ @@~sV:%@ ~1Ku[}b'>CX^jkvˌO{7\!@(K@,& @B@ 4 @FwC1g^Wf RR]mEW<;L &흉^4Aȣ`OM @! s%HU`7sn==o{]>Ú,v'Ŀ~.]+{.;V @ @ @@B A*C*0Ɣ}}gLcO 8xP:ȗ7:L4=h@kI @ 5Gsn# 7w!ݾ71HP KR&Q_D/ @Y@0ϧw @&GDςPyZƎ; ` LN\;cVh*;q8mR( @J @Ȍ^3~y~Ѿ7ȅmw>g8E\I3f΍l}LL`/=rh& @ y>= @ȶ`Gw @ LY Hz7}ot8 \`ܮXcګrp_}ؐ@_ӸoŻsL @fl6+ @ n @ ..OY&zDc ,sѺ=ȜaG2μkLǐc V5Țu7LnNz  @ y=9} @Ⱦ`H @ q)s/ XT`w{e @ 5ޏjӊ귶d&/qM}fc;Ɠw @}7T @ .U а]?`*1cՔnhX~9.ij.5wi6qBK3r P`%Z @ P`%Z @u51_4$Fȋ賏]#/ qܯTruW[/rˌ(i /yIT\?+,?<@ xŻJ @@nȕ{UϚͷ]?ÿ|{2/p׿ >%zzzuCs ޳rY-"?[|ԘٕFk~Cƥg_$@(,6q @dqTB'սEl8 |xW+m@kKL:jUx)Ɏ'kf%Q.;*V}DzT @ BЫ @ӒUdP`湿Ͽ2ih[mF|I['^.?G#`SΊ~{XJf;l瞶W5z @`)Kq @>  @ _t|tjZ !q134, @ [G.XݧF g͢_~}aK[߭>}jK[V{--?6%@4`# @W@v'@51;^SD_j-b_#@ߥlqIWYni#P@wwO|j3[ 6 @R@) @j" Xf @ 1kտ4@ؑl M@zG9H|mqa_&~E )WK\uŃ>63<Ѻ @! @ @@ZiɪK2&ˮXWi&??)V[F6+) s'NG.VZ&;e=+/m{J ߗui\p_Kׂp|;k{ @^@؀ @& X7z @ oļv#_3[ ̟ow|~ž;o~1|X{OZJ Y;c/5_O%@( " @\@_'@1;^j֔h}1C3۟ȏq]p5mxl8t {lj+KbǽTj_}/%/^z;=bH<|1xL9 @@~'@ ]잍 @@b3xj?jD{& M餸.;Fڿh [ byjl1\xޱˎ_ @LRS- @* V  @?A3VFyjYdH_w(b# ɏTo]qɿ3/KԾ wqugPS @T# Xg @(G@%k @@^zsF' 0}?9.3rhGGLjW @ O @ = @d{⅍vK*(оՆ1į h6 cgKٯ֊|RKoٳ7O2λ\ߛ쿽j~ @L[X} @+ ؼgorhGW8I5fZ߶l,sYoSdLളߞuv޽{n;m#/}_Zϼ45W3|'푫5KȻ`OP @+ ݳHD`εԣL" a$, O}jbUW{Sbf'^c&]=7ϩ_~q/ Ǜ$@`mC @ ЍL%0 bU74Цʹ>L9!oo|U6I>ilѿ+@;Qk4IC2N֧jM @W@=E @@rYD2)0?97ܞ4՜#: ڷۼ975e <+ǜή=?Iӆq&lj?}\+-4p @ d!ҸQeH_ @T) X% @@^&&=cȮ6F$@/7`lD]cb-֎7_+&lzoy?ⲫoy!7_XeebC @( S3 @|tIxu#ѧ~ރ'c~>~0iW4?;pvٚVm C; ySqU3>3Ra`[koHG @4@@'4 @@ 6 ݗb+S`Ȍ]L?!@ SΊ?+vn4rݱSܭJ\y[!wG  \&  @R@0Ǧi P`VVF`k賾]B@ \u]/swQ}zo|Ÿow]/Nif7]qX(g5 @5- @4`  @y2頍׍Q?8"/ <Ύ?bfKǎ߱l;\6VX~T/M/ޠ3M  =x)f\ʓkUu~5Fv iaMHȑ`K @r& .T@R1i Oy=8뒿4)M[7X;zX*ƌF)>4FC Jiŗ}i1exm̘kn"Y@ - mm- ^+7?Ǽy b^߻Dg{ ^}ܘ\Z5;/|gW{WC P/,W7*zؗ @SUdE@0+'7lBt|7}xũˁ]?Z ȣ'^/.<1E M,WX#l @ ȇ`>ΩӇb7iJ@ O:;ڂ@6o8ь. @( D7 @裀`=N.f{EO95SCw6OܳF6+5'G"}j+him k  @* @) X&e @ /oXʔ1]p2# 8_SgJ[ @cƫ%VS! @tU @@3 6雝Bݾ]=2d ShVZ)3;߻:~zm>8' 4nTCbye @d\@0= @9i#0cYj to V5&_O-X>,XG'@4`3  @F@6v!@uǜ?nۘN=,MX&@@===n']M^HZ|eۇnZ. @@ kl  @M* ؤolhǟ~ؤbZ|$@1LǜvM\pŭӘCV]ql\t8@IDAT>޻Wl Ch6f;q @`D"0Gƌ[mJ`qѲr5x3ˮG̟]}mD\ŗ{o1h`[YG Q @4` @f_yCL;-q@M󈖖i3,I A}{tv_2 T].8s^}mF `z* @hvf^`}ks0-b?fuI@S 8/5fϝT6;wgѻeXtBHD@0FE @XbP\"@$S18aF,9h[e{r< htf/1ξ1}F|Xyqx׿ V @I & @o|  @@ 1t:cI}1Բ^ hRg̉s~OoWlRcB`}wbK{ @A@$@ $ |sD&Lt}KU_J(-U)ߟĜn_MW P`Őn_ʿ?YVS5j=sv?x1$%0ch| `jKlip{ W{@: tU$-ZY=y%Ң_Wҫ'-ZY=y%ҢK?ENMtO׷Tuҽ?]RZ:^?K^ 95-me5W3߰욿 nn?NY5|p|/~n2x`S^T,JŒ]?YJT,|k5jdw ^+PݽW']pIÿy$BoJ:V_T:ZnUJ-~[S&@~bюlߟd¿ZdxW'ψK}0EXD`ĐAq[!~( ].*ߟEEj7z/EEj7z/EEj7tku y}*طͯO`ߞ7>Ϳ}{>iG= 80>q}3kNV(0=|UjjoU/m{]wsJ?`E i_1qcځS\2OƱ*K98V[reb䆙 ٝq{L}Ӆߟ~%zz45>;Cy5Wvj @4`Q  @X@#@1yCnM$0lb!7F%@@:o . . Ƚʊc.k+  @ B @@28Br#0kbMͿ߿v12(0{».|7~>Pj*]Xm\?ǎ @HT@0QN @x[0JAg] 1m+WL@'Z @@s <+ss/L}-yyijCc <(Z+|+z[vDCm8 @l λM}~˺`s @LTE y)s/}j0CwH ?lb4Sij`oPϼ0k{]39!mˍ(|񅟽o{dǐL) @ @ `j @ ̺캘>gR{ 0漣c7(f @@S Ҵ;N3. 승c޼]YY9~w.I޿{ϝW{׷֖hkm-- w[[{z\]V7}򣢽}@Sa  @ @ @@)RB @Xk ?{֜HY{cԤóҎ> @ @ @ p w"@ L?*FwHoqOz @ @ @,G Ѐ|0&HYc'f= @ @ @ @ah FJ L9Ę{ZABCخ§,'@ @ @ @J+Ѳ4S{VMez t 189ZFwԻ @ @ @ @x G.5?^k1] e>q  @ @ @F3 @fx+h"S_kKh{l @ @ @h 8fC @% tϘvt=’CL,y2W[F @ @ @}싞g  @@̾v 21%2fdɱѲz`_ @ @ @4`Sa  @&xltC/Cڡ*  @ @ @ @ )$!@9;cNگ@۪+Ƙ ~ԫ @ @ @ @ L,0[?m !?wL @ @ @i&ȩ?}vz `}Ƒ޾ @ @ @hZ=z @ k`I#=ݒnN @ @ @5- @@^|p^O.ݾ۷0Ft7Q @ @ @J $4K_@2bߊ֕_ @ @ @ @>q+ȕϋY)W=k6RdtJ @ @ @ Ve1h^~':}yLt $ @ @ @ @ #9m @<վ=cVZc?6!:s`U#@ @ @ @Eϳ @ZM6qz>h @ @ @ 1v @@:?&񸬷ZFwIJםP5e @ @ @ @ I$5"@M"0bʡ6ɴ=r8''@ @ @ @@F3z0"@Yo(mc/:>xG*x @ @ @LSWm s)p^Oٜ)η5&@ @ @ @@NsrP$@Yu1ڞ*\yzSᓖ @ @ @ @@k-n? Ѐ3/*^?Fj]q\>h\ @ @ @9idM`SaO6Z':;$R @ @ @ @@}'@ $G4D1Jc~6$@ @ @ @@6AdE`+SbꡧļJKXm7h鿔Un @ @ @ @@x*z"@9ӏ;'f_wk'iq= @ @ @  |F#@x}beջ /"ecW>& @ @ @ @@tZz%@9uYGOvx-B / @ @ @ȷ`O @ }$fBw=~a{þKg. @ @ @4`  @@zzbƙnj~Sw嗉a}:ڷݴyLN @ @ @lC5Ȳܿ3ξ<.m6LoCvP ghYfTd @ @ @xC@+@ p5ͲV!Dofٜ @ @ @h:;r @̹1Tt2l_! 0 @ @ @ @`IKqnY_]O>_=qyW - ݈㙉 @ @ @\ğ @@}zf͉Y]_^ ͨO9uW!;l;n M @ @ @V @ 5Ͽ Awt{n @ @ @ @@C 6_ѧco9 `FD{oC%XU) @ @ @ț`NL @6-^{s̾z& d4a/ @ @ @hJ@`Fg!3A.g7-z Xgu4 @ @ @j/ X{s; @i tD}1Gb޿KkDX߻b{׈ N" @ @ @ qΦ$@)Pλ ~λX0ez],ZWmo+uV)ؔ @ @ @% Xi @^= ^|{9K-c:Ѻ[|4>%? @ @ @B@ 4 @)3+zf͉yѳ;{}kFAޟ/ @ @ @ PKZjۋ @ @ @ @$$ 2 @ @ @ @ @Զ @ @ @ @HH@0!He @ @ @ @ @@-km/ @ @ @ @ `B @ @ @ @ @Z R^ @ @ @ @ @ ! !@ @ @ @ @ @ @ @ @ @@B A*C @ @ @ @j) XKm{ @ @ @ @ @T @ @ @ @R@"@ @ @ @ @ &  @ @ @ @`-E @ @ @ @LR @ @ @ @ PKZjۋ @ @ @ @$$ 2 @ @ @ @ @Զ @ @ @ @HH@0!He @ @ @ @ @@-km/ @ @ @ @ `B @ @ @ @ @Z R^ @ @ @ @ @ ! !@ @ @ @ @ @ @ @ @ @@B A*C @ @ @ @j) XKm{ @ @ @ @ @r`„ ?oK {/%}οP[:RB矮oK {/%}οP[:RB矮oK {/%}οP[:RB矮oI2ob )OοP[:RB矮oK {/%}οP[:RB矮oK {/%}οP[:RB矮oK {/%}KM4!7%?r/W*uq-*rY?r/W*uq-*rY?r/W*uq-*rY?r/W*uq-*rY?r/W*uq-*rY?r/W*uq-*rY?r&/Xu ~bLe^I柴heWjIVVe^I柴heWjIVVe^I柴heWjIVVe^I柴heWjIVVe^I柴heWjIVVe^I柴heWjIVV/i~u/` /0hWr VX` /0hWr VX` /0hWr VX` /0hWr VX` /0hWr VX` /0hWr VX.i ٗ'lvJ_X'Yi5%V_X'Yi5%V_X'Yi5%V_X'Yi5%V_X'Yi5%V_X'Yi5%VK?w^%!L0RO_ZOR@K bVQh >?A*J-G'YE)U%1(ſ  fW#Ĭ*|U_ZOR@K bVQh >?A*J-G'YE)U%H Z. +R@Jq q(Ϳ O@Jq q(Ϳ O@Jq q(Ϳ O@Jq q(Ϳ O@Jq q(Ϳ O@Jq q(Ϳ O@JqIR h4 @ @ @ @ȼ`H @ @ @ @ @X@ @ @ @ @ y  @ @ @ @ @@`+ @ @ @ @ @ ?"  @ @ @ @ @bbW @ @ @ @ @@3D$@ @ @ @ @& @ @ @ @ @ f4H @ @ @ @M\!@ @ @ @ @i @ @ @ @ B @ @ @ @2/ #  @ @ @ @(,6q @ @ @ @d^@0GA @ @ @ @ P, Xl  @ @ @ @ȼ`H @ @ @ @ @X@ @ @ @ @ y  @ @ @ @ @@`+ @ @ @ @ @ ?"  @ @ @ @ @bbW @ @ @ @ @@3D$@ @ @ @ @& @ @ @ @ @ f4H @ @ @ @M\!@ @ @ @ @i @ @ @ @ B @ @ @ @2/ #  @ @ @ @(,6q @ @ @ @d^@0GA @ @ @ @ P, Xl  @ @ @ @ȼ`H @ @ @ @ @X@ @ @ @ @ y  @ @ @ @ @@`+ @ @ @ @ @ ?"  @ @ @ @ @bbW @ @ @ @ @@3D$@ @ @ @ @& @ @ @ @ @ f4H @ @ @ @M\!@ @ @ @ @i @ @ @ @ B @ @ @ @2/ #  @ @ @ @(,6q @ @ @ @d^@0GA @ @ @ @ P, Xl  @ @ @ @ȼ`H @ @ @ @ @X@ @ @ @ @ y  @ @ @ @ @@`+ @ @ @ @ @ ?"  @ @ @ @ @bbW @ @ @ @ @@3D$@ @ @ @ @& @ @ @ @ @ f4H @ @ @ @M\!@ @ @ @ @i @ @ @ @ B @ @ @ @2/ #  @ @ @ @(,6q @ @ @ @d^@0GA @ @ @ @ P, Xl  @ @ @ @ȼ`H @ @ @ @ @X@ @ @ @ @ y  @ @ @ @ @@`+ @ @ @ @ @ ?"  @ @ @ @ @bbW @ @ @ @ @@3D$@ @ @ @ @& @ @ @ @ @ f4H @ @ @ @M\!@ @ @ @ @i @ @ @ @ B @ @v β^٧?d-[zBl9ʰ @ @ׯ  @ @ @ @ @@P&N @ @ @ @ @z+@ @ @ @ @ P @ @ @ @ ^@p< @ @ @ @Tj @ @ @ @P\"$@ @ @ @ @U@8!@ @ @ @ @ׯ  @ @ @ @ @@P&N @ @ @ @ @z+@ @ @ @ @ P>ׯ??~ˬYS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YSz# XJ?ffffffffffffғW-1m*[93ofϸަ򿕚?zVjfm*[93ofϸަ򿕚?zVjfm*[93ofϸަ򿕚?zVjfmj_V@==4h/+=?-JOOӢ<=4h/+=?-JOOӢ<=4h/+=?-JOOӢ`OD@ ~?- C'QЂb>D@ ~?- C'QЂb>D@ ~o˿aQ/G^D@?{ip E4 /G^D@?{ip E4 /G^D@?{ip E4 /GR[M @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @C6 IDAT @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @_~?{J?{J?{J?{J?{J?{J?{J?{J?{J?{J?{J?{J?{JOd?K [b'YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YS:=YSz є%f~qM+536TR3sg\oSJqM+536TR3sg\oSJqM+536TR3sg\oSJqMM+ʛ_ '畞yE{y{^ii^WzZǿ畞yE{y{^ii^WzZǿ畞yE{y{^ii^WzZW?moAq`qaf&Xxǿ moAq`qaf&Xxǿ moAq`qaf&Xxǿ mƥ wŲ4]D@ ~?- C'QЂb>D@ ~?- C'QЂb>D@ ~?-I# -FP ]D@?{ip E4 /G^D@?{ip E4 /G^D@?{ip E4 /G^D@IlpV4 @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ (V' @ @ @ @ @`y  @ @ @ @  @ @ @ @X/~EH @ @ @ @`5qB @ @ @ @ (_ @ @ @ @ @*XM @ @ @ @ @ W @ @ @ @ @ #çGвIENDB`graph-0.23.0/img/cities.svg000066400000000000000000000065421445126167600154750ustar00rootroot00000000000000 munich munich london london munich--london 3 madrid madrid munich--madrid 6 paris paris munich--paris 2 madrid--london 5 paris--london 2 paris--madrid 4 graph-0.23.0/img/cycles.svg000066400000000000000000000034101445126167600154660ustar00rootroot00000000000000 2 2 1 1 2--1 3 3 2--3 3--1 graph-0.23.0/img/dag.svg000066400000000000000000000055011445126167600147420ustar00rootroot00000000000000 2 2 4 4 2->4 3 3 2->3 3->4 1 1 1->2 1->3 graph-0.23.0/img/dfs.svg000066400000000000000000000044151445126167600147660ustar00rootroot00000000000000 4 4 1 1 2 2 1->2 3 3 1->3 3->4 graph-0.23.0/img/dijkstra.svg000066400000000000000000000123541445126167600160260ustar00rootroot00000000000000 A A C C A--C 3 F F A--F 2 C--F 2 E E C--E 1 D D C--D 4 G G F--G 5 B B B--F 6 B--E 2 B--D 1 B--G 2 E--F 3 graph-0.23.0/img/logo.svg000066400000000000000000000072721445126167600151560ustar00rootroot00000000000000 graph-0.23.0/img/mst.svg000066400000000000000000000063241445126167600150160ustar00rootroot00000000000000 B B A A B--A 2 C C B--C 4 D D B--D 1 C--A 4 C--D 3 D--A 2 graph-0.23.0/img/scc.svg000066400000000000000000000146141445126167600147640ustar00rootroot00000000000000 3 3 4 4 3->4 7 7 3->7 4->3 8 8 4->8 6 6 7->6 8->4 8->7 6->7 1 1 2 2 1->2 2->3 2->6 5 5 2->5 5->6 5->1 graph-0.23.0/img/simple.svg000066400000000000000000000054411445126167600155030ustar00rootroot00000000000000 5 5 2 2 5--2 3 3 5--3 2--3 4 4 2--4 1 1 1--2 1--4 graph-0.23.0/img/topological-sort.svg000066400000000000000000000071651445126167600175200ustar00rootroot00000000000000 1 1 3 3 1->3 2 2 1->2 4 4 3->4 2->3 5 5 2->5 2->4 4->5 graph-0.23.0/img/transitive-reduction-after.svg000066400000000000000000000060311445126167600214670ustar00rootroot00000000000000 A A B B A->B C C A->C D D B->D C->D E E D->E graph-0.23.0/img/transitive-reduction-before.svg000066400000000000000000000077511445126167600216420ustar00rootroot00000000000000 D D E E D->E A A A->D A->E B B A->B C C A->C B->D C->D C->E graph-0.23.0/paths.go000066400000000000000000000216711445126167600143660ustar00rootroot00000000000000package graph import ( "errors" "fmt" "math" ) var ErrTargetNotReachable = errors.New("target vertex not reachable from source") // CreatesCycle determines whether adding an edge between the two given vertices // would introduce a cycle in the graph. CreatesCycle will not create an edge. // // A potential edge would create a cycle if the target vertex is also a parent // of the source vertex. In order to determine this, CreatesCycle runs a DFS. func CreatesCycle[K comparable, T any](g Graph[K, T], source, target K) (bool, error) { if _, err := g.Vertex(source); err != nil { return false, fmt.Errorf("could not get vertex with hash %v: %w", source, err) } if _, err := g.Vertex(target); err != nil { return false, fmt.Errorf("could not get vertex with hash %v: %w", target, err) } if source == target { return true, nil } predecessorMap, err := g.PredecessorMap() if err != nil { return false, fmt.Errorf("failed to get predecessor map: %w", err) } stack := make([]K, 0) visited := make(map[K]bool) stack = append(stack, source) for len(stack) > 0 { currentHash := stack[len(stack)-1] stack = stack[:len(stack)-1] if _, ok := visited[currentHash]; !ok { // If the adjacent vertex also is the target vertex, the target is a // parent of the source vertex. An edge would introduce a cycle. if currentHash == target { return true, nil } visited[currentHash] = true for adjacency := range predecessorMap[currentHash] { stack = append(stack, adjacency) } } } return false, nil } // ShortestPath computes the shortest path between a source and a target vertex // under consideration of the edge weights. It returns a slice of hash values of // the vertices forming that path. // // The returned path includes the source and target vertices. If the target is // not reachable from the source, ErrTargetNotReachable will be returned. Should // there be multiple shortest paths, and arbitrary one will be returned. // // ShortestPath has a time complexity of O(|V|+|E|log(|V|)). func ShortestPath[K comparable, T any](g Graph[K, T], source, target K) ([]K, error) { weights := make(map[K]float64) visited := make(map[K]bool) weights[source] = 0 visited[target] = true queue := newPriorityQueue[K]() adjacencyMap, err := g.AdjacencyMap() if err != nil { return nil, fmt.Errorf("could not get adjacency map: %w", err) } for hash := range adjacencyMap { if hash != source { weights[hash] = math.Inf(1) visited[hash] = false } queue.Push(hash, weights[hash]) } // bestPredecessors stores the cheapest or least-weighted predecessor for // each vertex. Given an edge AC with weight=4 and an edge BC with weight=2, // the cheapest predecessor for C is B. bestPredecessors := make(map[K]K) for queue.Len() > 0 { vertex, _ := queue.Pop() hasInfiniteWeight := math.IsInf(weights[vertex], 1) for adjacency, edge := range adjacencyMap[vertex] { edgeWeight := edge.Properties.Weight // Setting the weight to 1 is required for unweighted graphs whose // edge weights are 0. Otherwise, all paths would have a sum of 0 // and a random path would be returned. if !g.Traits().IsWeighted { edgeWeight = 1 } weight := weights[vertex] + float64(edgeWeight) if weight < weights[adjacency] && !hasInfiniteWeight { weights[adjacency] = weight bestPredecessors[adjacency] = vertex queue.UpdatePriority(adjacency, weight) } } } path := []K{target} current := target for current != source { // If the current vertex is not present in bestPredecessors, current is // set to the zero value of K. Without this check, this would lead to an // endless prepending of zero values to the path. Also, the target would // not be reachable from one of the preceding vertices. if _, ok := bestPredecessors[current]; !ok { return nil, ErrTargetNotReachable } current = bestPredecessors[current] path = append([]K{current}, path...) } return path, nil } type sccState[K comparable] struct { adjacencyMap map[K]map[K]Edge[K] components [][]K stack []K onStack map[K]bool visited map[K]struct{} lowlink map[K]int index map[K]int time int } // StronglyConnectedComponents detects all strongly connected components within // the graph and returns the hashes of the vertices shaping these components, so // each component is represented by a []K. // // StronglyConnectedComponents can only run on directed graphs. func StronglyConnectedComponents[K comparable, T any](g Graph[K, T]) ([][]K, error) { if !g.Traits().IsDirected { return nil, errors.New("SCCs can only be detected in directed graphs") } adjacencyMap, err := g.AdjacencyMap() if err != nil { return nil, fmt.Errorf("could not get adjacency map: %w", err) } state := &sccState[K]{ adjacencyMap: adjacencyMap, components: make([][]K, 0), stack: make([]K, 0), onStack: make(map[K]bool), visited: make(map[K]struct{}), lowlink: make(map[K]int), index: make(map[K]int), } for hash := range state.adjacencyMap { if _, ok := state.visited[hash]; !ok { findSCC(hash, state) } } return state.components, nil } func findSCC[K comparable](vertexHash K, state *sccState[K]) { state.stack = append(state.stack, vertexHash) state.onStack[vertexHash] = true state.visited[vertexHash] = struct{}{} state.index[vertexHash] = state.time state.lowlink[vertexHash] = state.time state.time++ for adjacency := range state.adjacencyMap[vertexHash] { if _, ok := state.visited[adjacency]; !ok { findSCC(adjacency, state) smallestLowlink := math.Min( float64(state.lowlink[vertexHash]), float64(state.lowlink[adjacency]), ) state.lowlink[vertexHash] = int(smallestLowlink) } else { // If the adjacent vertex already is on the stack, the edge joining // the current and the adjacent vertex is a back ege. Therefore, the // lowlink value of the vertex has to be updated to the index of the // adjacent vertex if it is smaller than the current lowlink value. if state.onStack[adjacency] { smallestLowlink := math.Min( float64(state.lowlink[vertexHash]), float64(state.index[adjacency]), ) state.lowlink[vertexHash] = int(smallestLowlink) } } } // If the lowlink value of the vertex is equal to its DFS value, this is the // head vertex of a strongly connected component that's shaped by the vertex // and all vertices on the stack. if state.lowlink[vertexHash] == state.index[vertexHash] { var hash K var component []K for hash != vertexHash { hash = state.stack[len(state.stack)-1] state.stack = state.stack[:len(state.stack)-1] state.onStack[hash] = false component = append(component, hash) } state.components = append(state.components, component) } } // AllPathsBetween computes and returns all paths between two given vertices. A // path is represented as a slice of vertex hashes. The returned slice contains // these paths. // // AllPathsBetween utilizes a non-recursive, stack-based implementation. It has // an estimated runtime complexity of O(n^2) where n is the number of vertices. func AllPathsBetween[K comparable, T any](g Graph[K, T], start, end K) ([][]K, error) { adjacencyMap, err := g.AdjacencyMap() if err != nil { return nil, err } // The algorithm used relies on stacks instead of recursion. It is described // here: https://boycgit.github.io/all-paths-between-two-vertex/ mainStack := newStack[K]() viceStack := newStack[stack[K]]() checkEmpty := func() error { if mainStack.isEmpty() || viceStack.isEmpty() { return errors.New("empty stack") } return nil } buildLayer := func(element K) { mainStack.push(element) newElements := newStack[K]() for e := range adjacencyMap[element] { var contains bool mainStack.forEach(func(k K) { if e == k { contains = true } }) if contains { continue } newElements.push(e) } viceStack.push(newElements) } buildStack := func() error { if err = checkEmpty(); err != nil { return fmt.Errorf("unable to build stack: %w", err) } elements, _ := viceStack.top() for !elements.isEmpty() { element, _ := elements.pop() buildLayer(element) elements, _ = viceStack.top() } return nil } removeLayer := func() error { if err = checkEmpty(); err != nil { return fmt.Errorf("unable to remove layer: %w", err) } if e, _ := viceStack.top(); !e.isEmpty() { return errors.New("the top element of vice-stack is not empty") } _, _ = mainStack.pop() _, _ = viceStack.pop() return nil } buildLayer(start) allPaths := make([][]K, 0) for !mainStack.isEmpty() { v, _ := mainStack.top() adjs, _ := viceStack.top() if adjs.isEmpty() { if v == end { path := make([]K, 0) mainStack.forEach(func(k K) { path = append(path, k) }) allPaths = append(allPaths, path) } err = removeLayer() if err != nil { return nil, err } } else { if err = buildStack(); err != nil { return nil, err } } } return allPaths, nil } graph-0.23.0/paths_test.go000066400000000000000000000455621445126167600154320ustar00rootroot00000000000000package graph import ( "reflect" "sort" "testing" ) func TestDirectedCreatesCycle(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] sourceHash int targetHash int createsCycle bool }{ "directed 2-4-7-5 cycle": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 2}, }, sourceHash: 7, targetHash: 5, createsCycle: true, }, "undirected 2-4-7-5 'cycle'": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 2}, }, sourceHash: 5, targetHash: 7, // The direction of the edge (57 instead of 75) doesn't create a directed cycle. createsCycle: false, }, "no cycle": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 2}, }, sourceHash: 5, targetHash: 6, createsCycle: false, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } createsCycle, err := CreatesCycle(graph, test.sourceHash, test.targetHash) if err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } if createsCycle != test.createsCycle { t.Errorf("%s: cycle expectancy doesn't match: expected %v, got %v", name, test.createsCycle, createsCycle) } } } func TestUndirectedCreatesCycle(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] sourceHash int targetHash int createsCycle bool }{ "undirected 2-4-7-5 cycle": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 7}, }, sourceHash: 2, targetHash: 5, createsCycle: true, }, "undirected 5-6-3-1-2-7 cycle": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 7}, }, sourceHash: 5, targetHash: 6, createsCycle: true, }, "no cycle": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, }, sourceHash: 5, targetHash: 7, createsCycle: false, }, } for name, test := range tests { graph := New(IntHash) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } createsCycle, err := CreatesCycle(graph, test.sourceHash, test.targetHash) if err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } if createsCycle != test.createsCycle { t.Errorf("%s: cycle expectancy doesn't match: expected %v, got %v", name, test.createsCycle, createsCycle) } } } func TestDirectedShortestPath(t *testing.T) { tests := map[string]struct { vertices []string edges []Edge[string] isWeighted bool sourceHash string targetHash string expectedShortestPath []string shouldFail bool }{ "graph as on img/dijkstra.svg": { vertices: []string{"A", "B", "C", "D", "E", "F", "G"}, edges: []Edge[string]{ {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 3}}, {Source: "A", Target: "F", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 4}}, {Source: "C", Target: "E", Properties: EdgeProperties{Weight: 1}}, {Source: "C", Target: "F", Properties: EdgeProperties{Weight: 2}}, {Source: "D", Target: "B", Properties: EdgeProperties{Weight: 1}}, {Source: "E", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "E", Target: "F", Properties: EdgeProperties{Weight: 3}}, {Source: "F", Target: "G", Properties: EdgeProperties{Weight: 5}}, {Source: "G", Target: "B", Properties: EdgeProperties{Weight: 2}}, }, isWeighted: true, sourceHash: "A", targetHash: "B", expectedShortestPath: []string{"A", "C", "E", "B"}, }, "diamond-shaped graph": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 2}}, }, isWeighted: true, sourceHash: "A", targetHash: "D", expectedShortestPath: []string{"A", "B", "D"}, }, "unweighted graph": { vertices: []string{"A", "B", "C", "D", "E", "F", "G"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{}}, {Source: "A", Target: "C", Properties: EdgeProperties{}}, {Source: "B", Target: "D", Properties: EdgeProperties{}}, {Source: "C", Target: "F", Properties: EdgeProperties{}}, {Source: "D", Target: "G", Properties: EdgeProperties{}}, {Source: "E", Target: "G", Properties: EdgeProperties{}}, {Source: "F", Target: "E", Properties: EdgeProperties{}}, }, sourceHash: "A", targetHash: "G", expectedShortestPath: []string{"A", "B", "D", "G"}, }, "source equal to target": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 2}}, }, isWeighted: true, sourceHash: "B", targetHash: "B", expectedShortestPath: []string{"B"}, }, "target not reachable in a disconnected graph": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, }, isWeighted: true, sourceHash: "A", targetHash: "D", expectedShortestPath: []string{}, shouldFail: true, }, "target not reachable in a connected graph": { vertices: []string{"A", "B", "C"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 0}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 0}}, }, sourceHash: "B", targetHash: "C", expectedShortestPath: []string{}, shouldFail: true, }, "graph from issue 88": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 6}}, {Source: "B", Target: "C", Properties: EdgeProperties{Weight: 3}}, {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 5}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 1}}, }, isWeighted: true, sourceHash: "A", targetHash: "D", expectedShortestPath: []string{"A", "B", "C", "D"}, }, } for name, test := range tests { graph := New(StringHash, Directed()) graph.(*directed[string, string]).traits.IsWeighted = test.isWeighted for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } shortestPath, err := ShortestPath(graph, test.sourceHash, test.targetHash) if test.shouldFail != (err != nil) { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, (err != nil), err) } if len(shortestPath) != len(test.expectedShortestPath) { t.Fatalf("%s: path length expectancy doesn't match: expected %v, got %v", name, len(test.expectedShortestPath), len(shortestPath)) } for i, expectedVertex := range test.expectedShortestPath { if shortestPath[i] != expectedVertex { t.Errorf("%s: path vertex expectancy doesn't match: expected %v at index %d, got %v", name, expectedVertex, i, shortestPath[i]) } } } } func TestUndirectedShortestPath(t *testing.T) { tests := map[string]struct { vertices []string edges []Edge[string] sourceHash string targetHash string isWeighted bool expectedShortestPath []string shouldFail bool }{ "graph as on img/dijkstra.svg": { vertices: []string{"A", "B", "C", "D", "E", "F", "G"}, edges: []Edge[string]{ {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 3}}, {Source: "A", Target: "F", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 4}}, {Source: "C", Target: "E", Properties: EdgeProperties{Weight: 1}}, {Source: "C", Target: "F", Properties: EdgeProperties{Weight: 2}}, {Source: "D", Target: "B", Properties: EdgeProperties{Weight: 1}}, {Source: "E", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "E", Target: "F", Properties: EdgeProperties{Weight: 3}}, {Source: "F", Target: "G", Properties: EdgeProperties{Weight: 5}}, {Source: "G", Target: "B", Properties: EdgeProperties{Weight: 2}}, }, isWeighted: true, sourceHash: "A", targetHash: "B", expectedShortestPath: []string{"A", "C", "E", "B"}, }, "diamond-shaped graph": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 2}}, }, isWeighted: true, sourceHash: "A", targetHash: "D", expectedShortestPath: []string{"A", "B", "D"}, }, "unweighted graph": { vertices: []string{"A", "B", "C", "D", "E", "F", "G"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{}}, {Source: "A", Target: "C", Properties: EdgeProperties{}}, {Source: "B", Target: "D", Properties: EdgeProperties{}}, {Source: "C", Target: "F", Properties: EdgeProperties{}}, {Source: "D", Target: "G", Properties: EdgeProperties{}}, {Source: "E", Target: "G", Properties: EdgeProperties{}}, {Source: "F", Target: "E", Properties: EdgeProperties{}}, }, sourceHash: "A", targetHash: "G", expectedShortestPath: []string{"A", "B", "D", "G"}, }, "source equal to target": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 2}}, }, isWeighted: true, sourceHash: "B", targetHash: "B", expectedShortestPath: []string{"B"}, }, "target not reachable in a disconnected graph": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, }, isWeighted: true, sourceHash: "A", targetHash: "D", expectedShortestPath: []string{}, shouldFail: true, }, } for name, test := range tests { graph := New(StringHash) graph.(*undirected[string, string]).traits.IsWeighted = test.isWeighted for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } shortestPath, err := ShortestPath(graph, test.sourceHash, test.targetHash) if test.shouldFail != (err != nil) { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, (err != nil), err) } if len(shortestPath) != len(test.expectedShortestPath) { t.Fatalf("%s: path length expectancy doesn't match: expected %v, got %v", name, len(test.expectedShortestPath), len(shortestPath)) } for i, expectedVertex := range test.expectedShortestPath { if shortestPath[i] != expectedVertex { t.Errorf("%s: path vertex expectancy doesn't match: expected %v at index %d, got %v", name, expectedVertex, i, shortestPath[i]) } } } } func TestDirectedStronglyConnectedComponents(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expectedSCCs [][]int }{ "graph with SCCs as on img/scc.svg": { vertices: []int{1, 2, 3, 4, 5, 6, 7, 8}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 2, Target: 5}, {Source: 2, Target: 6}, {Source: 3, Target: 4}, {Source: 3, Target: 7}, {Source: 4, Target: 3}, {Source: 4, Target: 8}, {Source: 5, Target: 1}, {Source: 5, Target: 6}, {Source: 6, Target: 7}, {Source: 7, Target: 6}, {Source: 8, Target: 4}, {Source: 8, Target: 7}, }, expectedSCCs: [][]int{{1, 2, 5}, {3, 4, 8}, {6, 7}}, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } sccs, _ := StronglyConnectedComponents(graph) matchedSCCs := 0 for _, scc := range sccs { for _, expectedSCC := range test.expectedSCCs { if slicesAreEqual(scc, expectedSCC) { matchedSCCs++ } } } if matchedSCCs != len(test.expectedSCCs) { t.Errorf("%s: expected SCCs don't match: expected %v, got %v", name, test.expectedSCCs, sccs) } } } func TestUndirectedStronglyConnectedComponents(t *testing.T) { tests := map[string]struct { expectedSCCs [][]int shouldFail bool }{ "return error": { expectedSCCs: nil, shouldFail: true, }, } for name, test := range tests { graph := New(IntHash) sccs, err := StronglyConnectedComponents(graph) if test.shouldFail != (err != nil) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, (err != nil), err) } if test.expectedSCCs == nil && sccs != nil { t.Errorf("%s: SCC expectancy doesn't match: expcted %v, got %v", name, test.expectedSCCs, sccs) } } } func TestAllPathsBetween(t *testing.T) { type args[K comparable, T any] struct { g Graph[K, T] start K end K } type testCase[K comparable, T any] struct { name string args args[K, T] want [][]K wantErr bool } tests := []testCase[int, int]{ { name: "directed", args: args[int, int]{ g: func() Graph[int, int] { g := New(IntHash, Directed()) for i := 0; i <= 8; i++ { _ = g.AddVertex(i) } _ = g.AddEdge(0, 2) _ = g.AddEdge(1, 0) _ = g.AddEdge(1, 4) _ = g.AddEdge(2, 6) _ = g.AddEdge(3, 1) _ = g.AddEdge(3, 7) _ = g.AddEdge(4, 5) _ = g.AddEdge(5, 2) _ = g.AddEdge(5, 6) _ = g.AddEdge(6, 8) _ = g.AddEdge(7, 4) return g }(), start: 3, end: 6, }, want: [][]int{ {3, 1, 0, 2, 6}, {3, 1, 4, 5, 6}, {3, 1, 4, 5, 2, 6}, {3, 7, 4, 5, 2, 6}, {3, 7, 4, 5, 6}, }, wantErr: false, }, { name: "undirected", args: args[int, int]{ g: func() Graph[int, int] { g := New(IntHash) for i := 0; i <= 8; i++ { _ = g.AddVertex(i) } _ = g.AddEdge(0, 1) _ = g.AddEdge(0, 2) _ = g.AddEdge(1, 3) _ = g.AddEdge(1, 4) _ = g.AddEdge(2, 5) _ = g.AddEdge(2, 6) _ = g.AddEdge(3, 7) _ = g.AddEdge(4, 5) _ = g.AddEdge(4, 7) _ = g.AddEdge(5, 6) _ = g.AddEdge(6, 8) return g }(), start: 3, end: 6, }, want: [][]int{ {3, 1, 0, 2, 6}, {3, 1, 0, 2, 5, 6}, {3, 1, 4, 5, 6}, {3, 1, 4, 5, 2, 6}, {3, 7, 4, 5, 2, 6}, {3, 7, 4, 5, 6}, {3, 7, 4, 1, 0, 2, 6}, {3, 7, 4, 1, 0, 2, 5, 6}, }, wantErr: false, }, { name: "undirected (complex)", args: args[int, int]{ g: func() Graph[int, int] { g := New(IntHash) for i := 0; i <= 9; i++ { _ = g.AddVertex(i) } _ = g.AddEdge(0, 1) _ = g.AddEdge(0, 2) _ = g.AddEdge(0, 3) _ = g.AddEdge(2, 3) _ = g.AddEdge(2, 6) _ = g.AddEdge(3, 4) _ = g.AddEdge(4, 8) _ = g.AddEdge(5, 6) _ = g.AddEdge(5, 8) _ = g.AddEdge(5, 9) _ = g.AddEdge(6, 7) _ = g.AddEdge(7, 9) _ = g.AddEdge(8, 9) return g }(), start: 0, end: 9, }, want: [][]int{ {0, 2, 3, 4, 8, 5, 6, 7, 9}, {0, 2, 3, 4, 8, 5, 9}, {0, 2, 3, 4, 8, 9}, {0, 2, 6, 5, 8, 9}, {0, 2, 6, 5, 9}, {0, 2, 6, 7, 9}, {0, 3, 2, 6, 5, 8, 9}, {0, 3, 2, 6, 5, 9}, {0, 3, 2, 6, 7, 9}, {0, 3, 4, 8, 5, 6, 7, 9}, {0, 3, 4, 8, 5, 9}, {0, 3, 4, 8, 9}, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := AllPathsBetween(tt.args.g, tt.args.start, tt.args.end) if (err != nil) != tt.wantErr { t.Errorf("AllPathsBetween() error = %v, wantErr %v", err, tt.wantErr) return } toStr := func(s []int) string { var num string for _, n := range s { num = num + string(rune(n)) } return num } sort.Slice(got, func(i, j int) bool { return toStr(got[i]) < toStr(got[j]) }) sort.Slice(tt.want, func(i, j int) bool { return toStr(tt.want[i]) < toStr(tt.want[j]) }) if !reflect.DeepEqual(got, tt.want) { t.Errorf("AllPathsBetween() got = %v, want %v", got, tt.want) } }) } } graph-0.23.0/sets.go000066400000000000000000000060071445126167600142210ustar00rootroot00000000000000package graph import ( "fmt" ) // Union combines two given graphs into a new graph. The vertex hashes in both // graphs are expected to be unique. The two input graphs will remain unchanged. // // Both graphs should be either directed or undirected. All traits for the new // graph will be derived from g. func Union[K comparable, T any](g, h Graph[K, T]) (Graph[K, T], error) { union, err := g.Clone() if err != nil { return union, fmt.Errorf("failed to clone g: %w", err) } adjacencyMap, err := h.AdjacencyMap() if err != nil { return union, fmt.Errorf("failed to get adjacency map: %w", err) } addedEdges := make(map[K]map[K]struct{}) for currentHash := range adjacencyMap { vertex, properties, err := h.VertexWithProperties(currentHash) //nolint:govet if err != nil { return union, fmt.Errorf("failed to get vertex %v: %w", currentHash, err) } err = union.AddVertex(vertex, copyVertexProperties(properties)) if err != nil { return union, fmt.Errorf("failed to add vertex %v: %w", currentHash, err) } } for _, adjacencies := range adjacencyMap { for _, edge := range adjacencies { if _, sourceOK := addedEdges[edge.Source]; sourceOK { if _, targetOK := addedEdges[edge.Source][edge.Target]; targetOK { // If the edge addedEdges[source][target] exists, the edge // has already been created and thus can be skipped here. continue } } err = union.AddEdge(copyEdge(edge)) if err != nil { return union, fmt.Errorf("failed to add edge (%v, %v): %w", edge.Source, edge.Target, err) } if _, ok := addedEdges[edge.Source]; !ok { addedEdges[edge.Source] = make(map[K]struct{}) } addedEdges[edge.Source][edge.Target] = struct{}{} } } return union, nil } // unionFind implements a union-find or disjoint set data structure that works // with vertex hashes as vertices. It's an internal helper type at the moment, // but could perhaps be exposed publicly in the future. // // unionFind is not related to the Union function. type unionFind[K comparable] struct { parents map[K]K } func newUnionFind[K comparable](vertices ...K) *unionFind[K] { u := &unionFind[K]{ parents: make(map[K]K, len(vertices)), } for _, vertex := range vertices { u.parents[vertex] = vertex } return u } func (u *unionFind[K]) add(vertex K) { u.parents[vertex] = vertex } func (u *unionFind[K]) union(vertex1, vertex2 K) { root1 := u.find(vertex1) root2 := u.find(vertex2) if root1 == root2 { return } u.parents[root2] = root1 } func (u *unionFind[K]) find(vertex K) K { root := vertex for u.parents[root] != root { root = u.parents[root] } // Perform a path compression in order to optimize of future find calls. current := vertex for u.parents[current] != root { parent := u.parents[vertex] u.parents[vertex] = root current = parent } return root } func copyVertexProperties(source VertexProperties) func(*VertexProperties) { return func(p *VertexProperties) { for k, v := range source.Attributes { p.Attributes[k] = v } p.Weight = source.Weight } } graph-0.23.0/sets_test.go000066400000000000000000000153101445126167600152550ustar00rootroot00000000000000package graph import ( "testing" ) func TestDirectedUnion(t *testing.T) { tests := map[string]struct { gVertices []int gVertexProperties map[int]VertexProperties gEdges []Edge[int] hVertices []int hVertexProperties map[int]VertexProperties hEdges []Edge[int] expectedAdjacencyMap map[int]map[int]Edge[int] }{ "two 3-vertices directed graphs": { gVertices: []int{1, 2, 3}, gVertexProperties: map[int]VertexProperties{}, gEdges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, }, hVertices: []int{4, 5, 6}, hVertexProperties: map[int]VertexProperties{}, hEdges: []Edge[int]{ {Source: 4, Target: 5}, {Source: 5, Target: 6}, }, expectedAdjacencyMap: map[int]map[int]Edge[int]{ 1: { 2: {Source: 1, Target: 2}, }, 2: { 3: {Source: 2, Target: 3}, }, 3: {}, 4: { 5: {Source: 4, Target: 5}, }, 5: { 6: {Source: 5, Target: 6}, }, 6: {}, }, }, "vertices and edges with properties": { gVertices: []int{1, 2}, gVertexProperties: map[int]VertexProperties{ 1: { Attributes: map[string]string{ "color": "red", }, Weight: 10, }, 2: { Attributes: map[string]string{}, Weight: 20, }, }, gEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Attributes: map[string]string{ "label": "my-edge", }, Weight: 42, Data: "edge data #1", }, }, }, hVertices: []int{3, 4}, hVertexProperties: map[int]VertexProperties{ 3: { Attributes: map[string]string{ "color": "blue", }, Weight: 15, }, }, hEdges: []Edge[int]{ { Source: 3, Target: 4, Properties: EdgeProperties{ Attributes: map[string]string{ "label": "another-edge", }, Weight: 50, Data: "edge data #2", }, }, }, expectedAdjacencyMap: map[int]map[int]Edge[int]{ 1: { 2: { Source: 1, Target: 2, Properties: EdgeProperties{ Attributes: map[string]string{ "label": "my-edge", }, Weight: 42, Data: "edge data #1", }, }, }, 2: {}, 3: { 4: { Source: 3, Target: 4, Properties: EdgeProperties{ Attributes: map[string]string{ "label": "another-edge", }, Weight: 50, Data: "edge data #2", }, }, }, 4: {}, }, }, } for name, test := range tests { g := New(IntHash, Directed()) for _, vertex := range test.gVertices { _ = g.AddVertex(vertex, copyVertexProperties(test.gVertexProperties[vertex])) } for _, edge := range test.gEdges { _ = g.AddEdge(copyEdge(edge)) } h := New(IntHash, Directed()) for _, vertex := range test.hVertices { _ = h.AddVertex(vertex, copyVertexProperties(test.gVertexProperties[vertex])) } for _, edge := range test.hEdges { _ = h.AddEdge(copyEdge(edge)) } union, err := Union(g, h) if err != nil { t.Fatalf("%s: unexpected union error: %s", name, err.Error()) } unionAdjacencyMap, err := union.AdjacencyMap() if err != nil { t.Fatalf("%s: unexpected adjaceny map error: %s", name, err.Error()) } edgesAreEqual := g.(*directed[int, int]).edgesAreEqual if !adjacencyMapsAreEqual(test.expectedAdjacencyMap, unionAdjacencyMap, edgesAreEqual) { t.Fatalf("expected adjacency map %v, got %v", test.expectedAdjacencyMap, unionAdjacencyMap) } } } func TestUnionFind_add(t *testing.T) { tests := map[string]struct { vertex int expectedParent int }{ "add vertex 1": { vertex: 1, expectedParent: 1, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { u := newUnionFind[int]() u.add(test.vertex) if u.parents[test.vertex] != test.expectedParent { t.Errorf("expected parent %v, got %v", test.expectedParent, u.parents[test.vertex]) } }) } } func TestUnionFind_union(t *testing.T) { tests := map[string]struct { vertices []int args [2]int expectedParents map[int]int }{ "merge 1 and 2": { vertices: []int{1, 2, 3, 4}, args: [2]int{1, 2}, expectedParents: map[int]int{ 1: 1, 2: 1, 3: 3, 4: 4, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { u := newUnionFind(test.vertices...) u.union(test.args[0], test.args[1]) for expectedKey, expectedValue := range test.expectedParents { actualValue, ok := u.parents[expectedKey] if !ok { t.Fatalf("expected key %v not found", expectedKey) } if actualValue != expectedValue { t.Fatalf("expected value %v for %v, got %v", expectedValue, expectedKey, actualValue) } } }) } } func TestUnionFind_find(t *testing.T) { tests := map[string]struct { parents map[int]int arg int expected int }{ "find 1 in sets 1, 2, 3": { parents: map[int]int{ 1: 1, 2: 2, 3: 3, }, arg: 1, expected: 1, }, "find 3 in sets 1-2, 3-4": { parents: map[int]int{ 1: 1, 2: 1, 3: 3, 4: 3, }, arg: 3, expected: 3, }, "find 4 in sets 1-2, 3-4": { parents: map[int]int{ 1: 1, 2: 1, 3: 3, 4: 3, }, arg: 4, expected: 3, }, "find 3 in set 1-2-3": { parents: map[int]int{ 1: 1, 2: 1, 3: 2, }, arg: 3, expected: 1, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { u := unionFind[int]{ parents: test.parents, } actual := u.find(test.arg) if actual != test.expected { t.Errorf("expected %v, got %v", test.expected, actual) } }) } } func adjacencyMapsAreEqual[K comparable](a, b map[K]map[K]Edge[K], edgesAreEqual func(a, b Edge[K]) bool) bool { for aHash, aAdjacencies := range a { bAdjacencies, ok := b[aHash] if !ok { return false } for aAdjacency, aEdge := range aAdjacencies { bEdge, ok := bAdjacencies[aAdjacency] if !ok { return false } if !edgesAreEqual(aEdge, bEdge) { return false } for aKey, aValue := range aEdge.Properties.Attributes { bValue, ok := bEdge.Properties.Attributes[aKey] if !ok { return false } if bValue != aValue { return false } } if bEdge.Properties.Weight != aEdge.Properties.Weight { return false } } } for aHash := range a { if _, ok := b[aHash]; !ok { return false } } return true } func mapsAreEqual[K comparable](a, b map[K]K) bool { for aHash, aValue := range a { bValue, ok := b[aHash] if !ok { return false } if aValue != bValue { return false } } for aHash := range a { if _, ok := b[aHash]; !ok { return false } } return true } graph-0.23.0/store.go000066400000000000000000000172531445126167600144040ustar00rootroot00000000000000package graph import ( "fmt" "sync" ) // Store represents a storage for vertices and edges. The graph library provides an in-memory store // by default and accepts any Store implementation to work with - for example, an SQL store. // // When implementing your own Store, make sure the individual methods and their behavior adhere to // this documentation. Otherwise, the graphs aren't guaranteed to behave as expected. type Store[K comparable, T any] interface { // AddVertex should add the given vertex with the given hash value and vertex properties to the // graph. If the vertex already exists, it is up to you whether ErrVertexAlreadyExists or no // error should be returned. AddVertex(hash K, value T, properties VertexProperties) error // Vertex should return the vertex and vertex properties with the given hash value. If the // vertex doesn't exist, ErrVertexNotFound should be returned. Vertex(hash K) (T, VertexProperties, error) // RemoveVertex should remove the vertex with the given hash value. If the vertex doesn't // exist, ErrVertexNotFound should be returned. If the vertex has edges to other vertices, // ErrVertexHasEdges should be returned. RemoveVertex(hash K) error // ListVertices should return all vertices in the graph in a slice. ListVertices() ([]K, error) // VertexCount should return the number of vertices in the graph. This should be equal to the // length of the slice returned by ListVertices. VertexCount() (int, error) // AddEdge should add an edge between the vertices with the given source and target hashes. // // If either vertex doesn't exit, ErrVertexNotFound should be returned for the respective // vertex. If the edge already exists, ErrEdgeAlreadyExists should be returned. AddEdge(sourceHash, targetHash K, edge Edge[K]) error // UpdateEdge should update the edge between the given vertices with the data of the given // Edge instance. If the edge doesn't exist, ErrEdgeNotFound should be returned. UpdateEdge(sourceHash, targetHash K, edge Edge[K]) error // RemoveEdge should remove the edge between the vertices with the given source and target // hashes. // // If either vertex doesn't exist, it is up to you whether ErrVertexNotFound or no error should // be returned. If the edge doesn't exist, it is up to you whether ErrEdgeNotFound or no error // should be returned. RemoveEdge(sourceHash, targetHash K) error // Edge should return the edge joining the vertices with the given hash values. It should // exclusively look for an edge between the source and the target vertex, not vice versa. The // graph implementation does this for undirected graphs itself. // // Note that unlike Graph.Edge, this function is supposed to return an Edge[K], i.e. an edge // that only contains the vertex hashes instead of the vertices themselves. // // If the edge doesn't exist, ErrEdgeNotFound should be returned. Edge(sourceHash, targetHash K) (Edge[K], error) // ListEdges should return all edges in the graph in a slice. ListEdges() ([]Edge[K], error) } type memoryStore[K comparable, T any] struct { lock sync.RWMutex vertices map[K]T vertexProperties map[K]VertexProperties // outEdges and inEdges store all outgoing and ingoing edges for all vertices. For O(1) access, // these edges themselves are stored in maps whose keys are the hashes of the target vertices. outEdges map[K]map[K]Edge[K] // source -> target inEdges map[K]map[K]Edge[K] // target -> source } func newMemoryStore[K comparable, T any]() Store[K, T] { return &memoryStore[K, T]{ vertices: make(map[K]T), vertexProperties: make(map[K]VertexProperties), outEdges: make(map[K]map[K]Edge[K]), inEdges: make(map[K]map[K]Edge[K]), } } func (s *memoryStore[K, T]) AddVertex(k K, t T, p VertexProperties) error { s.lock.Lock() defer s.lock.Unlock() if _, ok := s.vertices[k]; ok { return ErrVertexAlreadyExists } s.vertices[k] = t s.vertexProperties[k] = p return nil } func (s *memoryStore[K, T]) ListVertices() ([]K, error) { s.lock.RLock() defer s.lock.RUnlock() hashes := make([]K, 0, len(s.vertices)) for k := range s.vertices { hashes = append(hashes, k) } return hashes, nil } func (s *memoryStore[K, T]) VertexCount() (int, error) { s.lock.RLock() defer s.lock.RUnlock() return len(s.vertices), nil } func (s *memoryStore[K, T]) Vertex(k K) (T, VertexProperties, error) { s.lock.RLock() defer s.lock.RUnlock() v, ok := s.vertices[k] if !ok { return v, VertexProperties{}, ErrVertexNotFound } p := s.vertexProperties[k] return v, p, nil } func (s *memoryStore[K, T]) RemoveVertex(k K) error { s.lock.RLock() defer s.lock.RUnlock() if _, ok := s.vertices[k]; !ok { return ErrVertexNotFound } if edges, ok := s.inEdges[k]; ok { if len(edges) > 0 { return ErrVertexHasEdges } delete(s.inEdges, k) } if edges, ok := s.outEdges[k]; ok { if len(edges) > 0 { return ErrVertexHasEdges } delete(s.outEdges, k) } delete(s.vertices, k) delete(s.vertexProperties, k) return nil } func (s *memoryStore[K, T]) AddEdge(sourceHash, targetHash K, edge Edge[K]) error { s.lock.Lock() defer s.lock.Unlock() if _, ok := s.outEdges[sourceHash]; !ok { s.outEdges[sourceHash] = make(map[K]Edge[K]) } s.outEdges[sourceHash][targetHash] = edge if _, ok := s.inEdges[targetHash]; !ok { s.inEdges[targetHash] = make(map[K]Edge[K]) } s.inEdges[targetHash][sourceHash] = edge return nil } func (s *memoryStore[K, T]) UpdateEdge(sourceHash, targetHash K, edge Edge[K]) error { if _, err := s.Edge(sourceHash, targetHash); err != nil { return err } s.lock.Lock() defer s.lock.Unlock() s.outEdges[sourceHash][targetHash] = edge s.inEdges[targetHash][sourceHash] = edge return nil } func (s *memoryStore[K, T]) RemoveEdge(sourceHash, targetHash K) error { s.lock.Lock() defer s.lock.Unlock() delete(s.inEdges[targetHash], sourceHash) delete(s.outEdges[sourceHash], targetHash) return nil } func (s *memoryStore[K, T]) Edge(sourceHash, targetHash K) (Edge[K], error) { s.lock.RLock() defer s.lock.RUnlock() sourceEdges, ok := s.outEdges[sourceHash] if !ok { return Edge[K]{}, ErrEdgeNotFound } edge, ok := sourceEdges[targetHash] if !ok { return Edge[K]{}, ErrEdgeNotFound } return edge, nil } func (s *memoryStore[K, T]) ListEdges() ([]Edge[K], error) { s.lock.RLock() defer s.lock.RUnlock() res := make([]Edge[K], 0) for _, edges := range s.outEdges { for _, edge := range edges { res = append(res, edge) } } return res, nil } // CreatesCycle is a fastpath version of [CreatesCycle] that avoids calling // [PredecessorMap], which generates large amounts of garbage to collect. // // Because CreatesCycle doesn't need to modify the PredecessorMap, we can use // inEdges instead to compute the same thing without creating any copies. func (s *memoryStore[K, T]) CreatesCycle(source, target K) (bool, error) { if _, _, err := s.Vertex(source); err != nil { return false, fmt.Errorf("could not get vertex with hash %v: %w", source, err) } if _, _, err := s.Vertex(target); err != nil { return false, fmt.Errorf("could not get vertex with hash %v: %w", target, err) } if source == target { return true, nil } stack := make([]K, 0) visited := make(map[K]struct{}) stack = append(stack, source) for len(stack) > 0 { currentHash := stack[len(stack)-1] stack = stack[:len(stack)-1] if _, ok := visited[currentHash]; !ok { // If the adjacent vertex also is the target vertex, the target is a // parent of the source vertex. An edge would introduce a cycle. if currentHash == target { return true, nil } visited[currentHash] = struct{}{} for adjacency := range s.inEdges[currentHash] { stack = append(stack, adjacency) } } } return false, nil } graph-0.23.0/store_test.go000066400000000000000000000022741445126167600154400ustar00rootroot00000000000000package graph import ( "errors" "testing" ) func TestRemoveEdge(t *testing.T) { noerr := func(err error) { if err != nil { panic(err) } } build := func(nodes []string, edges [][]string) Store[string, string] { store := newMemoryStore[string, string]() for _, n := range nodes { noerr(store.AddVertex(n, n, VertexProperties{})) } for _, e := range edges { noerr(store.AddEdge(e[0], e[1], Edge[string]{ Source: e[0], Target: e[1], })) } return store } t.Run("normal remove", func(t *testing.T) { g := build([]string{ "a", "b", "c", }, [][]string{ {"a", "b"}, {"a", "c"}, }) noerr(g.RemoveEdge("a", "b")) noerr(g.RemoveEdge("a", "c")) noerr(g.RemoveVertex("a")) }) t.Run("remove edge has in-edges", func(t *testing.T) { g := build([]string{ "a", "b", "c", }, [][]string{ {"a", "b"}, {"a", "c"}, }) if err := g.RemoveVertex("b"); !errors.Is(err, ErrVertexHasEdges) { t.Fail() } }) t.Run("remove edge has out-edges", func(t *testing.T) { g := build([]string{ "a", "b", "c", }, [][]string{ {"a", "b"}, {"a", "c"}, }) if err := g.RemoveVertex("a"); !errors.Is(err, ErrVertexHasEdges) { t.Fail() } }) } graph-0.23.0/traits.go000066400000000000000000000033431445126167600145510ustar00rootroot00000000000000package graph // Traits represents a set of graph traits and types, such as directedness or acyclicness. These // traits can be set when creating a graph by passing the corresponding functional options, for // example: // // g := graph.New(graph.IntHash, graph.Directed()) // // This will set the IsDirected field to true. type Traits struct { IsDirected bool IsAcyclic bool IsWeighted bool IsRooted bool PreventCycles bool } // Directed creates a directed graph. This has implications on graph traversal and the order of // arguments of the Edge and AddEdge functions. func Directed() func(*Traits) { return func(t *Traits) { t.IsDirected = true } } // Acyclic creates an acyclic graph. Note that creating edges that form a cycle will still be // possible. To prevent this explicitly, use PreventCycles. func Acyclic() func(*Traits) { return func(t *Traits) { t.IsAcyclic = true } } // Weighted creates a weighted graph. To set weights, use the Edge and AddEdge functions. func Weighted() func(*Traits) { return func(t *Traits) { t.IsWeighted = true } } // Rooted creates a rooted graph. This is particularly common for building tree data structures. func Rooted() func(*Traits) { return func(t *Traits) { t.IsRooted = true } } // Tree is an alias for Acyclic and Rooted, since most trees in Computer Science are rooted trees. func Tree() func(*Traits) { return func(t *Traits) { Acyclic()(t) Rooted()(t) } } // PreventCycles creates an acyclic graph that prevents and proactively prevents the creation of // cycles. These cycle checks affect the performance and complexity of operations such as AddEdge. func PreventCycles() func(*Traits) { return func(t *Traits) { Acyclic()(t) t.PreventCycles = true } } graph-0.23.0/traits_test.go000066400000000000000000000050251445126167600156070ustar00rootroot00000000000000package graph import "testing" func TestDirected(t *testing.T) { tests := map[string]struct { expected *Traits }{ "directed graph": { expected: &Traits{ IsDirected: true, }, }, } for name, test := range tests { p := &Traits{} Directed()(p) if !traitsAreEqual(test.expected, p) { t.Errorf("%s: trait expectation doesn't match: expected %v, got %v", name, test.expected, p) } } } func TestAcyclic(t *testing.T) { tests := map[string]struct { expected *Traits }{ "acyclic graph": { expected: &Traits{ IsAcyclic: true, }, }, } for name, test := range tests { p := &Traits{} Acyclic()(p) if !traitsAreEqual(test.expected, p) { t.Errorf("%s: trait expectation doesn't match: expected %v, got %v", name, test.expected, p) } } } func TestWeighted(t *testing.T) { tests := map[string]struct { expected *Traits }{ "weighted graph": { expected: &Traits{ IsWeighted: true, }, }, } for name, test := range tests { p := &Traits{} Weighted()(p) if !traitsAreEqual(test.expected, p) { t.Errorf("%s: trait expectation doesn't match: expected %v, got %v", name, test.expected, p) } } } func TestRooted(t *testing.T) { tests := map[string]struct { expected *Traits }{ "rooted graph": { expected: &Traits{ IsRooted: true, }, }, } for name, test := range tests { p := &Traits{} Rooted()(p) if !traitsAreEqual(test.expected, p) { t.Errorf("%s: trait expectation doesn't match: expected %v, got %v", name, test.expected, p) } } } func TestTree(t *testing.T) { tests := map[string]struct { expected *Traits }{ "tree graph": { expected: &Traits{ IsAcyclic: true, IsRooted: true, }, }, } for name, test := range tests { p := &Traits{} Tree()(p) if !traitsAreEqual(test.expected, p) { t.Errorf("%s: trait expectation doesn't match: expected %v, got %v", name, test.expected, p) } } } func TestPreventCycles(t *testing.T) { tests := map[string]struct { expected *Traits }{ "prevent cycles": { expected: &Traits{ IsAcyclic: true, PreventCycles: true, }, }, } for name, test := range tests { p := &Traits{} PreventCycles()(p) if !traitsAreEqual(test.expected, p) { t.Errorf("%s: trait expectation doesn't match: expected %v, got %v", name, test.expected, p) } } } func traitsAreEqual(a, b *Traits) bool { return a.IsAcyclic == b.IsAcyclic && a.IsDirected == b.IsDirected && a.IsRooted == b.IsRooted && a.IsWeighted == b.IsWeighted && a.PreventCycles == b.PreventCycles } graph-0.23.0/traversal.go000066400000000000000000000104221445126167600152420ustar00rootroot00000000000000package graph import "fmt" // DFS performs a depth-first search on the graph, starting from the given vertex. The visit // function will be invoked with the hash of the vertex currently visited. If it returns false, DFS // will continue traversing the graph, and if it returns true, the traversal will be stopped. In // case the graph is disconnected, only the vertices joined with the starting vertex are visited. // // This example prints all vertices of the graph in DFS-order: // // g := graph.New(graph.IntHash) // // _ = g.AddVertex(1) // _ = g.AddVertex(2) // _ = g.AddVertex(3) // // _ = g.AddEdge(1, 2) // _ = g.AddEdge(2, 3) // _ = g.AddEdge(3, 1) // // _ = graph.DFS(g, 1, func(value int) bool { // fmt.Println(value) // return false // }) // // Similarly, if you have a graph of City vertices and the traversal should stop at London, the // visit function would look as follows: // // func(c City) bool { // return c.Name == "London" // } // // DFS is non-recursive and maintains a stack instead. func DFS[K comparable, T any](g Graph[K, T], start K, visit func(K) bool) error { adjacencyMap, err := g.AdjacencyMap() if err != nil { return fmt.Errorf("could not get adjacency map: %w", err) } if _, ok := adjacencyMap[start]; !ok { return fmt.Errorf("could not find start vertex with hash %v", start) } stack := make([]K, 0) visited := make(map[K]bool) stack = append(stack, start) for len(stack) > 0 { currentHash := stack[len(stack)-1] stack = stack[:len(stack)-1] if _, ok := visited[currentHash]; !ok { // Stop traversing the graph if the visit function returns true. if stop := visit(currentHash); stop { break } visited[currentHash] = true for adjacency := range adjacencyMap[currentHash] { stack = append(stack, adjacency) } } } return nil } // BFS performs a breadth-first search on the graph, starting from the given vertex. The visit // function will be invoked with the hash of the vertex currently visited. If it returns false, BFS // will continue traversing the graph, and if it returns true, the traversal will be stopped. In // case the graph is disconnected, only the vertices joined with the starting vertex are visited. // // This example prints all vertices of the graph in BFS-order: // // g := graph.New(graph.IntHash) // // _ = g.AddVertex(1) // _ = g.AddVertex(2) // _ = g.AddVertex(3) // // _ = g.AddEdge(1, 2) // _ = g.AddEdge(2, 3) // _ = g.AddEdge(3, 1) // // _ = graph.BFS(g, 1, func(value int) bool { // fmt.Println(value) // return false // }) // // Similarly, if you have a graph of City vertices and the traversal should stop at London, the // visit function would look as follows: // // func(c City) bool { // return c.Name == "London" // } // // BFS is non-recursive and maintains a stack instead. func BFS[K comparable, T any](g Graph[K, T], start K, visit func(K) bool) error { ignoreDepth := func(vertex K, _ int) bool { return visit(vertex) } return BFSWithDepth(g, start, ignoreDepth) } // BFSWithDepth works just as BFS and performs a breadth-first search on the graph, but its // visit function is passed the current depth level as a second argument. Consequently, the // current depth can be used for deciding whether or not to proceed past a certain depth. // // _ = graph.BFSWithDepth(g, 1, func(value int, depth int) bool { // fmt.Println(value) // return depth > 3 // }) // // With the visit function from the example, the BFS traversal will stop once a depth greater // than 3 is reached. func BFSWithDepth[K comparable, T any](g Graph[K, T], start K, visit func(K, int) bool) error { adjacencyMap, err := g.AdjacencyMap() if err != nil { return fmt.Errorf("could not get adjacency map: %w", err) } if _, ok := adjacencyMap[start]; !ok { return fmt.Errorf("could not find start vertex with hash %v", start) } queue := make([]K, 0) visited := make(map[K]bool) visited[start] = true queue = append(queue, start) depth := 0 for len(queue) > 0 { currentHash := queue[0] queue = queue[1:] depth++ // Stop traversing the graph if the visit function returns true. if stop := visit(currentHash, depth); stop { break } for adjacency := range adjacencyMap[currentHash] { if _, ok := visited[adjacency]; !ok { visited[adjacency] = true queue = append(queue, adjacency) } } } return nil } graph-0.23.0/traversal_test.go000066400000000000000000000237771445126167600163220ustar00rootroot00000000000000package graph import ( "log" "testing" ) func TestDirectedDFS(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] startHash int expectedVisits []int stopAtVertex int }{ "traverse entire directed graph with 3 vertices": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, }, startHash: 1, expectedVisits: []int{1, 2, 3}, stopAtVertex: -1, }, "traverse entire directed triangle graph": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, startHash: 1, expectedVisits: []int{1, 2, 3}, stopAtVertex: -1, }, "traverse directed graph with 3 vertices until vertex 2": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, startHash: 1, expectedVisits: []int{1, 2}, stopAtVertex: 2, }, "traverse a disconnected directed graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 3, Target: 4}, }, startHash: 1, expectedVisits: []int{1, 2}, stopAtVertex: -1, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } visited := make(map[int]struct{}) visit := func(value int) bool { visited[value] = struct{}{} if test.stopAtVertex != -1 { if value == test.stopAtVertex { return true } } return false } _ = DFS(graph, test.startHash, visit) if len(visited) != len(test.expectedVisits) { t.Fatalf("%s: numbers of visited vertices don't match: expected %v, got %v", name, len(test.expectedVisits), len(visited)) } for _, expectedVisit := range test.expectedVisits { if _, ok := visited[expectedVisit]; !ok { t.Errorf("%s: expected vertex %v to be visited, but it isn't", name, expectedVisit) } } } } func TestUndirectedDFS(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] startHash int // It is not possible to expect a strict list of vertices to be visited. // If stopAtVertex is a neighbor of another vertex, that other vertex // might be visited before stopAtVertex. expectedMinimumVisits []int // In case stopAtVertex has downstream neighbors, those neighbors must // not be visited. forbiddenVisits []int stopAtVertex int }{ "traverse entire undirected graph with 3 vertices": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, }, startHash: 1, expectedMinimumVisits: []int{1, 2, 3}, stopAtVertex: -1, }, "traverse entire undirected triangle graph": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, startHash: 1, expectedMinimumVisits: []int{1, 2, 3}, stopAtVertex: -1, }, "traverse undirected graph with 3 vertices until vertex 2": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, startHash: 1, expectedMinimumVisits: []int{1, 2}, stopAtVertex: 2, }, "traverse undirected graph with 7 vertices until vertex 4": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 4, Target: 6}, {Source: 5, Target: 7}, }, startHash: 1, expectedMinimumVisits: []int{1, 2, 4}, forbiddenVisits: []int{6}, stopAtVertex: 4, }, "traverse undirected graph with 15 vertices until vertex 11": { vertices: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 3, Target: 4}, {Source: 3, Target: 5}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 13}, {Source: 5, Target: 14}, {Source: 6, Target: 7}, {Source: 7, Target: 8}, {Source: 7, Target: 9}, {Source: 8, Target: 10}, {Source: 9, Target: 11}, {Source: 9, Target: 12}, {Source: 10, Target: 14}, {Source: 11, Target: 15}, }, startHash: 1, expectedMinimumVisits: []int{1, 3, 7, 9, 11}, forbiddenVisits: []int{15}, stopAtVertex: 11, }, "traverse a disconnected undirected graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 3, Target: 4}, }, startHash: 1, expectedMinimumVisits: []int{1, 2}, stopAtVertex: -1, }, } for name, test := range tests { graph := New(IntHash) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } visited := make(map[int]struct{}) visit := func(value int) bool { visited[value] = struct{}{} if test.stopAtVertex != -1 { if value == test.stopAtVertex { return true } } return false } _ = DFS(graph, test.startHash, visit) if len(visited) < len(test.expectedMinimumVisits) { t.Fatalf("%s: expected number of minimum visits doesn't match: expected %v, got %v", name, len(test.expectedMinimumVisits), len(visited)) } if test.forbiddenVisits != nil { for _, forbiddenVisit := range test.forbiddenVisits { if _, ok := visited[forbiddenVisit]; ok { t.Errorf("%s: expected vertex %v to not be visited, but it is", name, forbiddenVisit) } } } for _, expectedVisit := range test.expectedMinimumVisits { if _, ok := visited[expectedVisit]; !ok { t.Errorf("%s: expected vertex %v to be visited, but it isn't", name, expectedVisit) } } } } func TestDirectedBFS(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] startHash int expectedVisits []int stopAtVertex int }{ "traverse entire graph with 3 vertices": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, }, startHash: 1, expectedVisits: []int{1, 2, 3}, stopAtVertex: -1, }, "traverse graph with 6 vertices until vertex 4": { vertices: []int{1, 2, 3, 4, 5, 6}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 6}, }, startHash: 1, expectedVisits: []int{1, 2, 3, 4}, stopAtVertex: 4, }, "traverse a disconnected graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 3, Target: 4}, }, startHash: 1, expectedVisits: []int{1, 2}, stopAtVertex: -1, }, } for name, test := range tests { graph := New(IntHash, Directed()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } visited := make(map[int]struct{}) visit := func(value int) bool { visited[value] = struct{}{} if test.stopAtVertex != -1 { if value == test.stopAtVertex { return true } } return false } _ = BFS(graph, test.startHash, visit) for _, expectedVisit := range test.expectedVisits { if _, ok := visited[expectedVisit]; !ok { t.Errorf("%s: expected vertex %v to be visited, but it isn't", name, expectedVisit) } } visitWithDepth := func(value int, depth int) bool { visited[value] = struct{}{} log.Printf("cur depth: %d", depth) if test.stopAtVertex != -1 { if value == test.stopAtVertex { return true } } return false } _ = BFSWithDepth(graph, test.startHash, visitWithDepth) } } func TestUndirectedBFS(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] startHash int expectedVisits []int stopAtVertex int }{ "traverse entire graph with 3 vertices": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, }, startHash: 1, expectedVisits: []int{1, 2, 3}, stopAtVertex: -1, }, "traverse graph with 6 vertices until vertex 4": { vertices: []int{1, 2, 3, 4, 5, 6}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 6}, }, startHash: 1, expectedVisits: []int{1, 2, 3, 4}, stopAtVertex: 4, }, "traverse a disconnected graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 3, Target: 4}, }, startHash: 1, expectedVisits: []int{1, 2}, stopAtVertex: -1, }, } for name, test := range tests { graph := New(IntHash) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } visited := make(map[int]struct{}) visit := func(value int) bool { visited[value] = struct{}{} if test.stopAtVertex != -1 { if value == test.stopAtVertex { return true } } return false } _ = BFS(graph, test.startHash, visit) for _, expectedVisit := range test.expectedVisits { if _, ok := visited[expectedVisit]; !ok { t.Errorf("%s: expected vertex %v to be visited, but it isn't", name, expectedVisit) } } } } graph-0.23.0/trees.go000066400000000000000000000042741445126167600143710ustar00rootroot00000000000000package graph import ( "errors" "fmt" "sort" ) // MinimumSpanningTree returns a minimum spanning tree within the given graph. // // The MST contains all vertices from the given graph as well as the required // edges for building the MST. The original graph remains unchanged. func MinimumSpanningTree[K comparable, T any](g Graph[K, T]) (Graph[K, T], error) { return spanningTree(g, false) } // MaximumSpanningTree returns a minimum spanning tree within the given graph. // // The MST contains all vertices from the given graph as well as the required // edges for building the MST. The original graph remains unchanged. func MaximumSpanningTree[K comparable, T any](g Graph[K, T]) (Graph[K, T], error) { return spanningTree(g, true) } func spanningTree[K comparable, T any](g Graph[K, T], maximum bool) (Graph[K, T], error) { if g.Traits().IsDirected { return nil, errors.New("spanning trees can only be determined for undirected graphs") } adjacencyMap, err := g.AdjacencyMap() if err != nil { return nil, fmt.Errorf("failed to get adjacency map: %w", err) } edges := make([]Edge[K], 0) subtrees := newUnionFind[K]() mst := NewLike(g) for v, adjacencies := range adjacencyMap { vertex, properties, err := g.VertexWithProperties(v) //nolint:govet if err != nil { return nil, fmt.Errorf("failed to get vertex %v: %w", v, err) } err = mst.AddVertex(vertex, copyVertexProperties(properties)) if err != nil { return nil, fmt.Errorf("failed to add vertex %v: %w", v, err) } subtrees.add(v) for _, edge := range adjacencies { edges = append(edges, edge) } } if maximum { sort.Slice(edges, func(i, j int) bool { return edges[i].Properties.Weight > edges[j].Properties.Weight }) } else { sort.Slice(edges, func(i, j int) bool { return edges[i].Properties.Weight < edges[j].Properties.Weight }) } for _, edge := range edges { sourceRoot := subtrees.find(edge.Source) targetRoot := subtrees.find(edge.Target) if sourceRoot != targetRoot { subtrees.union(sourceRoot, targetRoot) if err = mst.AddEdge(copyEdge(edge)); err != nil { return nil, fmt.Errorf("failed to add edge (%v, %v): %w", edge.Source, edge.Target, err) } } } return mst, nil } graph-0.23.0/trees_test.go000066400000000000000000000144201445126167600154220ustar00rootroot00000000000000package graph import ( "testing" ) func TestDirectedMinimumSpanningTree(t *testing.T) { tests := map[string]struct { shouldFail bool }{ "returns error": { shouldFail: true, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { graph := New(IntHash, Directed()) _, err := MinimumSpanningTree(graph) if test.shouldFail != (err != nil) { t.Errorf("expected error == %v, got %v", test.shouldFail, err) } }) } } func TestUndirectedMinimumSpanningTree(t *testing.T) { tests := map[string]struct { vertices []string edges []Edge[string] expectedErr error expectedMSTAdjacencyMap map[string]map[string]Edge[string] }{ "graph from img/mst.svg": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "A", Target: "D", Properties: EdgeProperties{Weight: 3}}, {Source: "B", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 1}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 3}}, }, expectedErr: nil, expectedMSTAdjacencyMap: map[string]map[string]Edge[string]{ "A": { "B": {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, }, "B": { "D": {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 1}}, "A": {Source: "B", Target: "A", Properties: EdgeProperties{Weight: 2}}, }, "C": { "D": {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 3}}, }, "D": { "B": {Source: "D", Target: "B", Properties: EdgeProperties{Weight: 1}}, "C": {Source: "D", Target: "C", Properties: EdgeProperties{Weight: 3}}, }, }, }, "two trees for a disconnected graph": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 4}}, }, expectedErr: nil, expectedMSTAdjacencyMap: map[string]map[string]Edge[string]{ "A": { "B": {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, }, "B": { "A": {Source: "B", Target: "A", Properties: EdgeProperties{Weight: 2}}, }, "C": { "D": {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 4}}, }, "D": { "C": {Source: "D", Target: "C", Properties: EdgeProperties{Weight: 4}}, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { g := New(StringHash) for _, vertex := range test.vertices { _ = g.AddVertex(vertex) } for _, edge := range test.edges { _ = g.AddEdge(copyEdge(edge)) } mst, _ := MinimumSpanningTree(g) adjacencyMap, _ := mst.AdjacencyMap() edgesAreEqual := g.(*undirected[string, string]).edgesAreEqual if !adjacencyMapsAreEqual(test.expectedMSTAdjacencyMap, adjacencyMap, edgesAreEqual) { t.Fatalf("expected adjacency map %v, got %v", test.expectedMSTAdjacencyMap, adjacencyMap) } }) } } func TestDirectedMaximumSpanningTree(t *testing.T) { tests := map[string]struct { shouldFail bool }{ "returns error": { shouldFail: true, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { graph := New(IntHash, Directed()) _, err := MaximumSpanningTree(graph) if test.shouldFail != (err != nil) { t.Errorf("expected error == %v, got %v", test.shouldFail, err) } }) } } func TestUndirectedMaximumSpanningTree(t *testing.T) { tests := map[string]struct { vertices []string edges []Edge[string] expectedErr error expectedMSTAdjacencyMap map[string]map[string]Edge[string] }{ "graph from img/mst.svg with higher weights": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 20}}, {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "A", Target: "D", Properties: EdgeProperties{Weight: 3}}, {Source: "B", Target: "C", Properties: EdgeProperties{Weight: 4}}, {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 10}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 30}}, }, expectedErr: nil, expectedMSTAdjacencyMap: map[string]map[string]Edge[string]{ "A": { "B": {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 20}}, }, "B": { "D": {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 10}}, "A": {Source: "B", Target: "A", Properties: EdgeProperties{Weight: 20}}, }, "C": { "D": {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 30}}, }, "D": { "B": {Source: "D", Target: "B", Properties: EdgeProperties{Weight: 10}}, "C": {Source: "D", Target: "C", Properties: EdgeProperties{Weight: 30}}, }, }, }, "two trees for a disconnected graph": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 4}}, }, expectedErr: nil, expectedMSTAdjacencyMap: map[string]map[string]Edge[string]{ "A": { "B": {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, }, "B": { "A": {Source: "B", Target: "A", Properties: EdgeProperties{Weight: 2}}, }, "C": { "D": {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 4}}, }, "D": { "C": {Source: "D", Target: "C", Properties: EdgeProperties{Weight: 4}}, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { g := New(StringHash) for _, vertex := range test.vertices { _ = g.AddVertex(vertex) } for _, edge := range test.edges { _ = g.AddEdge(copyEdge(edge)) } mst, _ := MaximumSpanningTree(g) adjacencyMap, _ := mst.AdjacencyMap() edgesAreEqual := g.(*undirected[string, string]).edgesAreEqual if !adjacencyMapsAreEqual(test.expectedMSTAdjacencyMap, adjacencyMap, edgesAreEqual) { t.Fatalf("expected adjacency map %v, got %v", test.expectedMSTAdjacencyMap, adjacencyMap) } }) } } graph-0.23.0/undirected.go000066400000000000000000000213141445126167600153670ustar00rootroot00000000000000package graph import ( "errors" "fmt" ) type undirected[K comparable, T any] struct { hash Hash[K, T] traits *Traits store Store[K, T] } func newUndirected[K comparable, T any](hash Hash[K, T], traits *Traits, store Store[K, T]) *undirected[K, T] { return &undirected[K, T]{ hash: hash, traits: traits, store: store, } } func (u *undirected[K, T]) Traits() *Traits { return u.traits } func (u *undirected[K, T]) AddVertex(value T, options ...func(*VertexProperties)) error { hash := u.hash(value) prop := VertexProperties{ Weight: 0, Attributes: make(map[string]string), } for _, option := range options { option(&prop) } return u.store.AddVertex(hash, value, prop) } func (u *undirected[K, T]) Vertex(hash K) (T, error) { vertex, _, err := u.store.Vertex(hash) return vertex, err } func (u *undirected[K, T]) VertexWithProperties(hash K) (T, VertexProperties, error) { vertex, prop, err := u.store.Vertex(hash) if err != nil { return vertex, VertexProperties{}, err } return vertex, prop, nil } func (u *undirected[K, T]) RemoveVertex(hash K) error { return u.store.RemoveVertex(hash) } func (u *undirected[K, T]) AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error { if _, _, err := u.store.Vertex(sourceHash); err != nil { return fmt.Errorf("could not find source vertex with hash %v: %w", sourceHash, err) } if _, _, err := u.store.Vertex(targetHash); err != nil { return fmt.Errorf("could not find target vertex with hash %v: %w", targetHash, err) } //nolint:govet // False positive. if _, err := u.Edge(sourceHash, targetHash); !errors.Is(err, ErrEdgeNotFound) { return ErrEdgeAlreadyExists } // If the user opted in to preventing cycles, run a cycle check. if u.traits.PreventCycles { createsCycle, err := CreatesCycle[K, T](u, sourceHash, targetHash) if err != nil { return fmt.Errorf("check for cycles: %w", err) } if createsCycle { return ErrEdgeCreatesCycle } } edge := Edge[K]{ Source: sourceHash, Target: targetHash, Properties: EdgeProperties{ Attributes: make(map[string]string), }, } for _, option := range options { option(&edge.Properties) } if err := u.addEdge(sourceHash, targetHash, edge); err != nil { return fmt.Errorf("failed to add edge: %w", err) } return nil } func (u *undirected[K, T]) AddEdgesFrom(g Graph[K, T]) error { edges, err := g.Edges() if err != nil { return fmt.Errorf("failed to get edges: %w", err) } for _, edge := range edges { if err := u.AddEdge(copyEdge(edge)); err != nil { return fmt.Errorf("failed to add (%v, %v): %w", edge.Source, edge.Target, err) } } return nil } func (u *undirected[K, T]) AddVerticesFrom(g Graph[K, T]) error { adjacencyMap, err := g.AdjacencyMap() if err != nil { return fmt.Errorf("failed to get adjacency map: %w", err) } for hash := range adjacencyMap { vertex, properties, err := g.VertexWithProperties(hash) if err != nil { return fmt.Errorf("failed to get vertex %v: %w", hash, err) } if err = u.AddVertex(vertex, copyVertexProperties(properties)); err != nil { return fmt.Errorf("failed to add vertex %v: %w", hash, err) } } return nil } func (u *undirected[K, T]) Edge(sourceHash, targetHash K) (Edge[T], error) { // In an undirected graph, since multigraphs aren't supported, the edge AB // is the same as BA. Therefore, if source[target] cannot be found, this // function also looks for target[source]. edge, err := u.store.Edge(sourceHash, targetHash) if errors.Is(err, ErrEdgeNotFound) { edge, err = u.store.Edge(targetHash, sourceHash) } if err != nil { return Edge[T]{}, err } sourceVertex, _, err := u.store.Vertex(sourceHash) if err != nil { return Edge[T]{}, err } targetVertex, _, err := u.store.Vertex(targetHash) if err != nil { return Edge[T]{}, err } return Edge[T]{ Source: sourceVertex, Target: targetVertex, Properties: EdgeProperties{ Weight: edge.Properties.Weight, Attributes: edge.Properties.Attributes, Data: edge.Properties.Data, }, }, nil } type tuple[K comparable] struct { source, target K } func (u *undirected[K, T]) Edges() ([]Edge[K], error) { storedEdges, err := u.store.ListEdges() if err != nil { return nil, fmt.Errorf("failed to get edges: %w", err) } // An undirected graph creates each edge twice internally: The edge (A,B) is // stored both as (A,B) and (B,A). The Edges method is supposed to return // one of these two edges, because from an outside perspective, it only is // a single edge. // // To achieve this, Edges keeps track of already-added edges. For each edge, // it also checks if the reversed edge has already been added - e.g., for // an edge (A,B), Edges checks if the edge has been added as (B,A). // // These reversed edges are built as a custom tuple type, which is then used // as a map key for access in O(1) time. It looks scarier than it is. edges := make([]Edge[K], 0, len(storedEdges)/2) added := make(map[tuple[K]]struct{}) for _, storedEdge := range storedEdges { reversedEdge := tuple[K]{ source: storedEdge.Target, target: storedEdge.Source, } if _, ok := added[reversedEdge]; ok { continue } edges = append(edges, storedEdge) addedEdge := tuple[K]{ source: storedEdge.Source, target: storedEdge.Target, } added[addedEdge] = struct{}{} } return edges, nil } func (u *undirected[K, T]) UpdateEdge(source, target K, options ...func(properties *EdgeProperties)) error { existingEdge, err := u.store.Edge(source, target) if err != nil { return err } for _, option := range options { option(&existingEdge.Properties) } if err := u.store.UpdateEdge(source, target, existingEdge); err != nil { return err } reversedEdge := existingEdge reversedEdge.Source = existingEdge.Target reversedEdge.Target = existingEdge.Source return u.store.UpdateEdge(target, source, reversedEdge) } func (u *undirected[K, T]) RemoveEdge(source, target K) error { if _, err := u.Edge(source, target); err != nil { return err } if err := u.store.RemoveEdge(source, target); err != nil { return fmt.Errorf("failed to remove edge from %v to %v: %w", source, target, err) } if err := u.store.RemoveEdge(target, source); err != nil { return fmt.Errorf("failed to remove edge from %v to %v: %w", target, source, err) } return nil } func (u *undirected[K, T]) AdjacencyMap() (map[K]map[K]Edge[K], error) { vertices, err := u.store.ListVertices() if err != nil { return nil, fmt.Errorf("failed to list vertices: %w", err) } edges, err := u.store.ListEdges() if err != nil { return nil, fmt.Errorf("failed to list edges: %w", err) } m := make(map[K]map[K]Edge[K], len(vertices)) for _, vertex := range vertices { m[vertex] = make(map[K]Edge[K]) } for _, edge := range edges { m[edge.Source][edge.Target] = edge } return m, nil } func (u *undirected[K, T]) PredecessorMap() (map[K]map[K]Edge[K], error) { return u.AdjacencyMap() } func (u *undirected[K, T]) Clone() (Graph[K, T], error) { traits := &Traits{ IsDirected: u.traits.IsDirected, IsAcyclic: u.traits.IsAcyclic, IsWeighted: u.traits.IsWeighted, IsRooted: u.traits.IsRooted, } clone := &undirected[K, T]{ hash: u.hash, traits: traits, store: newMemoryStore[K, T](), } if err := clone.AddVerticesFrom(u); err != nil { return nil, fmt.Errorf("failed to add vertices: %w", err) } if err := clone.AddEdgesFrom(u); err != nil { return nil, fmt.Errorf("failed to add edges: %w", err) } return clone, nil } func (u *undirected[K, T]) Order() (int, error) { return u.store.VertexCount() } func (u *undirected[K, T]) Size() (int, error) { size := 0 outEdges, err := u.AdjacencyMap() if err != nil { return 0, fmt.Errorf("failed to get adjacency map: %w", err) } for _, outEdges := range outEdges { size += len(outEdges) } // Divide by 2 since every add edge operation on undirected graph is counted // twice. return size / 2, nil } func (u *undirected[K, T]) edgesAreEqual(a, b Edge[T]) bool { aSourceHash := u.hash(a.Source) aTargetHash := u.hash(a.Target) bSourceHash := u.hash(b.Source) bTargetHash := u.hash(b.Target) if aSourceHash == bSourceHash && aTargetHash == bTargetHash { return true } if !u.traits.IsDirected { return aSourceHash == bTargetHash && aTargetHash == bSourceHash } return false } func (u *undirected[K, T]) addEdge(sourceHash, targetHash K, edge Edge[K]) error { err := u.store.AddEdge(sourceHash, targetHash, edge) if err != nil { return err } rEdge := Edge[K]{ Source: edge.Target, Target: edge.Source, Properties: EdgeProperties{ Weight: edge.Properties.Weight, Attributes: edge.Properties.Attributes, Data: edge.Properties.Data, }, } err = u.store.AddEdge(targetHash, sourceHash, rEdge) if err != nil { return err } return nil } graph-0.23.0/undirected_test.go000066400000000000000000001061511445126167600164310ustar00rootroot00000000000000package graph import ( "errors" "testing" ) func TestUndirected_Traits(t *testing.T) { tests := map[string]struct { traits *Traits expected *Traits }{ "default traits": { traits: &Traits{}, expected: &Traits{}, }, "directed": { traits: &Traits{IsDirected: true}, expected: &Traits{IsDirected: true}, }, } for name, test := range tests { g := newUndirected(IntHash, test.traits, newMemoryStore[int, int]()) traits := g.Traits() if !traitsAreEqual(traits, test.expected) { t.Errorf("%s: traits expectancy doesn't match: expected %v, got %v", name, test.expected, traits) } } } func TestUndirected_AddVertex(t *testing.T) { tests := map[string]struct { vertices []int properties *VertexProperties expectedVertices []int expectedProperties *VertexProperties // Even though some AddVertex calls might work, at least one of them // could fail, e.g. if the last call would add an existing vertex. finallyExpectedError error }{ "graph with 3 vertices": { vertices: []int{1, 2, 3}, properties: &VertexProperties{ Attributes: map[string]string{"color": "red"}, Weight: 10, }, expectedVertices: []int{1, 2, 3}, expectedProperties: &VertexProperties{ Attributes: map[string]string{"color": "red"}, Weight: 10, }, }, "graph with duplicated vertex": { vertices: []int{1, 2, 2}, expectedVertices: []int{1, 2}, finallyExpectedError: ErrVertexAlreadyExists, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) var err error for _, vertex := range test.vertices { if test.properties == nil { err = graph.AddVertex(vertex) continue } // If there are vertex attributes, iterate over them and call the // VertexAttribute functional option for each entry. A vertex should // only have one attribute so that AddVertex is invoked once. for key, value := range test.properties.Attributes { err = graph.AddVertex(vertex, VertexWeight(test.properties.Weight), VertexAttribute(key, value)) } } if err != test.finallyExpectedError { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.finallyExpectedError, err) } graphStore := graph.store.(*memoryStore[int, int]) for _, vertex := range test.vertices { if len(graphStore.vertices) != len(test.expectedVertices) { t.Errorf("%s: vertex count doesn't match: expected %v, got %v", name, len(test.expectedVertices), len(graphStore.vertices)) } hash := graph.hash(vertex) vertices := graph.store.(*memoryStore[int, int]).vertices if _, ok := vertices[hash]; !ok { t.Errorf("%s: vertex %v not found in graph: %v", name, vertex, vertices) } if test.properties == nil { continue } if graphStore.vertexProperties[hash].Weight != test.expectedProperties.Weight { t.Errorf("%s: edge weights don't match: expected weight %v, got %v", name, test.expectedProperties.Weight, graphStore.vertexProperties[hash].Weight) } if len(graphStore.vertexProperties[hash].Attributes) != len(test.expectedProperties.Attributes) { t.Fatalf("%s: attributes lengths don't match: expcted %v, got %v", name, len(test.expectedProperties.Attributes), len(graphStore.vertexProperties[hash].Attributes)) } for expectedKey, expectedValue := range test.expectedProperties.Attributes { value, ok := graphStore.vertexProperties[hash].Attributes[expectedKey] if !ok { t.Errorf("%s: attribute keys don't match: expected key %v not found", name, expectedKey) } if value != expectedValue { t.Errorf("%s: attribute values don't match: expected value %v for key %v, got %v", name, expectedValue, expectedKey, value) } } } } } func TestUndirected_AddVerticesFrom(t *testing.T) { tests := map[string]struct { vertices []int properties map[int]VertexProperties existingVertices []int expectedVertices []int expectedProperties map[int]VertexProperties expectedError error }{ "graph with 3 vertices": { vertices: []int{1, 2, 3}, properties: map[int]VertexProperties{ 1: { Attributes: map[string]string{"color": "red"}, Weight: 10, }, 2: { Attributes: map[string]string{"color": "green"}, Weight: 20, }, 3: { Attributes: map[string]string{"color": "blue"}, Weight: 30, }, }, existingVertices: []int{}, expectedVertices: []int{1, 2, 3}, expectedProperties: map[int]VertexProperties{ 1: { Attributes: map[string]string{"color": "red"}, Weight: 10, }, 2: { Attributes: map[string]string{"color": "green"}, Weight: 20, }, 3: { Attributes: map[string]string{"color": "blue"}, Weight: 30, }, }, }, "graph with duplicated vertex": { vertices: []int{1, 2, 3}, properties: map[int]VertexProperties{}, existingVertices: []int{2}, expectedVertices: []int{1}, expectedProperties: map[int]VertexProperties{}, expectedError: ErrVertexAlreadyExists, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { source := New(IntHash) for _, vertex := range test.vertices { _ = source.AddVertex(vertex, copyVertexProperties(test.properties[vertex])) } g := New(IntHash) for _, vertex := range test.existingVertices { _ = g.AddVertex(vertex) } err := g.AddVerticesFrom(source) if !errors.Is(err, test.expectedError) { t.Errorf("expected error %v, got %v", test.expectedError, err) } if err != nil { return } for _, vertex := range test.expectedVertices { _, actualProperties, err := g.VertexWithProperties(vertex) if err != nil { t.Errorf("failed to get vertex %v with properties: %v", vertex, err.Error()) return } if expectedProperties, ok := test.expectedProperties[vertex]; ok { if !vertexPropertiesAreEqual(expectedProperties, actualProperties) { t.Errorf("expected properties %v for %v, got %v", expectedProperties, vertex, actualProperties) } } } }) } } func TestUndirected_Vertex(t *testing.T) { tests := map[string]struct { vertices []int vertex int expectedError error }{ "existing vertex": { vertices: []int{1, 2, 3}, vertex: 2, }, "non-existent vertex": { vertices: []int{1, 2, 3}, vertex: 4, expectedError: ErrVertexNotFound, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } vertex, err := graph.Vertex(test.vertex) if err != test.expectedError { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } if test.expectedError != nil { continue } if vertex != test.vertex { t.Errorf("%s: vertex expectancy doesn't match: expected %v, got %v", name, test.vertex, vertex) } } } func TestUndirected_AddEdge(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] traits *Traits expectedEdges []Edge[int] // Even though some AddVertex calls might work, at least one of them // could fail, e.g. if the last call would introduce a cycle. finallyExpectedError error }{ "graph with 2 edges": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2, Properties: EdgeProperties{Weight: 10}}, {Source: 1, Target: 3, Properties: EdgeProperties{Weight: 20}}, }, traits: &Traits{}, expectedEdges: []Edge[int]{ {Source: 1, Target: 2, Properties: EdgeProperties{Weight: 10}}, {Source: 1, Target: 3, Properties: EdgeProperties{Weight: 20}}, }, }, "hashes for non-existent vertices": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 3, Properties: EdgeProperties{Weight: 20}}, }, traits: &Traits{}, finallyExpectedError: ErrVertexNotFound, }, "edge introducing a cycle in an acyclic graph": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, }, traits: &Traits{ PreventCycles: true, }, finallyExpectedError: ErrEdgeCreatesCycle, }, "edge already exists": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, {Source: 3, Target: 1}, {Source: 3, Target: 1}, }, traits: &Traits{}, finallyExpectedError: ErrEdgeAlreadyExists, }, "edge with attributes": { vertices: []int{1, 2}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Attributes: map[string]string{ "color": "red", }, }, }, }, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Attributes: map[string]string{ "color": "red", }, }, }, }, traits: &Traits{}, }, "edge with data": { vertices: []int{1, 2}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Data: "foo", }, }, }, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Data: "foo", }, }, }, traits: &Traits{}, }, } for name, test := range tests { graph := newUndirected(IntHash, test.traits, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } var err error for _, edge := range test.edges { if len(edge.Properties.Attributes) == 0 { err = graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight), EdgeData(edge.Properties.Data)) } // If there are vertex attributes, iterate over them and call the // VertexAttribute functional option for each entry. A vertex should // only have one attribute so that AddVertex is invoked once. for key, value := range edge.Properties.Attributes { err = graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight), EdgeData(edge.Properties.Data), EdgeAttribute(key, value)) } if err != nil { break } } if !errors.Is(err, test.finallyExpectedError) { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v", name, test.finallyExpectedError, err) } for _, expectedEdge := range test.expectedEdges { sourceHash := graph.hash(expectedEdge.Source) targetHash := graph.hash(expectedEdge.Target) edge, ok := graph.store.(*memoryStore[int, int]).outEdges[sourceHash][targetHash] if !ok { t.Fatalf("%s: edge with source %v and target %v not found", name, expectedEdge.Source, expectedEdge.Target) } if !edgesAreEqual(expectedEdge, edge, false) { t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, edge) } } } } func TestUndirected_AddEdgesFrom(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] existingVertices []int existingEdges []Edge[int] expectedEdges []Edge[int] expectedError error }{ "graph with 3 edges": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, existingVertices: []int{1, 2, 3}, existingEdges: []Edge[int]{}, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, expectedError: nil, }, "edge with non-existing vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 3}, }, existingVertices: []int{1, 2}, existingEdges: []Edge[int]{}, expectedEdges: []Edge[int]{}, expectedError: ErrVertexNotFound, }, "graph with duplicated edge": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 2}, }, existingVertices: []int{1, 2}, existingEdges: []Edge[int]{ {Source: 1, Target: 2}, }, expectedEdges: []Edge[int]{}, expectedError: ErrEdgeAlreadyExists, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { source := New(IntHash) for _, vertex := range test.vertices { _ = source.AddVertex(vertex) } for _, edge := range test.edges { _ = source.AddEdge(copyEdge(edge)) } g := New(IntHash) for _, vertex := range test.existingVertices { _ = g.AddVertex(vertex) } for _, edge := range test.existingEdges { _ = g.AddEdge(copyEdge(edge)) } err := g.AddEdgesFrom(source) if !errors.Is(err, test.expectedError) { t.Fatalf("expected error %v, got %v", test.expectedError, err) } for _, edge := range test.expectedEdges { actualEdge, err := g.Edge(edge.Source, edge.Target) if err != nil { t.Fatalf("failed to get edge (%v, %v): %v", edge.Source, edge.Target, err.Error()) } if !edgesAreEqual(edge, actualEdge, false) { t.Errorf("%s: expected edge %v, got %v", name, edge, actualEdge) } } }) } } func TestUndirected_RemoveVertex(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] vertex int expectedError error }{ "existing disconnected vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 2, Target: 3}, }, vertex: 1, }, "existing connected vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 2, Target: 3}, }, vertex: 1, expectedError: ErrVertexHasEdges, }, "non-existent vertex": { vertices: []int{1, 2, 3}, edges: []Edge[int]{}, vertex: 4, expectedError: ErrVertexNotFound, }, } for name, test := range tests { graph := New(IntHash) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { _ = graph.AddEdge(edge.Source, edge.Target) } err := graph.RemoveVertex(test.vertex) if !errors.Is(err, test.expectedError) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } } } func TestUndirected_Edge(t *testing.T) { tests := map[string]struct { vertices []int edge Edge[int] args [2]int expectedError error }{ "get edge of undirected graph": { vertices: []int{1, 2, 3}, edge: Edge[int]{Source: 1, Target: 2}, args: [2]int{1, 2}, }, "get edge of undirected graph with swapped source and target": { vertices: []int{1, 2, 3}, edge: Edge[int]{Source: 1, Target: 2}, args: [2]int{2, 1}, }, "get non-existent edge of undirected graph": { vertices: []int{1, 2, 3}, edge: Edge[int]{Source: 1, Target: 2}, args: [2]int{2, 3}, expectedError: ErrEdgeNotFound, }, "get edge with properties": { vertices: []int{1, 2, 3}, edge: Edge[int]{ Source: 1, Target: 2, Properties: EdgeProperties{ // Attributes can't be tested at the moment, because there // is no way to add multiple attributes at once (using a // functional option like EdgeAttributes). // ToDo: Add Attributes once EdgeAttributes exists. Attributes: map[string]string{}, Weight: 10, Data: "this is an edge", }, }, args: [2]int{1, 2}, }, } for name, test := range tests { graph := New(IntHash) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } if err := graph.AddEdge(test.edge.Source, test.edge.Target, EdgeWeight(test.edge.Properties.Weight), EdgeData(test.edge.Properties.Data)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } edge, err := graph.Edge(test.args[0], test.args[1]) if !errors.Is(err, test.expectedError) { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } if test.expectedError != nil { continue } if edge.Source != test.args[0] { t.Errorf("%s: source expectancy doesn't match: expected %v, got %v", name, test.args[0], edge.Source) } if edge.Target != test.args[1] { t.Errorf("%s: target expectancy doesn't match: expected %v, got %v", name, test.args[1], edge.Target) } if !edgesAreEqual(test.edge, edge, false) { t.Errorf("%s: expected edge %v, got %v", name, test.edge, edge) } } } func TestUndirected_Edges(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expectedEdges []Edge[int] }{ "graph with 3 edges": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, expectedEdges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, }, }, { Source: 2, Target: 3, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "green", }, }, }, { Source: 3, Target: 1, Properties: EdgeProperties{ Weight: 30, Attributes: map[string]string{ "color": "blue", }, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { g := New(IntHash) for _, vertex := range test.vertices { _ = g.AddVertex(vertex) } for _, edge := range test.edges { _ = g.AddEdge(copyEdge(edge)) } edges, err := g.Edges() if err != nil { t.Fatalf("unexpected error: %v", err.Error()) } for _, expectedEdge := range test.expectedEdges { for _, actualEdge := range edges { if actualEdge.Source != expectedEdge.Source || actualEdge.Target != expectedEdge.Target { continue } if !edgesAreEqual(expectedEdge, actualEdge, false) { t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, actualEdge) } } } }) } } func TestUndirected_UpdateEdge(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] updateEdge Edge[int] expectedErr error }{ "update an edge": { vertices: []int{1, 2}, edges: []Edge[int]{ { Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 10, Attributes: map[string]string{ "color": "red", }, Data: "my-edge", }, }, }, updateEdge: Edge[int]{ Source: 1, Target: 2, Properties: EdgeProperties{ Weight: 20, Attributes: map[string]string{ "color": "blue", "label": "a blue edge", }, Data: "my-updated-edge", }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { g := New(IntHash) for _, vertex := range test.vertices { _ = g.AddVertex(vertex) } for _, edge := range test.edges { _ = g.AddEdge(copyEdge(edge)) } err := g.UpdateEdge(copyEdge(test.updateEdge)) if !errors.Is(err, test.expectedErr) { t.Fatalf("expected error %v, got %v", test.expectedErr, err) } actualEdge, err := g.Edge(test.updateEdge.Source, test.updateEdge.Target) if err != nil { t.Fatalf("unexpected error: %v", err.Error()) } if !edgesAreEqual(test.updateEdge, actualEdge, false) { t.Errorf("expected edge %v, got %v", test.updateEdge, actualEdge) } }) } } func TestUndirected_RemoveEdge(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] removeEdges []Edge[int] expectedError error }{ "two-vertices graph": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 2}, }, removeEdges: []Edge[int]{ {Source: 1, Target: 2}, }, }, "remove 2 edges from triangle": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 3}, }, removeEdges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, }, }, "remove non-existent edge": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, }, removeEdges: []Edge[int]{ {Source: 2, Target: 3}, }, expectedError: ErrEdgeNotFound, }, } for name, test := range tests { graph := New(IntHash) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } for _, removeEdge := range test.removeEdges { if err := graph.RemoveEdge(removeEdge.Source, removeEdge.Target); !errors.Is(err, test.expectedError) { t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err) } // After removing the edge, verify that it can't be retrieved using // Edge anymore. if _, err := graph.Edge(removeEdge.Source, removeEdge.Target); err != ErrEdgeNotFound { t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v", name, ErrEdgeNotFound, err) } } } } func TestUndirected_Adjacencies(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expected map[int]map[int]Edge[int] }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: { 3: {Source: 1, Target: 3}, }, 2: { 3: {Source: 2, Target: 3}, }, 3: { 1: {Source: 3, Target: 1}, 2: {Source: 3, Target: 2}, 4: {Source: 3, Target: 4}, }, 4: { 3: {Source: 4, Target: 3}, }, }, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: { 2: {Source: 1, Target: 2}, 3: {Source: 1, Target: 3}, }, 2: { 1: {Source: 2, Target: 1}, 4: {Source: 2, Target: 4}, }, 3: { 1: {Source: 3, Target: 1}, 4: {Source: 3, Target: 4}, }, 4: { 2: {Source: 4, Target: 2}, 3: {Source: 4, Target: 3}, }, }, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } adjacencyMap, _ := graph.AdjacencyMap() for expectedVertex, expectedAdjacencies := range test.expected { adjacencies, ok := adjacencyMap[expectedVertex] if !ok { t.Errorf("%s: expected vertex %v does not exist in adjacency map", name, expectedVertex) } for expectedAdjacency, expectedEdge := range expectedAdjacencies { edge, ok := adjacencies[expectedAdjacency] if !ok { t.Errorf("%s: expected adjacency %v does not exist in map of %v", name, expectedAdjacency, expectedVertex) } if edge.Source != expectedEdge.Source || edge.Target != expectedEdge.Target { t.Errorf("%s: edge expectancy doesn't match: expected %v, got %v", name, expectedEdge, edge) } } } } } func TestUndirected_PredecessorMap(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expected map[int]map[int]Edge[int] }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: { 3: {Source: 1, Target: 3}, }, 2: { 3: {Source: 2, Target: 3}, }, 3: { 1: {Source: 3, Target: 1}, 2: {Source: 3, Target: 2}, 4: {Source: 3, Target: 4}, }, 4: { 3: {Source: 4, Target: 3}, }, }, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, expected: map[int]map[int]Edge[int]{ 1: { 2: {Source: 1, Target: 2}, 3: {Source: 1, Target: 3}, }, 2: { 1: {Source: 2, Target: 1}, 4: {Source: 2, Target: 4}, }, 3: { 1: {Source: 3, Target: 1}, 4: {Source: 3, Target: 4}, }, 4: { 2: {Source: 4, Target: 2}, 3: {Source: 4, Target: 3}, }, }, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } predecessors, _ := graph.PredecessorMap() for expectedVertex, expectedPredecessors := range test.expected { predecessors, ok := predecessors[expectedVertex] if !ok { t.Errorf("%s: expected vertex %v does not exist in adjacency map", name, expectedVertex) } for expectedPredecessor, expectedEdge := range expectedPredecessors { edge, ok := predecessors[expectedPredecessor] if !ok { t.Errorf("%s: expected adjacency %v does not exist in map of %v", name, expectedPredecessor, expectedVertex) } if edge.Source != expectedEdge.Source || edge.Target != expectedEdge.Target { t.Errorf("%s: edge expectancy doesn't match: expected %v, got %v", name, expectedEdge, edge) } } } } } func TestUndirected_Clone(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, }, } for name, test := range tests { graph := New(IntHash) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex, VertexWeight(vertex), VertexAttribute("color", "red")) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } clonedGraph, err := graph.Clone() if err != nil { t.Fatalf("%s: failed to clone graph: %s", name, err.Error()) } expected := graph.(*undirected[int, int]) actual := clonedGraph.(*undirected[int, int]) if actual.hash(5) != expected.hash(5) { t.Errorf("%s: hash expectancy doesn't match: expected %v, got %v", name, expected.hash, actual.hash) } if !traitsAreEqual(actual.traits, expected.traits) { t.Errorf("%s: traits expectancy doesn't match: expected %v, got %v", name, expected.traits, actual.traits) } expectedAdjacencyMap, _ := graph.AdjacencyMap() actualAdjacencyMap, _ := actual.AdjacencyMap() if !adjacencyMapsAreEqual(expectedAdjacencyMap, actualAdjacencyMap, expected.edgesAreEqual) { t.Errorf("%s: expected adjacency map %v, got %v", name, expectedAdjacencyMap, actualAdjacencyMap) } _ = clonedGraph.AddVertex(10) if _, err := graph.Vertex(10); err == nil { t.Errorf("%s: vertex 10 shouldn't exist in original graph", name) } } } func TestUndirected_OrderAndSize(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] expectedOrder int expectedSize int }{ "Y-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 3}, {Source: 2, Target: 3}, {Source: 3, Target: 4}, }, expectedOrder: 4, expectedSize: 3, }, "diamond-shaped graph": { vertices: []int{1, 2, 3, 4}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 3, Target: 4}, }, expectedOrder: 4, expectedSize: 4, }, "two-vertices graph": { vertices: []int{1, 2}, edges: []Edge[int]{ {Source: 1, Target: 2}, }, expectedOrder: 2, expectedSize: 1, }, "edgeless graph": { vertices: []int{1, 2}, edges: []Edge[int]{}, expectedOrder: 2, expectedSize: 0, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } order, _ := graph.Order() size, _ := graph.Size() if order != test.expectedOrder { t.Errorf("%s: order expectancy doesn't match: expected %d, got %d", name, test.expectedOrder, order) } if size != test.expectedSize { t.Errorf("%s: size expectancy doesn't match: expected %d, got %d", name, test.expectedSize, size) } } } func TestUndirected_edgesAreEqual(t *testing.T) { tests := map[string]struct { a Edge[int] b Edge[int] edgesAreEqual bool }{ "equal edges in undirected graph": { a: Edge[int]{Source: 1, Target: 2}, b: Edge[int]{Source: 1, Target: 2}, edgesAreEqual: true, }, "swapped equal edges in undirected graph": { a: Edge[int]{Source: 1, Target: 2}, b: Edge[int]{Source: 2, Target: 1}, edgesAreEqual: true, }, "unequal edges in undirected graph": { a: Edge[int]{Source: 1, Target: 2}, b: Edge[int]{Source: 1, Target: 3}, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) actual := graph.edgesAreEqual(test.a, test.b) if actual != test.edgesAreEqual { t.Errorf("%s: equality expectations don't match: expected %v, got %v", name, test.edgesAreEqual, actual) } } } func TestUndirected_addEdge(t *testing.T) { tests := map[string]struct { edges []Edge[int] }{ "add 3 edges": { edges: []Edge[int]{ {Source: 1, Target: 2, Properties: EdgeProperties{Weight: 1}}, {Source: 2, Target: 3, Properties: EdgeProperties{Weight: 2}}, {Source: 3, Target: 1, Properties: EdgeProperties{Weight: 3}}, }, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, edge := range test.edges { sourceHash := graph.hash(edge.Source) TargetHash := graph.hash(edge.Target) err := graph.addEdge(sourceHash, TargetHash, edge) if err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } outEdges := graph.store.(*memoryStore[int, int]).outEdges if len(outEdges) != len(test.edges) { t.Errorf("%s: number of outgoing edges doesn't match: expected %v, got %v", name, len(test.edges), len(outEdges)) } inEdges := graph.store.(*memoryStore[int, int]).inEdges if len(inEdges) != len(test.edges) { t.Errorf("%s: number of ingoing edges doesn't match: expected %v, got %v", name, len(test.edges), len(inEdges)) } } } func TestUndirected_adjacencies(t *testing.T) { tests := map[string]struct { vertices []int edges []Edge[int] vertex int expectedAdjancencies []int }{ "graph with 3 vertices": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, }, vertex: 2, expectedAdjancencies: []int{1}, }, "graph with 6 vertices": { vertices: []int{1, 2, 3, 4, 5, 6}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 6}, }, vertex: 2, expectedAdjancencies: []int{1, 4, 5}, }, "graph with 7 vertices and a diamond cycle (#1)": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 7}, }, vertex: 5, expectedAdjancencies: []int{2, 7}, }, "graph with 7 vertices and a diamond cycle (#2)": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 7}, }, vertex: 7, expectedAdjancencies: []int{4, 5}, }, "graph with 7 vertices and a diamond cycle (#3)": { vertices: []int{1, 2, 3, 4, 5, 6, 7}, edges: []Edge[int]{ {Source: 1, Target: 2}, {Source: 1, Target: 3}, {Source: 2, Target: 4}, {Source: 2, Target: 5}, {Source: 3, Target: 6}, {Source: 4, Target: 7}, {Source: 5, Target: 7}, }, vertex: 2, expectedAdjancencies: []int{1, 4, 5}, }, } for name, test := range tests { graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]()) for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) } for _, edge := range test.edges { if err := graph.AddEdge(edge.Source, edge.Target); err != nil { t.Fatalf("%s: failed to add edge: %s", name, err.Error()) } } adjacencyList := adjacencyList(graph.store, graph.hash(test.vertex)) if !slicesAreEqual(adjacencyList, test.expectedAdjancencies) { t.Errorf("%s: adjacencies don't match: expected %v, got %v", name, test.expectedAdjancencies, adjacencyList) } } } func adjacencyList[K comparable, T any](store Store[K, T], vertexHash K) []K { var adjacencyHashes []K // An undirected graph creates an undirected edge as two directed edges in // the opposite direction, so both the in-edges and the out-edges work here. inEdges, ok := store.(*memoryStore[K, T]).inEdges[vertexHash] if !ok { return adjacencyHashes } for hash := range inEdges { adjacencyHashes = append(adjacencyHashes, hash) } return adjacencyHashes }