Skip to main content

use_weighted_graph/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive weighted graph helpers.
3//!
4//! The crate provides a small weighted-edge type plus adjacency builders and a
5//! few helpers for summarizing edge weights.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_weighted_graph::{WeightedEdge, build_weighted_directed_adjacency, path_weight};
11//!
12//! let edge = WeightedEdge::new(0, 1, 2.5).unwrap();
13//! let adjacency = build_weighted_directed_adjacency(2, &[edge]).unwrap();
14//!
15//! assert_eq!(adjacency, vec![vec![(1, 2.5)], vec![]]);
16//! assert_eq!(path_weight(&[edge]), Some(2.5));
17//! ```
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct WeightedEdge {
21    pub source: usize,
22    pub target: usize,
23    pub weight: f64,
24}
25
26pub type WeightedAdjacencyList = Vec<Vec<(usize, f64)>>;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum WeightedGraphError {
30    InvalidNodeIndex,
31    InvalidWeight,
32}
33
34impl WeightedEdge {
35    pub fn new(source: usize, target: usize, weight: f64) -> Result<Self, WeightedGraphError> {
36        if !weight.is_finite() {
37            return Err(WeightedGraphError::InvalidWeight);
38        }
39
40        Ok(Self {
41            source,
42            target,
43            weight,
44        })
45    }
46
47    #[must_use]
48    pub const fn reversed(&self) -> Self {
49        Self {
50            source: self.target,
51            target: self.source,
52            weight: self.weight,
53        }
54    }
55}
56
57fn validate_edge(node_count: usize, edge: WeightedEdge) -> Result<(), WeightedGraphError> {
58    if !edge.weight.is_finite() {
59        return Err(WeightedGraphError::InvalidWeight);
60    }
61
62    if edge.source >= node_count || edge.target >= node_count {
63        Err(WeightedGraphError::InvalidNodeIndex)
64    } else {
65        Ok(())
66    }
67}
68
69pub fn build_weighted_directed_adjacency(
70    node_count: usize,
71    edges: &[WeightedEdge],
72) -> Result<WeightedAdjacencyList, WeightedGraphError> {
73    let mut adjacency = vec![Vec::new(); node_count];
74
75    for &edge in edges {
76        validate_edge(node_count, edge)?;
77        adjacency[edge.source].push((edge.target, edge.weight));
78    }
79
80    Ok(adjacency)
81}
82
83pub fn build_weighted_undirected_adjacency(
84    node_count: usize,
85    edges: &[WeightedEdge],
86) -> Result<WeightedAdjacencyList, WeightedGraphError> {
87    let mut adjacency = vec![Vec::new(); node_count];
88
89    for &edge in edges {
90        validate_edge(node_count, edge)?;
91        adjacency[edge.source].push((edge.target, edge.weight));
92
93        if edge.source != edge.target {
94            let reversed = edge.reversed();
95            adjacency[reversed.source].push((reversed.target, reversed.weight));
96        }
97    }
98
99    Ok(adjacency)
100}
101
102#[must_use]
103pub fn path_weight(edges: &[WeightedEdge]) -> Option<f64> {
104    if edges.is_empty() || edges.iter().any(|edge| !edge.weight.is_finite()) {
105        None
106    } else {
107        Some(edges.iter().map(|edge| edge.weight).sum())
108    }
109}
110
111#[must_use]
112pub fn min_weight_edge(edges: &[WeightedEdge]) -> Option<WeightedEdge> {
113    if edges.is_empty() || edges.iter().any(|edge| !edge.weight.is_finite()) {
114        None
115    } else {
116        edges
117            .iter()
118            .copied()
119            .min_by(|left, right| left.weight.total_cmp(&right.weight))
120    }
121}
122
123#[must_use]
124pub fn max_weight_edge(edges: &[WeightedEdge]) -> Option<WeightedEdge> {
125    if edges.is_empty() || edges.iter().any(|edge| !edge.weight.is_finite()) {
126        None
127    } else {
128        edges
129            .iter()
130            .copied()
131            .max_by(|left, right| left.weight.total_cmp(&right.weight))
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::{
138        build_weighted_directed_adjacency, build_weighted_undirected_adjacency, max_weight_edge,
139        min_weight_edge, path_weight, WeightedEdge, WeightedGraphError,
140    };
141
142    #[test]
143    fn constructs_weighted_edges() {
144        let edge = WeightedEdge::new(0, 1, 2.5).unwrap();
145
146        assert_eq!(edge.reversed(), WeightedEdge::new(1, 0, 2.5).unwrap());
147    }
148
149    #[test]
150    fn builds_weighted_adjacency_lists() {
151        let edges = [
152            WeightedEdge::new(0, 1, 2.0).unwrap(),
153            WeightedEdge::new(1, 2, 3.5).unwrap(),
154        ];
155
156        assert_eq!(
157            build_weighted_directed_adjacency(3, &edges).unwrap(),
158            vec![vec![(1, 2.0)], vec![(2, 3.5)], vec![]]
159        );
160        assert_eq!(
161            build_weighted_undirected_adjacency(3, &edges).unwrap(),
162            vec![vec![(1, 2.0)], vec![(0, 2.0), (2, 3.5)], vec![(1, 3.5)]]
163        );
164    }
165
166    #[test]
167    fn summarizes_weights() {
168        let edges = [
169            WeightedEdge::new(0, 1, 2.0).unwrap(),
170            WeightedEdge::new(1, 2, 3.5).unwrap(),
171            WeightedEdge::new(2, 3, 1.0).unwrap(),
172        ];
173
174        assert_eq!(path_weight(&edges), Some(6.5));
175        assert_eq!(
176            min_weight_edge(&edges),
177            Some(WeightedEdge::new(2, 3, 1.0).unwrap())
178        );
179        assert_eq!(
180            max_weight_edge(&edges),
181            Some(WeightedEdge::new(1, 2, 3.5).unwrap())
182        );
183    }
184
185    #[test]
186    fn rejects_invalid_weighted_inputs() {
187        assert_eq!(
188            WeightedEdge::new(0, 1, f64::NAN),
189            Err(WeightedGraphError::InvalidWeight)
190        );
191        assert_eq!(
192            build_weighted_directed_adjacency(
193                2,
194                &[WeightedEdge {
195                    source: 0,
196                    target: 2,
197                    weight: 1.0,
198                }]
199            ),
200            Err(WeightedGraphError::InvalidNodeIndex)
201        );
202    }
203
204    #[test]
205    fn handles_empty_weight_summaries() {
206        assert_eq!(path_weight(&[]), None);
207        assert_eq!(min_weight_edge(&[]), None);
208        assert_eq!(max_weight_edge(&[]), None);
209    }
210}