Skip to main content

use_graph_path/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive path helpers.
3//!
4//! The crate keeps path handling explicit through a small `Path` wrapper and a
5//! breadth-first shortest-path helper for unweighted graphs.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_graph_path::{Path, is_valid_path, shortest_path_unweighted};
11//!
12//! let adjacency = vec![vec![1], vec![2], vec![]];
13//! let shortest = shortest_path_unweighted(&adjacency, 0, 2).unwrap().unwrap();
14//! let path = Path::new(vec![0, 1, 2]).unwrap();
15//!
16//! assert_eq!(shortest.nodes(), &[0, 1, 2]);
17//! assert!(is_valid_path(&adjacency, path.nodes()).unwrap());
18//! ```
19
20use std::collections::VecDeque;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Path {
24    nodes: Vec<usize>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum PathError {
29    EmptyPath,
30    InvalidStartNode,
31    InvalidTargetNode,
32    InvalidNodeIndex,
33    InvalidNeighborIndex,
34}
35
36impl Path {
37    pub fn new(nodes: Vec<usize>) -> Result<Self, PathError> {
38        if nodes.is_empty() {
39            Err(PathError::EmptyPath)
40        } else {
41            Ok(Self { nodes })
42        }
43    }
44
45    #[must_use]
46    pub fn nodes(&self) -> &[usize] {
47        self.nodes.as_slice()
48    }
49
50    #[must_use]
51    pub fn len(&self) -> usize {
52        self.nodes.len()
53    }
54
55    #[must_use]
56    pub fn is_empty(&self) -> bool {
57        self.nodes.is_empty()
58    }
59
60    #[must_use]
61    pub fn edge_count(&self) -> usize {
62        self.nodes.len().saturating_sub(1)
63    }
64
65    #[must_use]
66    pub fn starts_at(&self, node: usize) -> bool {
67        self.nodes.first().copied() == Some(node)
68    }
69
70    #[must_use]
71    pub fn ends_at(&self, node: usize) -> bool {
72        self.nodes.last().copied() == Some(node)
73    }
74}
75
76fn validate_node(adjacency: &[Vec<usize>], node: usize, error: PathError) -> Result<(), PathError> {
77    if node >= adjacency.len() {
78        Err(error)
79    } else {
80        Ok(())
81    }
82}
83
84fn reconstruct_path(parents: &[Option<usize>], start: usize, target: usize) -> Option<Path> {
85    let mut nodes = vec![target];
86    let mut current = target;
87
88    while current != start {
89        current = parents[current]?;
90        nodes.push(current);
91    }
92
93    nodes.reverse();
94    Path::new(nodes).ok()
95}
96
97pub fn shortest_path_unweighted(
98    adjacency: &[Vec<usize>],
99    start: usize,
100    target: usize,
101) -> Result<Option<Path>, PathError> {
102    validate_node(adjacency, start, PathError::InvalidStartNode)?;
103    validate_node(adjacency, target, PathError::InvalidTargetNode)?;
104
105    if start == target {
106        return Ok(Some(Path::new(vec![start])?));
107    }
108
109    let mut visited = vec![false; adjacency.len()];
110    let mut parents = vec![None; adjacency.len()];
111    let mut queue = VecDeque::new();
112
113    visited[start] = true;
114    queue.push_back(start);
115
116    while let Some(node) = queue.pop_front() {
117        for &neighbor in &adjacency[node] {
118            if neighbor >= adjacency.len() {
119                return Err(PathError::InvalidNeighborIndex);
120            }
121
122            if !visited[neighbor] {
123                visited[neighbor] = true;
124                parents[neighbor] = Some(node);
125
126                if neighbor == target {
127                    return Ok(reconstruct_path(&parents, start, target));
128                }
129
130                queue.push_back(neighbor);
131            }
132        }
133    }
134
135    Ok(None)
136}
137
138pub fn is_valid_path(adjacency: &[Vec<usize>], path: &[usize]) -> Result<bool, PathError> {
139    if path.is_empty() {
140        return Err(PathError::EmptyPath);
141    }
142
143    for &node in path {
144        validate_node(adjacency, node, PathError::InvalidNodeIndex)?;
145    }
146
147    Ok(path
148        .windows(2)
149        .all(|edge| adjacency[edge[0]].contains(&edge[1])))
150}
151
152#[cfg(test)]
153mod tests {
154    use super::{is_valid_path, shortest_path_unweighted, Path, PathError};
155
156    #[test]
157    fn constructs_paths_and_reports_basic_properties() {
158        let path = Path::new(vec![0, 1, 2]).unwrap();
159
160        assert_eq!(path.nodes(), &[0, 1, 2]);
161        assert_eq!(path.len(), 3);
162        assert!(!path.is_empty());
163        assert_eq!(path.edge_count(), 2);
164        assert!(path.starts_at(0));
165        assert!(path.ends_at(2));
166    }
167
168    #[test]
169    fn finds_shortest_unweighted_paths() {
170        let adjacency = vec![vec![1, 2], vec![3], vec![3], vec![]];
171        let path = shortest_path_unweighted(&adjacency, 0, 3).unwrap().unwrap();
172
173        assert_eq!(path.nodes(), &[0, 1, 3]);
174    }
175
176    #[test]
177    fn handles_unreachable_and_single_node_paths() {
178        let adjacency = vec![vec![1], vec![], vec![]];
179
180        assert_eq!(shortest_path_unweighted(&adjacency, 0, 2).unwrap(), None);
181        assert_eq!(
182            shortest_path_unweighted(&adjacency, 1, 1)
183                .unwrap()
184                .unwrap()
185                .nodes(),
186            &[1]
187        );
188    }
189
190    #[test]
191    fn validates_paths_against_adjacency() {
192        let adjacency = vec![vec![1], vec![2], vec![]];
193
194        assert!(is_valid_path(&adjacency, &[0, 1, 2]).unwrap());
195        assert!(!is_valid_path(&adjacency, &[0, 2]).unwrap());
196    }
197
198    #[test]
199    fn rejects_invalid_inputs() {
200        let adjacency = vec![vec![3], vec![]];
201
202        assert_eq!(Path::new(vec![]), Err(PathError::EmptyPath));
203        assert_eq!(
204            shortest_path_unweighted(&adjacency, 2, 0),
205            Err(PathError::InvalidStartNode)
206        );
207        assert_eq!(
208            shortest_path_unweighted(&adjacency, 0, 1),
209            Err(PathError::InvalidNeighborIndex)
210        );
211        assert_eq!(is_valid_path(&[vec![]], &[]), Err(PathError::EmptyPath));
212        assert_eq!(
213            is_valid_path(&[vec![]], &[1]),
214            Err(PathError::InvalidNodeIndex)
215        );
216    }
217}