1#![forbid(unsafe_code)]
2#[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}