pax_global_header 0000666 0000000 0000000 00000000064 14451261676 0014526 g ustar 00root root 0000000 0000000 52 comment=c61dfea98c04bdb3e321ae0858a3057280585f04
graph-0.23.0/ 0000775 0000000 0000000 00000000000 14451261676 0012711 5 ustar 00root root 0000000 0000000 graph-0.23.0/.github/ 0000775 0000000 0000000 00000000000 14451261676 0014251 5 ustar 00root root 0000000 0000000 graph-0.23.0/.github/.gitkeep 0000664 0000000 0000000 00000000000 14451261676 0015670 0 ustar 00root root 0000000 0000000 graph-0.23.0/.github/FUNDING.yml 0000664 0000000 0000000 00000000027 14451261676 0016065 0 ustar 00root root 0000000 0000000 github: [dominikbraun]
graph-0.23.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14451261676 0016306 5 ustar 00root root 0000000 0000000 graph-0.23.0/.github/workflows/go.yml 0000664 0000000 0000000 00000001401 14451261676 0017432 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000425 14451261676 0014702 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000000416 14451261676 0015276 0 ustar 00root root 0000000 0000000 run:
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.md 0000664 0000000 0000000 00000026712 14451261676 0014532 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000026135 14451261676 0013725 0 ustar 00root root 0000000 0000000 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.md 0000664 0000000 0000000 00000023462 14451261676 0014177 0 ustar 00root root 0000000 0000000 [中文版](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

```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

```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

```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.

```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

```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

```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

```go
g := graph.New(graph.StringHash, graph.Weighted())
// Add vertices and edges ...
mst, _ := graph.MinimumSpanningTree(g)
```
## Perform a topological sort

```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

```go
g := graph.New(graph.StringHash, graph.Directed(), graph.PreventCycles())
// Add vertices and edges ...
transitiveReduction, _ := graph.TransitiveReduction(g)
```

## Prevent the creation of cycles

```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

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.md 0000664 0000000 0000000 00000022411 14451261676 0014550 0 ustar 00root root 0000000 0000000 [中文版](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图

```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有向无环图

```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)
```
## 创建边带权重的图

```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 顺序遍历并打印图中的所有顶点。

```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
```
## 查找强联通分量

```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]]
```
## 查找最短路径

```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]
```
## 查找生成树

```go
g := graph.New(graph.StringHash, graph.Weighted())
// Add vertices and edges ...
mst, _ := graph.MinimumSpanningTree(g)
```
## 执行拓扑排序

```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]
```
## 执行传递闭包削减

```go
g := graph.New(graph.StringHash, graph.Directed(), graph.PreventCycles())
// Add vertices and edges ...
transitiveReduction, _ := graph.TransitiveReduction(g)
```

## 禁止创建环路

```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"))
```
### 按照此文档绘制图

图使用以下程序进行渲染:
```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.go 0000664 0000000 0000000 00000006646 14451261676 0015407 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000017733 14451261676 0016445 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014115 14451261676 0013775 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000020027 14451261676 0015033 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000017135 14451261676 0015032 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000106033 14451261676 0016065 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14451261676 0013646 5 ustar 00root root 0000000 0000000 graph-0.23.0/draw/draw.go 0000664 0000000 0000000 00000007724 14451261676 0015144 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000026207 14451261676 0016200 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000000056 14451261676 0014020 0 ustar 00root root 0000000 0000000 module github.com/dominikbraun/graph
go 1.18
graph-0.23.0/graph.go 0000664 0000000 0000000 00000034406 14451261676 0014350 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000014617 14451261676 0015411 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14451261676 0013465 5 ustar 00root root 0000000 0000000 graph-0.23.0/img/banner.png 0000664 0000000 0000000 00000422362 14451261676 0015451 0 ustar 00root root 0000000 0000000 PNG
IHDR
Ԫ:
iCCPICC Profile HTS{CBKЛ@J-t%@P!kAE+\"
Q,,`]EDY.p;oΙ3_&3