philiprehberger-graph 0.3.0 → 0.5.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 +13 -0
- data/README.md +53 -0
- data/lib/philiprehberger/graph/graph.rb +84 -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: 178b5edb15123981edaaa93532c454677d40753f5ff3729fbe62026c0020f768
|
|
4
|
+
data.tar.gz: 3446356ba683b0077338dfa55dc052e36546c39d9606e67e0e37ad114368cdfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f24fbd86ab0237395a61f5b012091817c4b152b24af2797e7264b2e853177fba4502a234863f1c56c83ecd9adb4bd11f5f0af4d804fae3afdf820a574ae09530
|
|
7
|
+
data.tar.gz: 16ecb22d98f3a29f2db9d12ae2f08d7ca4e5b03abd0af0c6a348dc2706a6bd4b0bec4e966a05bf662166c857bd6f73c06842c0cd3610caeafeba8454d9e1bf77
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-04-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Graph#total_weight` returning the sum of edge weights across all edges (cheap baseline for MST/flow comparisons)
|
|
14
|
+
|
|
15
|
+
## [0.4.0] - 2026-04-17
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Enumerator-style node iteration (`each_node`)
|
|
19
|
+
- Enumerator-style edge iteration (`each_edge`)
|
|
20
|
+
- Distance-only shortest path query (`shortest_path_distance`)
|
|
21
|
+
- Graph complement operation (`complement`)
|
|
22
|
+
|
|
10
23
|
## [0.3.0] - 2026-04-10
|
|
11
24
|
|
|
12
25
|
### Added
|
data/README.md
CHANGED
|
@@ -139,6 +139,54 @@ 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
|
+
|
|
180
|
+
### Total Edge Weight
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
g = Philiprehberger::Graph.new
|
|
184
|
+
g.add_edge(:a, :b, weight: 3)
|
|
185
|
+
g.add_edge(:b, :c, weight: 5)
|
|
186
|
+
|
|
187
|
+
g.total_weight # => 8
|
|
188
|
+
```
|
|
189
|
+
|
|
142
190
|
### Query Methods
|
|
143
191
|
|
|
144
192
|
```ruby
|
|
@@ -231,11 +279,15 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
|
|
|
231
279
|
| `#edges` | All edges as hashes |
|
|
232
280
|
| `#node_count` | Number of nodes |
|
|
233
281
|
| `#edge_count` | Number of edges |
|
|
282
|
+
| `#total_weight` | Sum of edge weights across all edges |
|
|
234
283
|
| `#empty?` | Whether the graph has no nodes |
|
|
284
|
+
| `#each_node` | Iterate nodes (yields or returns Enumerator) |
|
|
285
|
+
| `#each_edge` | Iterate edges (yields or returns Enumerator) |
|
|
235
286
|
| `#path?(from, to)` | Check if a path exists |
|
|
236
287
|
| `#bfs(start)` | Breadth-first search |
|
|
237
288
|
| `#dfs(start)` | Depth-first search |
|
|
238
289
|
| `#shortest_path(from, to)` | Dijkstra's shortest path |
|
|
290
|
+
| `#shortest_path_distance(from, to)` | Shortest distance (numeric) |
|
|
239
291
|
| `#topological_sort` | Topological ordering (DAG only) |
|
|
240
292
|
| `#cycle?` | Check for cycles |
|
|
241
293
|
| `#connected_components` | Find connected components |
|
|
@@ -249,6 +301,7 @@ g2 = Philiprehberger::Graph::Graph.from_json(g.to_json)
|
|
|
249
301
|
| `#subgraph(nodes)` | Extract a subgraph |
|
|
250
302
|
| `#transpose` | Reverse all edges (directed) |
|
|
251
303
|
| `#density` | Graph density (0.0 to 1.0) |
|
|
304
|
+
| `#complement` | Graph complement (missing edges) |
|
|
252
305
|
| `#to_dot` | Serialize to DOT format |
|
|
253
306
|
| `#to_json` | Serialize to JSON |
|
|
254
307
|
| `.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
|
|
@@ -157,6 +177,17 @@ module Philiprehberger
|
|
|
157
177
|
edges.length
|
|
158
178
|
end
|
|
159
179
|
|
|
180
|
+
# Sum of weights across all edges. Returns 0 for an empty or edgeless graph.
|
|
181
|
+
#
|
|
182
|
+
# For undirected graphs each edge is counted once.
|
|
183
|
+
#
|
|
184
|
+
# @return [Numeric]
|
|
185
|
+
def total_weight
|
|
186
|
+
total = 0
|
|
187
|
+
each_edge { |edge| total += edge[:weight] }
|
|
188
|
+
total
|
|
189
|
+
end
|
|
190
|
+
|
|
160
191
|
# Whether the graph has no nodes.
|
|
161
192
|
#
|
|
162
193
|
# @return [Boolean]
|
|
@@ -254,6 +285,36 @@ module Philiprehberger
|
|
|
254
285
|
nil
|
|
255
286
|
end
|
|
256
287
|
|
|
288
|
+
# Find the shortest distance between two nodes using Dijkstra's algorithm.
|
|
289
|
+
#
|
|
290
|
+
# @param from [Object] source node
|
|
291
|
+
# @param to [Object] destination node
|
|
292
|
+
# @return [Numeric, nil] the shortest distance, or nil if no path exists
|
|
293
|
+
def shortest_path_distance(from, to)
|
|
294
|
+
return nil unless @adjacency.key?(from) && @adjacency.key?(to)
|
|
295
|
+
return 0 if from == to
|
|
296
|
+
|
|
297
|
+
distances = Hash.new(Float::INFINITY)
|
|
298
|
+
distances[from] = 0
|
|
299
|
+
unvisited = @adjacency.keys.dup
|
|
300
|
+
|
|
301
|
+
until unvisited.empty?
|
|
302
|
+
current = unvisited.min_by { |n| distances[n] }
|
|
303
|
+
break if distances[current] == Float::INFINITY
|
|
304
|
+
|
|
305
|
+
unvisited.delete(current)
|
|
306
|
+
|
|
307
|
+
return distances[to] if current == to
|
|
308
|
+
|
|
309
|
+
(@adjacency[current] || []).each do |edge|
|
|
310
|
+
alt = distances[current] + edge[:weight]
|
|
311
|
+
distances[edge[:node]] = alt if alt < distances[edge[:node]]
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
257
318
|
# Topological sort (directed acyclic graphs only).
|
|
258
319
|
#
|
|
259
320
|
# @return [Array<Object>] nodes in topological order
|
|
@@ -365,6 +426,29 @@ module Philiprehberger
|
|
|
365
426
|
m / max_edges
|
|
366
427
|
end
|
|
367
428
|
|
|
429
|
+
# Return the complement graph.
|
|
430
|
+
# The complement contains edges between all pairs of nodes that are NOT connected
|
|
431
|
+
# in the original graph.
|
|
432
|
+
#
|
|
433
|
+
# @return [Graph] a new graph with complementary edges
|
|
434
|
+
def complement
|
|
435
|
+
g = self.class.new(directed: @directed)
|
|
436
|
+
@adjacency.each_key { |id| g.add_node(id) }
|
|
437
|
+
|
|
438
|
+
node_list = @adjacency.keys
|
|
439
|
+
node_list.each_with_index do |a, i|
|
|
440
|
+
targets = @directed ? node_list : node_list[(i + 1)..]
|
|
441
|
+
targets.each do |b|
|
|
442
|
+
next if a == b
|
|
443
|
+
next if edge?(a, b)
|
|
444
|
+
|
|
445
|
+
g.add_edge(a, b)
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
g
|
|
450
|
+
end
|
|
451
|
+
|
|
368
452
|
# Find connected components (undirected) or weakly connected components (directed).
|
|
369
453
|
#
|
|
370
454
|
# @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.5.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-29 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
|