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};
#[derive(Debug, Clone, PartialEq, Eq)]
enum ParseState {
SearchingGraph,
InGraph,
InNode,
InEdge,
Done,
}
#[derive(Debug, Clone, PartialEq)]
enum GmlToken {
OpenBracket,
CloseBracket,
Identifier(String),
Integer(i64),
Float(f64),
String(String),
}
struct GmlLexer {
input: String,
position: usize,
}
impl GmlLexer {
fn new(input: String) -> Self {
Self { input, position: 0 }
}
fn next_token(&mut self) -> Option<GmlToken> {
self.skip_whitespace_and_comments();
if self.position >= self.input.len() {
return None;
}
let remaining = &self.input[self.position..];
if remaining.starts_with('[') {
self.position += 1;
return Some(GmlToken::OpenBracket);
}
if remaining.starts_with(']') {
self.position += 1;
return Some(GmlToken::CloseBracket);
}
if remaining.starts_with('"') {
return self.parse_string();
}
self.parse_number_or_identifier()
}
fn skip_whitespace_and_comments(&mut self) {
while self.position < self.input.len() {
let ch = self
.input
.chars()
.nth(self.position)
.expect("Character index out of bounds");
if ch.is_whitespace() {
self.position += 1;
} else if ch == '#' {
while self.position < self.input.len() {
let ch = self
.input
.chars()
.nth(self.position)
.expect("Character index out of bounds");
self.position += 1;
if ch == '\n' {
break;
}
}
} else {
break;
}
}
}
fn parse_string(&mut self) -> Option<GmlToken> {
self.position += 1; let start = self.position;
while self.position < self.input.len() {
let ch = self
.input
.chars()
.nth(self.position)
.expect("Character index out of bounds");
if ch == '"' {
let value = self.input[start..self.position].to_string();
self.position += 1; return Some(GmlToken::String(value));
}
self.position += 1;
}
None }
fn parse_number_or_identifier(&mut self) -> Option<GmlToken> {
let start = self.position;
while self.position < self.input.len() {
let ch = self
.input
.chars()
.nth(self.position)
.expect("Character index out of bounds");
if ch.is_alphanumeric() || ch == '_' || ch == '.' || ch == '-' {
self.position += 1;
} else {
break;
}
}
let value = &self.input[start..self.position];
if let Ok(int_val) = value.parse::<i64>() {
return Some(GmlToken::Integer(int_val));
}
if let Ok(float_val) = value.parse::<f64>() {
return Some(GmlToken::Float(float_val));
}
Some(GmlToken::Identifier(value.to_string()))
}
}
#[allow(dead_code)]
pub fn read_gml_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 content = reader
.lines()
.collect::<std::io::Result<Vec<_>>>()?
.join("\n");
let mut lexer = GmlLexer::new(content);
let mut graph = Graph::new();
let mut state = ParseState::SearchingGraph;
let mut is_directed = false;
let mut _current_node_id: Option<String> = None;
let mut current_edge_source: Option<String> = None;
let mut current_edge_target: Option<String> = None;
let mut current_edge_weight: Option<String> = None;
while let Some(token) = lexer.next_token() {
match (&state, &token) {
(ParseState::SearchingGraph, GmlToken::Identifier(id)) if id == "graph" => {
if let Some(GmlToken::OpenBracket) = lexer.next_token() {
state = ParseState::InGraph;
} else {
return Err(GraphError::Other("Expected '[' after 'graph'".to_string()));
}
}
(ParseState::InGraph, GmlToken::Identifier(id)) => {
match id.as_str() {
"directed" => {
if let Some(GmlToken::Integer(val)) = lexer.next_token() {
is_directed = val != 0;
}
}
"node" => {
if let Some(GmlToken::OpenBracket) = lexer.next_token() {
state = ParseState::InNode;
_current_node_id = None;
}
}
"edge" => {
if let Some(GmlToken::OpenBracket) = lexer.next_token() {
state = ParseState::InEdge;
current_edge_source = None;
current_edge_target = None;
current_edge_weight = None;
}
}
_ => {
lexer.next_token();
}
}
}
(ParseState::InNode, GmlToken::Identifier(id)) => {
match id.as_str() {
"id" => {
if let Some(token) = lexer.next_token() {
_current_node_id = match token {
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
GmlToken::Identifier(val) => Some(val),
_ => None,
};
}
}
_ => {
lexer.next_token();
}
}
}
(ParseState::InEdge, GmlToken::Identifier(id)) => {
match id.as_str() {
"source" => {
if let Some(token) = lexer.next_token() {
current_edge_source = match token {
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
GmlToken::Identifier(val) => Some(val),
_ => None,
};
}
}
"target" => {
if let Some(token) = lexer.next_token() {
current_edge_target = match token {
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
GmlToken::Identifier(val) => Some(val),
_ => None,
};
}
}
"weight" => {
if weighted {
if let Some(token) = lexer.next_token() {
current_edge_weight = match token {
GmlToken::Float(val) => Some(val.to_string()),
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
_ => None,
};
}
} else {
lexer.next_token(); }
}
_ => {
lexer.next_token();
}
}
}
(ParseState::InNode, GmlToken::CloseBracket) => {
state = ParseState::InGraph;
}
(ParseState::InEdge, GmlToken::CloseBracket) => {
if let (Some(source_str), Some(target_str)) =
(¤t_edge_source, ¤t_edge_target)
{
let source_node = N::from_str(source_str).map_err(|_| {
GraphError::Other(format!("Failed to parse source node: {source_str}"))
})?;
let target_node = N::from_str(target_str).map_err(|_| {
GraphError::Other(format!("Failed to parse target node: {target_str}"))
})?;
let weight = if let Some(weight_str) = ¤t_edge_weight {
E::from_str(weight_str).map_err(|_| {
GraphError::Other(format!("Failed to parse edge weight: {weight_str}"))
})?
} else {
E::default()
};
graph.add_edge(source_node, target_node, weight)?;
}
state = ParseState::InGraph;
}
(ParseState::InGraph, GmlToken::CloseBracket) => {
state = ParseState::Done;
break;
}
_ => {
}
}
}
if state == ParseState::SearchingGraph {
return Err(GraphError::Other(
"No valid GML graph structure found".to_string(),
));
}
if is_directed {
return Err(GraphError::Other(
"GML file contains a directed graph, but undirected graph was requested".to_string(),
));
}
Ok(graph)
}
#[allow(dead_code)]
pub fn read_gml_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 content = reader
.lines()
.collect::<std::io::Result<Vec<_>>>()?
.join("\n");
let mut lexer = GmlLexer::new(content);
let mut graph = DiGraph::new();
let mut state = ParseState::SearchingGraph;
let mut _current_node_id: Option<String> = None;
let mut current_edge_source: Option<String> = None;
let mut current_edge_target: Option<String> = None;
let mut current_edge_weight: Option<String> = None;
while let Some(token) = lexer.next_token() {
match (&state, &token) {
(ParseState::SearchingGraph, GmlToken::Identifier(id)) if id == "graph" => {
if let Some(GmlToken::OpenBracket) = lexer.next_token() {
state = ParseState::InGraph;
} else {
return Err(GraphError::Other("Expected '[' after 'graph'".to_string()));
}
}
(ParseState::InGraph, GmlToken::Identifier(id)) => {
match id.as_str() {
"directed" => {
lexer.next_token();
}
"node" => {
if let Some(GmlToken::OpenBracket) = lexer.next_token() {
state = ParseState::InNode;
_current_node_id = None;
}
}
"edge" => {
if let Some(GmlToken::OpenBracket) = lexer.next_token() {
state = ParseState::InEdge;
current_edge_source = None;
current_edge_target = None;
current_edge_weight = None;
}
}
_ => {
lexer.next_token();
}
}
}
(ParseState::InNode, GmlToken::Identifier(id)) => {
match id.as_str() {
"id" => {
if let Some(token) = lexer.next_token() {
_current_node_id = match token {
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
GmlToken::Identifier(val) => Some(val),
_ => None,
};
}
}
_ => {
lexer.next_token();
}
}
}
(ParseState::InEdge, GmlToken::Identifier(id)) => {
match id.as_str() {
"source" => {
if let Some(token) = lexer.next_token() {
current_edge_source = match token {
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
GmlToken::Identifier(val) => Some(val),
_ => None,
};
}
}
"target" => {
if let Some(token) = lexer.next_token() {
current_edge_target = match token {
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
GmlToken::Identifier(val) => Some(val),
_ => None,
};
}
}
"weight" => {
if weighted {
if let Some(token) = lexer.next_token() {
current_edge_weight = match token {
GmlToken::Float(val) => Some(val.to_string()),
GmlToken::Integer(val) => Some(val.to_string()),
GmlToken::String(val) => Some(val),
_ => None,
};
}
} else {
lexer.next_token(); }
}
_ => {
lexer.next_token();
}
}
}
(ParseState::InNode, GmlToken::CloseBracket) => {
state = ParseState::InGraph;
}
(ParseState::InEdge, GmlToken::CloseBracket) => {
if let (Some(source_str), Some(target_str)) =
(¤t_edge_source, ¤t_edge_target)
{
let source_node = N::from_str(source_str).map_err(|_| {
GraphError::Other(format!("Failed to parse source node: {source_str}"))
})?;
let target_node = N::from_str(target_str).map_err(|_| {
GraphError::Other(format!("Failed to parse target node: {target_str}"))
})?;
let weight = if let Some(weight_str) = ¤t_edge_weight {
E::from_str(weight_str).map_err(|_| {
GraphError::Other(format!("Failed to parse edge weight: {weight_str}"))
})?
} else {
E::default()
};
graph.add_edge(source_node, target_node, weight)?;
}
state = ParseState::InGraph;
}
(ParseState::InGraph, GmlToken::CloseBracket) => {
state = ParseState::Done;
break;
}
_ => {
}
}
}
if state == ParseState::SearchingGraph {
return Err(GraphError::Other(
"No valid GML graph structure found".to_string(),
));
}
Ok(graph)
}
#[allow(dead_code)]
pub fn write_gml_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, "# Generated by scirs2-graph")?;
writeln!(file, "graph [")?;
writeln!(file, " directed 0")?;
for node in graph.nodes() {
writeln!(file, " node [")?;
writeln!(file, " id {node}")?;
writeln!(file, " ]")?;
}
let edges = graph.edges();
for edge in edges {
writeln!(file, " edge [")?;
writeln!(file, " source {}", edge.source)?;
writeln!(file, " target {}", edge.target)?;
if weighted {
writeln!(file, " weight {}", edge.weight)?;
}
writeln!(file, " ]")?;
}
writeln!(file, "]")?;
Ok(())
}
#[allow(dead_code)]
pub fn write_gml_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, "# Generated by scirs2-graph (directed)")?;
writeln!(file, "graph [")?;
writeln!(file, " directed 1")?;
for node in graph.nodes() {
writeln!(file, " node [")?;
writeln!(file, " id {node}")?;
writeln!(file, " ]")?;
}
let edges = graph.edges();
for edge in edges {
writeln!(file, " edge [")?;
writeln!(file, " source {}", edge.source)?;
writeln!(file, " target {}", edge.target)?;
if weighted {
writeln!(file, " weight {}", edge.weight)?;
}
writeln!(file, " ]")?;
}
writeln!(file, "]")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_read_simple_gml() {
let mut temp_file = NamedTempFile::new().expect("Test I/O operation failed");
writeln!(temp_file, "graph [").expect("Test I/O operation failed");
writeln!(temp_file, " directed 0").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 1 ]").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 3 ]").expect("Test I/O operation failed");
writeln!(temp_file, " edge [ source 1 target 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, " edge [ source 2 target 3 ]").expect("Test I/O operation failed");
writeln!(temp_file, "]").expect("Test I/O operation failed");
temp_file.flush().expect("Test I/O operation failed");
let graph: Graph<i32, f64> =
read_gml_format(temp_file.path(), false).expect("Test I/O operation failed");
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn test_read_weighted_gml() {
let mut temp_file = NamedTempFile::new().expect("Test I/O operation failed");
writeln!(temp_file, "graph [").expect("Test I/O operation failed");
writeln!(temp_file, " directed 0").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 1 ]").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, " edge [ source 1 target 2 weight 1.5 ]")
.expect("Test I/O operation failed");
writeln!(temp_file, "]").expect("Test I/O operation failed");
temp_file.flush().expect("Test I/O operation failed");
let graph: Graph<i32, f64> =
read_gml_format(temp_file.path(), true).expect("Test I/O operation failed");
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
}
#[test]
fn test_read_directed_gml() {
let mut temp_file = NamedTempFile::new().expect("Test I/O operation failed");
writeln!(temp_file, "graph [").expect("Test I/O operation failed");
writeln!(temp_file, " directed 1").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 1 ]").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, " edge [ source 1 target 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, "]").expect("Test I/O operation failed");
temp_file.flush().expect("Test I/O operation failed");
let graph: DiGraph<i32, f64> =
read_gml_format_digraph(temp_file.path(), false).expect("Test I/O operation failed");
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
}
#[test]
fn test_write_read_roundtrip() {
let mut original_graph: Graph<i32, f64> = Graph::new();
original_graph
.add_edge(1i32, 2i32, 1.5f64)
.expect("Test I/O operation failed");
original_graph
.add_edge(2i32, 3i32, 2.0f64)
.expect("Test I/O operation failed");
let temp_file = NamedTempFile::new().expect("Test I/O operation failed");
write_gml_format(&original_graph, temp_file.path(), false)
.expect("Test I/O operation failed");
let read_graph: Graph<i32, f64> =
read_gml_format(temp_file.path(), false).expect("Test I/O 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_digraph_write_read_roundtrip() {
let mut original_graph: DiGraph<i32, f64> = DiGraph::new();
original_graph
.add_edge(1i32, 2i32, 1.5f64)
.expect("Test I/O operation failed");
original_graph
.add_edge(2i32, 3i32, 2.0f64)
.expect("Test I/O operation failed");
let temp_file = NamedTempFile::new().expect("Test I/O operation failed");
write_gml_format_digraph(&original_graph, temp_file.path(), false)
.expect("Test I/O operation failed");
let read_graph: DiGraph<i32, f64> =
read_gml_format_digraph(temp_file.path(), false).expect("Test I/O 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_invalid_gml() {
let mut temp_file = NamedTempFile::new().expect("Test I/O operation failed");
writeln!(temp_file, "invalid gml format").expect("Test I/O operation failed");
temp_file.flush().expect("Test I/O operation failed");
let result: Result<Graph<i32, f64>> = read_gml_format(temp_file.path(), false);
assert!(result.is_err());
}
#[test]
fn test_directed_mismatch() {
let mut temp_file = NamedTempFile::new().expect("Test I/O operation failed");
writeln!(temp_file, "graph [").expect("Test I/O operation failed");
writeln!(temp_file, " directed 1").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 1 ]").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, " edge [ source 1 target 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, "]").expect("Test I/O operation failed");
temp_file.flush().expect("Test I/O operation failed");
let result: Result<Graph<i32, f64>> = read_gml_format(temp_file.path(), false);
assert!(result.is_err());
}
#[test]
fn test_gml_with_comments() {
let mut temp_file = NamedTempFile::new().expect("Test I/O operation failed");
writeln!(temp_file, "# This is a comment").expect("Test I/O operation failed");
writeln!(temp_file, "graph [").expect("Test I/O operation failed");
writeln!(temp_file, " # Another comment").expect("Test I/O operation failed");
writeln!(temp_file, " directed 0").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 1 label \"Node 1\" ]").expect("Test I/O operation failed");
writeln!(temp_file, " node [ id 2 label \"Node 2\" ]").expect("Test I/O operation failed");
writeln!(temp_file, " edge [ source 1 target 2 ]").expect("Test I/O operation failed");
writeln!(temp_file, "]").expect("Test I/O operation failed");
temp_file.flush().expect("Test I/O operation failed");
let graph: Graph<i32, f64> =
read_gml_format(temp_file.path(), false).expect("Test I/O operation failed");
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
}
}