use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::str::FromStr;
use crate::base::{DiGraph, EdgeWeight, Graph, Node};
use crate::error::{GraphError, Result};
#[allow(dead_code)]
pub fn read_adjacency_list_format<N, E, P>(path: P, weighted: bool) -> Result<Graph<N, E>>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut graph = Graph::new();
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(colon_pos) = line.find(':') {
let node_str = line[..colon_pos].trim();
let neighbors_str = line[colon_pos + 1..].trim();
let source_node = match N::from_str(node_str) {
Ok(node) => node,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse node '{}' on line {}",
node_str,
line_num + 1
)));
}
};
if !neighbors_str.is_empty() {
let tokens: Vec<&str> = neighbors_str.split_whitespace().collect();
if weighted {
if !tokens.len().is_multiple_of(2) {
return Err(GraphError::Other(format!(
"Weighted adjacency list must have even number of tokens (neighbor weight pairs) on line {}",
line_num + 1
)));
}
for chunk in tokens.chunks(2) {
let neighbor_str = chunk[0];
let weight_str = chunk[1];
let neighbor_node = match N::from_str(neighbor_str) {
Ok(node) => node,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse neighbor '{}' on line {}",
neighbor_str,
line_num + 1
)));
}
};
let weight = match E::from_str(weight_str) {
Ok(w) => w,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse weight '{}' on line {}",
weight_str,
line_num + 1
)));
}
};
if !graph.has_edge(&source_node, &neighbor_node) {
graph.add_edge(source_node.clone(), neighbor_node, weight)?;
}
}
} else {
for neighbor_str in tokens {
let neighbor_node = match N::from_str(neighbor_str) {
Ok(node) => node,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse neighbor '{}' on line {}",
neighbor_str,
line_num + 1
)));
}
};
if !graph.has_edge(&source_node, &neighbor_node) {
graph.add_edge(source_node.clone(), neighbor_node, E::default())?;
}
}
}
}
} else {
return Err(GraphError::Other(format!(
"Missing ':' separator on line {}",
line_num + 1
)));
}
}
Ok(graph)
}
#[allow(dead_code)]
pub fn read_adjacency_list_format_digraph<N, E, P>(path: P, weighted: bool) -> Result<DiGraph<N, E>>
where
N: Node + std::fmt::Debug + FromStr + Clone,
E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut graph = DiGraph::new();
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(colon_pos) = line.find(':') {
let node_str = line[..colon_pos].trim();
let neighbors_str = line[colon_pos + 1..].trim();
let source_node = match N::from_str(node_str) {
Ok(node) => node,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse node '{}' on line {}",
node_str,
line_num + 1
)));
}
};
if !neighbors_str.is_empty() {
let tokens: Vec<&str> = neighbors_str.split_whitespace().collect();
if weighted {
if !tokens.len().is_multiple_of(2) {
return Err(GraphError::Other(format!(
"Weighted adjacency list must have even number of tokens (neighbor weight pairs) on line {}",
line_num + 1
)));
}
for chunk in tokens.chunks(2) {
let neighbor_str = chunk[0];
let weight_str = chunk[1];
let neighbor_node = match N::from_str(neighbor_str) {
Ok(node) => node,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse neighbor '{}' on line {}",
neighbor_str,
line_num + 1
)));
}
};
let weight = match E::from_str(weight_str) {
Ok(w) => w,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse weight '{}' on line {}",
weight_str,
line_num + 1
)));
}
};
graph.add_edge(source_node.clone(), neighbor_node, weight)?;
}
} else {
for neighbor_str in tokens {
let neighbor_node = match N::from_str(neighbor_str) {
Ok(node) => node,
Err(_) => {
return Err(GraphError::Other(format!(
"Failed to parse neighbor '{}' on line {}",
neighbor_str,
line_num + 1
)));
}
};
graph.add_edge(source_node.clone(), neighbor_node, E::default())?;
}
}
}
} else {
return Err(GraphError::Other(format!(
"Missing ':' separator on line {}",
line_num + 1
)));
}
}
Ok(graph)
}
#[allow(dead_code)]
pub fn write_adjacency_list_format<N, E, Ix, P>(
graph: &Graph<N, E, Ix>,
path: P,
weighted: bool,
) -> Result<()>
where
N: Node + std::fmt::Debug + std::fmt::Display + Clone,
E: EdgeWeight
+ std::marker::Copy
+ std::fmt::Debug
+ std::default::Default
+ std::fmt::Display
+ Clone,
Ix: petgraph::graph::IndexType,
P: AsRef<Path>,
{
let mut file = File::create(path)?;
writeln!(file, "# Adjacency list format")?;
writeln!(file, "# node: neighbor1 neighbor2 ...")?;
if weighted {
writeln!(file, "# Format: node: neighbor1 weight1 neighbor2 weight2")?;
}
writeln!(file)?;
let all_edges = graph.edges();
let all_nodes = graph.nodes();
let mut node_neighbors = std::collections::HashMap::new();
for edge in all_edges {
let source = &edge.source;
let target = &edge.target;
let weight = &edge.weight;
node_neighbors
.entry(source.clone())
.or_insert_with(Vec::new)
.push((target.clone(), *weight));
}
for node in all_nodes {
write!(file, "{node}: ")?;
if let Some(neighbors) = node_neighbors.get(node) {
let neighbor_strs: Vec<String> = neighbors
.iter()
.map(|(neighbor, weight)| {
if weighted {
format!("{neighbor} {weight}")
} else {
format!("{neighbor}")
}
})
.collect();
if !neighbor_strs.is_empty() {
writeln!(file, "{}", neighbor_strs.join(" "))?;
} else {
writeln!(file)?;
}
} else {
writeln!(file)?;
}
}
Ok(())
}
#[allow(dead_code)]
pub fn write_adjacency_list_format_digraph<N, E, Ix, P>(
graph: &DiGraph<N, E, Ix>,
path: P,
weighted: bool,
) -> Result<()>
where
N: Node + std::fmt::Debug + std::fmt::Display + Clone,
E: EdgeWeight
+ std::marker::Copy
+ std::fmt::Debug
+ std::default::Default
+ std::fmt::Display
+ Clone,
Ix: petgraph::graph::IndexType,
P: AsRef<Path>,
{
let mut file = File::create(path)?;
writeln!(file, "# Directed adjacency list format")?;
writeln!(file, "# node: neighbor1 neighbor2 ...")?;
if weighted {
writeln!(file, "# Format: node: neighbor1 weight1 neighbor2 weight2")?;
}
writeln!(file)?;
let all_edges = graph.edges();
let all_nodes = graph.nodes();
let mut node_neighbors = std::collections::HashMap::new();
for edge in all_edges {
let source = &edge.source;
let target = &edge.target;
let weight = &edge.weight;
node_neighbors
.entry(source.clone())
.or_insert_with(Vec::new)
.push((target.clone(), *weight));
}
for node in all_nodes {
write!(file, "{node}: ")?;
if let Some(neighbors) = node_neighbors.get(node) {
let neighbor_strs: Vec<String> = neighbors
.iter()
.map(|(neighbor, weight)| {
if weighted {
format!("{neighbor} {weight}")
} else {
format!("{neighbor}")
}
})
.collect();
if !neighbor_strs.is_empty() {
writeln!(file, "{}", neighbor_strs.join(" "))?;
} else {
writeln!(file)?;
}
} else {
writeln!(file)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_read_unweighted_adjacency_list() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(temp_file, "# Test adjacency list").expect("Operation failed");
writeln!(temp_file, "1: 2 3").expect("Operation failed");
writeln!(temp_file, "2: 1 3").expect("Operation failed");
writeln!(temp_file, "3: 1 2").expect("Operation failed");
temp_file.flush().expect("Operation failed");
let graph: Graph<i32, f64> =
read_adjacency_list_format(temp_file.path(), false).expect("Operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 3); }
#[test]
fn test_read_weighted_adjacency_list() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(temp_file, "1: 2 0.5 3 1.0").expect("Operation failed");
writeln!(temp_file, "2: 1 0.5").expect("Operation failed");
temp_file.flush().expect("Operation failed");
let graph: Graph<i32, f64> =
read_adjacency_list_format(temp_file.path(), true).expect("Operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn test_read_directed_adjacency_list() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(temp_file, "1: 2 3").expect("Operation failed");
writeln!(temp_file, "2: 3").expect("Operation failed");
writeln!(temp_file, "3:").expect("Operation failed");
temp_file.flush().expect("Operation failed");
let graph: DiGraph<i32, f64> =
read_adjacency_list_format_digraph(temp_file.path(), false).expect("Operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 3); }
#[test]
fn test_write_read_roundtrip() {
let mut original_graph: Graph<i32, f64> = Graph::new();
original_graph
.add_edge(1i32, 2i32, 0.0f64)
.expect("Operation failed");
original_graph
.add_edge(2i32, 3i32, 0.0f64)
.expect("Operation failed");
let temp_file = NamedTempFile::new().expect("Operation failed");
write_adjacency_list_format(&original_graph, temp_file.path(), false)
.expect("Operation failed");
let read_graph: Graph<i32, f64> =
read_adjacency_list_format(temp_file.path(), false).expect("Operation failed");
assert_eq!(read_graph.node_count(), original_graph.node_count());
assert_eq!(read_graph.edge_count(), original_graph.edge_count());
}
#[test]
fn test_malformed_line_handling() {
let mut temp_file = NamedTempFile::new().expect("Operation failed");
writeln!(temp_file, "1 2 3").expect("Operation failed"); temp_file.flush().expect("Operation failed");
let result: Result<Graph<i32, f64>> = read_adjacency_list_format(temp_file.path(), false);
assert!(result.is_err());
}
#[test]
fn test_empty_file() {
let temp_file = NamedTempFile::new().expect("Operation failed");
let graph: Graph<i32, f64> =
read_adjacency_list_format(temp_file.path(), false).expect("Operation failed");
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
}