use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct SerializableGraph {
pub nodes: HashMap<usize, String>,
pub edges: Vec<(usize, usize, Option<String>)>,
pub directed: bool,
pub name: Option<String>,
}
impl SerializableGraph {
#[allow(dead_code)]
pub fn new_directed() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
directed: true,
name: None,
}
}
#[allow(dead_code)]
pub fn new_undirected() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
directed: false,
name: None,
}
}
#[allow(dead_code)]
pub fn add_node(&mut self, id: usize, label: impl Into<String>) {
self.nodes.insert(id, label.into());
}
#[allow(dead_code)]
pub fn add_edge(&mut self, from: usize, to: usize, label: Option<String>) {
self.edges.push((from, to, label));
}
#[allow(dead_code)]
pub fn to_dot(&self) -> String {
let graph_type = if self.directed { "digraph" } else { "graph" };
let name = self.name.as_deref().unwrap_or("G");
let edge_op = if self.directed { "->" } else { "--" };
let mut out = String::new();
out.push_str(&format!("{graph_type} {name} {{\n"));
let mut node_ids: Vec<usize> = self.nodes.keys().copied().collect();
node_ids.sort_unstable();
for id in node_ids {
let label = &self.nodes[&id];
if label.is_empty() {
out.push_str(&format!(" {id};\n"));
} else {
out.push_str(&format!(" {id} [label=\"{label}\"];\n"));
}
}
for (from, to, label) in &self.edges {
if let Some(lbl) = label {
out.push_str(&format!(" {from} {edge_op} {to} [label=\"{lbl}\"];\n"));
} else {
out.push_str(&format!(" {from} {edge_op} {to};\n"));
}
}
out.push_str("}\n");
out
}
#[allow(dead_code)]
pub fn to_adjacency_list(&self) -> String {
let mut adj: HashMap<usize, Vec<usize>> = HashMap::new();
for &id in self.nodes.keys() {
adj.entry(id).or_default();
}
for &(from, to, _) in &self.edges {
adj.entry(from).or_default().push(to);
if !self.directed {
adj.entry(to).or_default().push(from);
}
}
let mut node_ids: Vec<usize> = adj.keys().copied().collect();
node_ids.sort_unstable();
let mut out = String::new();
for id in node_ids {
let mut neighbors = adj[&id].clone();
neighbors.sort_unstable();
let neighbor_str = neighbors
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(" ");
out.push_str(&format!("{id}: {neighbor_str}\n"));
}
out
}
#[allow(dead_code)]
pub fn to_edge_list(&self) -> String {
let mut out = String::new();
for &(from, to, ref label) in &self.edges {
if let Some(lbl) = label {
out.push_str(&format!("{from} {to} {lbl}\n"));
} else {
out.push_str(&format!("{from} {to}\n"));
}
}
out
}
#[allow(dead_code)]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[allow(dead_code)]
pub fn edge_count(&self) -> usize {
self.edges.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_simple_graph() -> SerializableGraph {
let mut g = SerializableGraph::new_directed();
g.add_node(0, "A");
g.add_node(1, "B");
g.add_node(2, "C");
g.add_edge(0, 1, None);
g.add_edge(1, 2, Some("edge".to_string()));
g
}
#[test]
fn test_node_count() {
let g = make_simple_graph();
assert_eq!(g.node_count(), 3);
}
#[test]
fn test_edge_count() {
let g = make_simple_graph();
assert_eq!(g.edge_count(), 2);
}
#[test]
fn test_dot_contains_digraph() {
let g = make_simple_graph();
let dot = g.to_dot();
assert!(dot.contains("digraph"));
}
#[test]
fn test_dot_contains_nodes() {
let g = make_simple_graph();
let dot = g.to_dot();
assert!(dot.contains("label=\"A\""));
assert!(dot.contains("label=\"B\""));
}
#[test]
fn test_dot_contains_edge_op() {
let g = make_simple_graph();
let dot = g.to_dot();
assert!(dot.contains("->"));
}
#[test]
fn test_dot_undirected_uses_double_dash() {
let mut g = SerializableGraph::new_undirected();
g.add_node(0, "X");
g.add_node(1, "Y");
g.add_edge(0, 1, None);
let dot = g.to_dot();
assert!(dot.contains("--"));
assert!(!dot.contains("->"));
}
#[test]
fn test_dot_labeled_edge() {
let g = make_simple_graph();
let dot = g.to_dot();
assert!(dot.contains("label=\"edge\""));
}
#[test]
fn test_adjacency_list_contains_nodes() {
let g = make_simple_graph();
let al = g.to_adjacency_list();
assert!(al.contains("0:"));
assert!(al.contains("1:"));
assert!(al.contains("2:"));
}
#[test]
fn test_adjacency_list_has_edges() {
let g = make_simple_graph();
let al = g.to_adjacency_list();
assert!(al.contains("0: 1"));
}
#[test]
fn test_edge_list_format() {
let g = make_simple_graph();
let el = g.to_edge_list();
assert!(el.contains("0 1"));
assert!(el.contains("1 2 edge"));
}
#[test]
fn test_empty_graph_dot() {
let g = SerializableGraph::new_directed();
let dot = g.to_dot();
assert!(dot.contains("digraph G {"));
assert!(dot.contains("}"));
}
#[test]
fn test_graph_with_name() {
let mut g = SerializableGraph::new_directed();
g.name = Some("MyGraph".to_string());
let dot = g.to_dot();
assert!(dot.contains("digraph MyGraph {"));
}
#[test]
fn test_adjacency_list_undirected_bidirectional() {
let mut g = SerializableGraph::new_undirected();
g.add_node(0, "A");
g.add_node(1, "B");
g.add_edge(0, 1, None);
let al = g.to_adjacency_list();
assert!(al.contains("0: 1"));
assert!(al.contains("1: 0"));
}
}