hullabaloo 0.1.0

Backend-agnostic geometry construction utilities.
Documentation
use std::collections::VecDeque;

use crate::error::{HullabalooError, HullabalooResult};

/// Simple undirected graph represented by adjacency lists.
///
/// Vertices are 0-based indices.
#[derive(Debug, Clone)]
pub struct Graph {
    /// `adjacency[v]` is the list of neighbors of vertex `v`.
    pub adjacency: Vec<Vec<usize>>,
}

impl Graph {
    pub fn num_vertices(&self) -> usize {
        self.adjacency.len()
    }

    pub fn degree(&self, v: usize) -> usize {
        self.adjacency[v].len()
    }

    pub fn neighbors(&self, v: usize) -> &[usize] {
        &self.adjacency[v]
    }

    pub fn distance(&self, start: usize, goal: usize) -> HullabalooResult<usize> {
        if start >= self.num_vertices() || goal >= self.num_vertices() {
            return Err(HullabalooError::InvalidStructure {
                message: format!(
                    "graph indices out of range (start={start}, goal={goal}, size={})",
                    self.num_vertices()
                ),
            });
        }
        if start == goal {
            return Ok(0);
        }

        let dist = self.bfs(start, Some(goal))?;
        match dist[goal] {
            usize::MAX => Err(HullabalooError::InvalidStructure {
                message: "graph is disconnected: no path between requested vertices".to_string(),
            }),
            goal_dist => Ok(goal_dist),
        }
    }

    pub fn diameter(&self) -> HullabalooResult<usize> {
        let n = self.num_vertices();
        if n == 0 {
            return Ok(0);
        }

        let mut diameter = 0usize;
        for start in 0..n {
            let dist = self.bfs_from(start)?;
            if let Some(max_for_start) = dist.into_iter().max() {
                diameter = diameter.max(max_for_start);
            }
        }
        Ok(diameter)
    }

    fn bfs_from(&self, start: usize) -> HullabalooResult<Vec<usize>> {
        let dist = self.bfs(start, None)?;
        if dist.contains(&usize::MAX) {
            return Err(HullabalooError::InvalidStructure {
                message: "graph is disconnected".to_string(),
            });
        }
        Ok(dist)
    }

    fn bfs(&self, start: usize, stop_at: Option<usize>) -> HullabalooResult<Vec<usize>> {
        if start >= self.num_vertices() {
            return Err(HullabalooError::InvalidStructure {
                message: format!(
                    "graph start index out of range (start={start}, size={})",
                    self.num_vertices()
                ),
            });
        }

        let vertex_count = self.num_vertices();
        let mut dist = vec![usize::MAX; vertex_count];
        let mut queue = VecDeque::new();
        dist[start] = 0;
        queue.push_back(start);

        while let Some(v) = queue.pop_front() {
            let next_dist = dist[v] + 1;
            for &n in &self.adjacency[v] {
                if n >= vertex_count {
                    return Err(HullabalooError::InvalidStructure {
                        message: format!(
                            "graph adjacency references {n} but size is {vertex_count}"
                        ),
                    });
                }
                if dist[n] != usize::MAX {
                    continue;
                }
                dist[n] = next_dist;
                if Some(n) == stop_at {
                    return Ok(dist);
                }
                queue.push_back(n);
            }
        }

        Ok(dist)
    }
}

#[cfg(test)]
mod tests {
    use super::Graph;

    #[test]
    fn distances_work_on_cycle() {
        let graph = Graph {
            adjacency: vec![vec![1, 3], vec![0, 2], vec![1, 3], vec![0, 2]],
        };

        assert_eq!(graph.distance(0, 0).unwrap(), 0);
        assert_eq!(graph.distance(0, 1).unwrap(), 1);
        assert_eq!(graph.distance(0, 2).unwrap(), 2);
        assert_eq!(graph.distance(0, 3).unwrap(), 1);
        assert_eq!(graph.diameter().unwrap(), 2);
    }
}