philiprehberger-graph 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +42 -0
- data/lib/philiprehberger/graph/graph.rb +73 -0
- data/lib/philiprehberger/graph/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73596f486ec100249fa2df0e4a9ef6aa1bdcd5e37e16b9b587dd6802f3fc9584
|
|
4
|
+
data.tar.gz: 0ddc969e510fb5543d9b981d53f73c15b53e73066defb0b7226998c79ef7ec15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cffa4c6398bfa251b9b8f82704d883d36e4c2dc7f1296369ff073364b8a71d0256a8f75b056d78372a38ca99809e8a09c560d549da4b8b8efa38ccf35130811
|
|
7
|
+
data.tar.gz: 80ca432cc5c0b1b35c979ccfebb1e7409d9170687a99ae9f4336e5159ce72a8d063cd5c8f7cc4829a4e22d9bad34c8d329e4fb8e8d3a3a668876994455ac628d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Enumerator-style node iteration (`each_node`)
|
|
14
|
+
- Enumerator-style edge iteration (`each_edge`)
|
|
15
|
+
- Distance-only shortest path query (`shortest_path_distance`)
|
|
16
|
+
- Graph complement operation (`complement`)
|
|
17
|
+
|
|
10
18
|
## [0.3.0] - 2026-04-10
|
|
11
19
|
|
|
12
20
|
### Added
|
data/README.md
CHANGED
|
@@ -139,6 +139,44 @@ g.add_edge(:d, :c)
|
|
|
139
139
|
g.strongly_connected_components # => [[:d, :c], [:b, :a]]
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
### Iteration
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
g = Philiprehberger::Graph.new
|
|
146
|
+
g.add_edge(:a, :b)
|
|
147
|
+
g.add_edge(:a, :c)
|
|
148
|
+
|
|
149
|
+
g.each_node { |id| puts id }
|
|
150
|
+
g.each_edge { |e| puts "#{e[:from]} -> #{e[:to]}" }
|
|
151
|
+
|
|
152
|
+
# Enumerator chaining
|
|
153
|
+
high_degree = g.each_node.select { |id| g.degree(id) > 1 }
|
|
154
|
+
heavy_edges = g.each_edge.select { |e| e[:weight] > 5 }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Shortest Path Distance
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
g = Philiprehberger::Graph.new
|
|
161
|
+
g.add_edge(:a, :b, weight: 1)
|
|
162
|
+
g.add_edge(:b, :c, weight: 2)
|
|
163
|
+
g.add_edge(:a, :c, weight: 10)
|
|
164
|
+
|
|
165
|
+
g.shortest_path_distance(:a, :c) # => 3
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Graph Complement
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
g = Philiprehberger::Graph.new
|
|
172
|
+
g.add_edge(:a, :b)
|
|
173
|
+
g.add_edge(:b, :c)
|
|
174
|
+
|
|
175
|
+
c = g.complement
|
|
176
|
+
c.edge?(:a, :c) # => true
|
|
177
|
+
c.edge?(:a, :b) # => false
|
|
178
|
+
```
|
|
179
|
+
|
|
142
180
|
### Query Methods
|
|
143
181
|
|
|
144
182
|
```ruby
|
|
@@ -232,10 +270,13 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
|
|
|
232
270
|
| `#node_count` | Number of nodes |
|
|
233
271
|
| `#edge_count` | Number of edges |
|
|
234
272
|
| `#empty?` | Whether the graph has no nodes |
|
|
273
|
+
| `#each_node` | Iterate nodes (yields or returns Enumerator) |
|
|
274
|
+
| `#each_edge` | Iterate edges (yields or returns Enumerator) |
|
|
235
275
|
| `#path?(from, to)` | Check if a path exists |
|
|
236
276
|
| `#bfs(start)` | Breadth-first search |
|
|
237
277
|
| `#dfs(start)` | Depth-first search |
|
|
238
278
|
| `#shortest_path(from, to)` | Dijkstra's shortest path |
|
|
279
|
+
| `#shortest_path_distance(from, to)` | Shortest distance (numeric) |
|
|
239
280
|
| `#topological_sort` | Topological ordering (DAG only) |
|
|
240
281
|
| `#cycle?` | Check for cycles |
|
|
241
282
|
| `#connected_components` | Find connected components |
|
|
@@ -249,6 +290,7 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
|
|
|
249
290
|
| `#subgraph(nodes)` | Extract a subgraph |
|
|
250
291
|
| `#transpose` | Reverse all edges (directed) |
|
|
251
292
|
| `#density` | Graph density (0.0 to 1.0) |
|
|
293
|
+
| `#complement` | Graph complement (missing edges) |
|
|
252
294
|
| `#to_dot` | Serialize to DOT format |
|
|
253
295
|
| `#to_json` | Serialize to JSON |
|
|
254
296
|
| `.from_json(str)` | Deserialize from JSON |
|
|
@@ -17,6 +17,26 @@ module Philiprehberger
|
|
|
17
17
|
@directed
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Iterate over all node ids.
|
|
21
|
+
#
|
|
22
|
+
# @yield [Object] each node id
|
|
23
|
+
# @return [Enumerator] if no block given
|
|
24
|
+
def each_node(&)
|
|
25
|
+
return enum_for(:each_node) unless block_given?
|
|
26
|
+
|
|
27
|
+
@adjacency.each_key(&)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Iterate over all edges as {from:, to:, weight:} hashes.
|
|
31
|
+
#
|
|
32
|
+
# @yield [Hash] each edge
|
|
33
|
+
# @return [Enumerator] if no block given
|
|
34
|
+
def each_edge(&)
|
|
35
|
+
return enum_for(:each_edge) unless block_given?
|
|
36
|
+
|
|
37
|
+
edges.each(&)
|
|
38
|
+
end
|
|
39
|
+
|
|
20
40
|
# Add a node to the graph.
|
|
21
41
|
#
|
|
22
42
|
# @param id [Object] the node identifier
|
|
@@ -254,6 +274,36 @@ module Philiprehberger
|
|
|
254
274
|
nil
|
|
255
275
|
end
|
|
256
276
|
|
|
277
|
+
# Find the shortest distance between two nodes using Dijkstra's algorithm.
|
|
278
|
+
#
|
|
279
|
+
# @param from [Object] source node
|
|
280
|
+
# @param to [Object] destination node
|
|
281
|
+
# @return [Numeric, nil] the shortest distance, or nil if no path exists
|
|
282
|
+
def shortest_path_distance(from, to)
|
|
283
|
+
return nil unless @adjacency.key?(from) && @adjacency.key?(to)
|
|
284
|
+
return 0 if from == to
|
|
285
|
+
|
|
286
|
+
distances = Hash.new(Float::INFINITY)
|
|
287
|
+
distances[from] = 0
|
|
288
|
+
unvisited = @adjacency.keys.dup
|
|
289
|
+
|
|
290
|
+
until unvisited.empty?
|
|
291
|
+
current = unvisited.min_by { |n| distances[n] }
|
|
292
|
+
break if distances[current] == Float::INFINITY
|
|
293
|
+
|
|
294
|
+
unvisited.delete(current)
|
|
295
|
+
|
|
296
|
+
return distances[to] if current == to
|
|
297
|
+
|
|
298
|
+
(@adjacency[current] || []).each do |edge|
|
|
299
|
+
alt = distances[current] + edge[:weight]
|
|
300
|
+
distances[edge[:node]] = alt if alt < distances[edge[:node]]
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
257
307
|
# Topological sort (directed acyclic graphs only).
|
|
258
308
|
#
|
|
259
309
|
# @return [Array<Object>] nodes in topological order
|
|
@@ -365,6 +415,29 @@ module Philiprehberger
|
|
|
365
415
|
m / max_edges
|
|
366
416
|
end
|
|
367
417
|
|
|
418
|
+
# Return the complement graph.
|
|
419
|
+
# The complement contains edges between all pairs of nodes that are NOT connected
|
|
420
|
+
# in the original graph.
|
|
421
|
+
#
|
|
422
|
+
# @return [Graph] a new graph with complementary edges
|
|
423
|
+
def complement
|
|
424
|
+
g = self.class.new(directed: @directed)
|
|
425
|
+
@adjacency.each_key { |id| g.add_node(id) }
|
|
426
|
+
|
|
427
|
+
node_list = @adjacency.keys
|
|
428
|
+
node_list.each_with_index do |a, i|
|
|
429
|
+
targets = @directed ? node_list : node_list[(i + 1)..]
|
|
430
|
+
targets.each do |b|
|
|
431
|
+
next if a == b
|
|
432
|
+
next if edge?(a, b)
|
|
433
|
+
|
|
434
|
+
g.add_edge(a, b)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
g
|
|
439
|
+
end
|
|
440
|
+
|
|
368
441
|
# Find connected components (undirected) or weakly connected components (directed).
|
|
369
442
|
#
|
|
370
443
|
# @return [Array<Array<Object>>] arrays of node ids per component
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-graph
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Graph data structure supporting directed and undirected modes with adjacency
|
|
14
14
|
list storage. Includes BFS, DFS, Dijkstra shortest path, topological sort, cycle
|