1use std::fs::File;
54use std::io::{BufRead, BufReader, Write};
55use std::path::Path;
56use std::str::FromStr;
57
58use crate::base::{DiGraph, EdgeWeight, Graph, Node};
59use crate::error::{GraphError, Result};
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63enum ParseState {
64 Header,
66 Body,
68 Done,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74enum GraphType {
75 Undirected,
77 Directed,
79}
80
81#[allow(dead_code)]
101pub fn read_dot_format<N, E, P>(path: P, weighted: bool) -> Result<Graph<N, E>>
102where
103 N: Node + std::fmt::Debug + FromStr + Clone,
104 E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
105 P: AsRef<Path>,
106{
107 let file = File::open(path)?;
108 let reader = BufReader::new(file);
109 let mut graph = Graph::new();
110 let mut state = ParseState::Header;
111 let mut graph_type = None;
112 let mut in_multiline_comment = false;
113
114 for (line_num, line_result) in reader.lines().enumerate() {
115 let line = line_result?;
116 let (processed_line, comment_continues) = remove_comments(&line, in_multiline_comment);
117 in_multiline_comment = comment_continues;
118 let line = processed_line.trim().to_string();
119
120 if line.is_empty() {
121 continue;
122 }
123
124 match state {
125 ParseState::Header => {
126 if let Some(detected_type) = parse_header(&line)? {
127 graph_type = Some(detected_type);
128 state = ParseState::Body;
129 }
130 }
131 ParseState::Body => {
132 if line.contains('}') {
133 state = ParseState::Done;
134 break;
135 }
136
137 if let Some(graph_type) = &graph_type {
139 parse_graph_element(&line, graph_type, &mut graph, weighted, line_num + 1)?;
140 }
141 }
142 ParseState::Done => break,
143 }
144 }
145
146 if graph_type.is_none() {
148 return Err(GraphError::Other(
149 "No valid graph declaration found".to_string(),
150 ));
151 }
152
153 if state != ParseState::Done && state != ParseState::Body {
154 return Err(GraphError::Other(
155 "Incomplete DOT file - missing closing brace".to_string(),
156 ));
157 }
158
159 Ok(graph)
160}
161
162#[allow(dead_code)]
174pub fn read_dot_format_digraph<N, E, P>(path: P, weighted: bool) -> Result<DiGraph<N, E>>
175where
176 N: Node + std::fmt::Debug + FromStr + Clone,
177 E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
178 P: AsRef<Path>,
179{
180 let file = File::open(path)?;
181 let reader = BufReader::new(file);
182 let mut graph = DiGraph::new();
183 let mut state = ParseState::Header;
184 let mut graph_type = None;
185 let mut in_multiline_comment = false;
186
187 for (line_num, line_result) in reader.lines().enumerate() {
188 let line = line_result?;
189 let (processed_line, comment_continues) = remove_comments(&line, in_multiline_comment);
190 in_multiline_comment = comment_continues;
191 let line = processed_line.trim().to_string();
192
193 if line.is_empty() {
194 continue;
195 }
196
197 match state {
198 ParseState::Header => {
199 if let Some(detected_type) = parse_header(&line)? {
200 graph_type = Some(detected_type);
201 state = ParseState::Body;
202 }
203 }
204 ParseState::Body => {
205 if line.contains('}') {
206 state = ParseState::Done;
207 break;
208 }
209
210 if let Some(graph_type) = &graph_type {
212 parse_digraph_element(&line, graph_type, &mut graph, weighted, line_num + 1)?;
213 }
214 }
215 ParseState::Done => break,
216 }
217 }
218
219 if graph_type.is_none() {
221 return Err(GraphError::Other(
222 "No valid graph declaration found".to_string(),
223 ));
224 }
225
226 if state != ParseState::Done && state != ParseState::Body {
227 return Err(GraphError::Other(
228 "Incomplete DOT file - missing closing brace".to_string(),
229 ));
230 }
231
232 Ok(graph)
233}
234
235#[allow(dead_code)]
248pub fn write_dot_format<N, E, Ix, P>(graph: &Graph<N, E, Ix>, path: P, weighted: bool) -> Result<()>
249where
250 N: Node + std::fmt::Debug + std::fmt::Display + Clone,
251 E: EdgeWeight
252 + std::marker::Copy
253 + std::fmt::Debug
254 + std::default::Default
255 + std::fmt::Display
256 + Clone,
257 Ix: petgraph::graph::IndexType,
258 P: AsRef<Path>,
259{
260 let mut file = File::create(path)?;
261
262 writeln!(file, "graph G {{")?;
263 writeln!(file, " // Generated by scirs2-graph")?;
264
265 for node in graph.nodes() {
267 writeln!(file, " {node};")?;
268 }
269
270 writeln!(file)?;
271
272 for edge in graph.edges() {
274 if weighted {
275 writeln!(
276 file,
277 " {} -- {} [weight={}];",
278 edge.source, edge.target, edge.weight
279 )?;
280 } else {
281 writeln!(file, " {} -- {};", edge.source, edge.target)?;
282 }
283 }
284
285 writeln!(file, "}}")?;
286
287 Ok(())
288}
289
290#[allow(dead_code)]
303pub fn write_dot_format_digraph<N, E, Ix, P>(
304 graph: &DiGraph<N, E, Ix>,
305 path: P,
306 weighted: bool,
307) -> Result<()>
308where
309 N: Node + std::fmt::Debug + std::fmt::Display + Clone,
310 E: EdgeWeight
311 + std::marker::Copy
312 + std::fmt::Debug
313 + std::default::Default
314 + std::fmt::Display
315 + Clone,
316 Ix: petgraph::graph::IndexType,
317 P: AsRef<Path>,
318{
319 let mut file = File::create(path)?;
320
321 writeln!(file, "digraph G {{")?;
322 writeln!(file, " // Generated by scirs2-graph")?;
323
324 for node in graph.nodes() {
326 writeln!(file, " {node};")?;
327 }
328
329 writeln!(file)?;
330
331 for edge in graph.edges() {
333 if weighted {
334 writeln!(
335 file,
336 " {} -> {} [weight={}];",
337 edge.source, edge.target, edge.weight
338 )?;
339 } else {
340 writeln!(file, " {} -> {};", edge.source, edge.target)?;
341 }
342 }
343
344 writeln!(file, "}}")?;
345
346 Ok(())
347}
348
349#[allow(dead_code)]
354fn remove_comments(line: &str, in_multiline_comment: bool) -> (String, bool) {
355 let mut result = String::new();
356 let mut chars = line.chars().peekable();
357 let mut in_comment = in_multiline_comment;
358
359 while let Some(ch) = chars.next() {
360 if in_comment {
361 if ch == '*' && chars.peek() == Some(&'/') {
363 chars.next(); in_comment = false;
365 }
366 } else {
368 if ch == '/' {
370 if let Some(&next_ch) = chars.peek() {
371 if next_ch == '/' {
372 break;
374 } else if next_ch == '*' {
375 chars.next(); in_comment = true;
378 continue;
379 }
380 }
381 }
382 if !in_comment {
383 result.push(ch);
384 }
385 }
386 }
387
388 (result, in_comment)
389}
390
391#[allow(dead_code)]
393fn parse_header(line: &str) -> Result<Option<GraphType>> {
394 let line = line.trim();
395
396 if line.starts_with("graph") && line.contains('{') {
397 return Ok(Some(GraphType::Undirected));
398 }
399
400 if line.starts_with("digraph") && line.contains('{') {
401 return Ok(Some(GraphType::Directed));
402 }
403
404 Ok(None)
406}
407
408#[allow(dead_code)]
410fn parse_graph_element<N, E>(
411 line: &str,
412 graph_type: &GraphType,
413 graph: &mut Graph<N, E>,
414 weighted: bool,
415 line_num: usize,
416) -> Result<()>
417where
418 N: Node + std::fmt::Debug + FromStr + Clone,
419 E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
420{
421 let line = line.trim_end_matches(';').trim();
422
423 let edge_separator = match graph_type {
425 GraphType::Undirected => "--",
426 GraphType::Directed => "->",
427 };
428
429 if line.contains(edge_separator) {
430 parse_edge(line, edge_separator, graph, weighted, line_num)?;
431 } else if !line.is_empty() && !line.starts_with('}') && !line.contains('[') {
432 parse_node(line, line_num)?;
434 }
435
436 Ok(())
437}
438
439#[allow(dead_code)]
441fn parse_digraph_element<N, E>(
442 line: &str,
443 graph_type: &GraphType,
444 graph: &mut DiGraph<N, E>,
445 weighted: bool,
446 line_num: usize,
447) -> Result<()>
448where
449 N: Node + std::fmt::Debug + FromStr + Clone,
450 E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
451{
452 let line = line.trim_end_matches(';').trim();
453
454 let edge_separator = match graph_type {
456 GraphType::Undirected => "--",
457 GraphType::Directed => "->",
458 };
459
460 if line.contains(edge_separator) {
461 parse_digraph_edge(line, edge_separator, graph, weighted, line_num)?;
462 } else if !line.is_empty() && !line.starts_with('}') && !line.contains('[') {
463 parse_node(line, line_num)?;
465 }
466
467 Ok(())
468}
469
470#[allow(dead_code)]
472fn parse_edge<N, E>(
473 line: &str,
474 edge_separator: &str,
475 graph: &mut Graph<N, E>,
476 weighted: bool,
477 line_num: usize,
478) -> Result<()>
479where
480 N: Node + std::fmt::Debug + FromStr + Clone,
481 E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
482{
483 let parts: Vec<&str> = line.split(edge_separator).collect();
485 if parts.len() != 2 {
486 return Err(GraphError::Other(format!(
487 "Invalid edge format on line {line_num}: {line}"
488 )));
489 }
490
491 let source_part = parts[0].trim();
492 let target_part = parts[1].trim();
493
494 let source_node = N::from_str(source_part).map_err(|_| {
496 GraphError::Other(format!(
497 "Failed to parse source node '{source_part}' on line {line_num}"
498 ))
499 })?;
500
501 let (target_str, attributes) = if target_part.contains('[') {
503 let bracket_pos = target_part.find('[').expect("Test operation failed");
504 (
505 target_part[..bracket_pos].trim(),
506 Some(&target_part[bracket_pos..]),
507 )
508 } else {
509 (target_part, None)
510 };
511
512 let target_node = N::from_str(target_str).map_err(|_| {
513 GraphError::Other(format!(
514 "Failed to parse target node '{target_str}' on line {line_num}"
515 ))
516 })?;
517
518 let weight = if let (true, Some(attrs)) = (weighted, attributes) {
520 parse_weight_from_attributes(attrs)?
521 } else {
522 E::default()
523 };
524
525 graph.add_edge(source_node, target_node, weight)?;
527
528 Ok(())
529}
530
531#[allow(dead_code)]
533fn parse_digraph_edge<N, E>(
534 line: &str,
535 edge_separator: &str,
536 graph: &mut DiGraph<N, E>,
537 weighted: bool,
538 line_num: usize,
539) -> Result<()>
540where
541 N: Node + std::fmt::Debug + FromStr + Clone,
542 E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
543{
544 let parts: Vec<&str> = line.split(edge_separator).collect();
546 if parts.len() != 2 {
547 return Err(GraphError::Other(format!(
548 "Invalid edge format on line {line_num}: {line}"
549 )));
550 }
551
552 let source_part = parts[0].trim();
553 let target_part = parts[1].trim();
554
555 let source_node = N::from_str(source_part).map_err(|_| {
557 GraphError::Other(format!(
558 "Failed to parse source node '{source_part}' on line {line_num}"
559 ))
560 })?;
561
562 let (target_str, attributes) = if target_part.contains('[') {
564 let bracket_pos = target_part.find('[').expect("Test operation failed");
565 (
566 target_part[..bracket_pos].trim(),
567 Some(&target_part[bracket_pos..]),
568 )
569 } else {
570 (target_part, None)
571 };
572
573 let target_node = N::from_str(target_str).map_err(|_| {
574 GraphError::Other(format!(
575 "Failed to parse target node '{target_str}' on line {line_num}"
576 ))
577 })?;
578
579 let weight = if let (true, Some(attrs)) = (weighted, attributes) {
581 parse_weight_from_attributes(attrs)?
582 } else {
583 E::default()
584 };
585
586 graph.add_edge(source_node, target_node, weight)?;
588
589 Ok(())
590}
591
592#[allow(dead_code)]
594fn parse_node(_line: &str, _line_num: usize) -> Result<()> {
595 Ok(())
598}
599
600#[allow(dead_code)]
602fn parse_weight_from_attributes<E>(attributes: &str) -> Result<E>
603where
604 E: EdgeWeight + std::marker::Copy + std::fmt::Debug + std::default::Default + FromStr,
605{
606 if let Some(weight_start) = attributes.find("weight=") {
608 let weight_part = &attributes[weight_start + 7..]; let weight_end = weight_part
612 .find(&[' ', ',', ']'][..])
613 .unwrap_or(weight_part.len());
614
615 let weight_str = &weight_part[..weight_end];
616
617 return E::from_str(weight_str)
618 .map_err(|_| GraphError::Other(format!("Failed to parse weight: {weight_str}")));
619 }
620
621 Ok(E::default())
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use std::io::Write;
628 use tempfile::NamedTempFile;
629
630 #[test]
631 fn test_read_undirected_dot() {
632 let mut temp_file = NamedTempFile::new().expect("Test operation failed");
633 writeln!(temp_file, "graph G {{").expect("Test operation failed");
634 writeln!(temp_file, " 1 -- 2;").expect("Test operation failed");
635 writeln!(temp_file, " 2 -- 3;").expect("Test operation failed");
636 writeln!(temp_file, "}}").expect("Test operation failed");
637 temp_file.flush().expect("Test operation failed");
638
639 let graph: Graph<i32, f64> =
640 read_dot_format(temp_file.path(), false).expect("Test operation failed");
641
642 assert_eq!(graph.node_count(), 3);
643 assert_eq!(graph.edge_count(), 2);
644 }
645
646 #[test]
647 fn test_read_directed_dot() {
648 let mut temp_file = NamedTempFile::new().expect("Test operation failed");
649 writeln!(temp_file, "digraph G {{").expect("Test operation failed");
650 writeln!(temp_file, " 1 -> 2;").expect("Test operation failed");
651 writeln!(temp_file, " 2 -> 3;").expect("Test operation failed");
652 writeln!(temp_file, "}}").expect("Test operation failed");
653 temp_file.flush().expect("Test operation failed");
654
655 let graph: DiGraph<i32, f64> =
656 read_dot_format_digraph(temp_file.path(), false).expect("Test operation failed");
657
658 assert_eq!(graph.node_count(), 3);
659 assert_eq!(graph.edge_count(), 2);
660 }
661
662 #[test]
663 fn test_read_weighted_dot() {
664 let mut temp_file = NamedTempFile::new().expect("Test operation failed");
665 writeln!(temp_file, "graph G {{").expect("Test operation failed");
666 writeln!(temp_file, " 1 -- 2 [weight=1.5];").expect("Test operation failed");
667 writeln!(temp_file, " 2 -- 3 [weight=2.0];").expect("Test operation failed");
668 writeln!(temp_file, "}}").expect("Test operation failed");
669 temp_file.flush().expect("Test operation failed");
670
671 let graph: Graph<i32, f64> =
672 read_dot_format(temp_file.path(), true).expect("Test operation failed");
673
674 assert_eq!(graph.node_count(), 3);
675 assert_eq!(graph.edge_count(), 2);
676 }
677
678 #[test]
679 fn test_write_read_roundtrip() {
680 let mut original_graph: Graph<i32, f64> = Graph::new();
681 original_graph
682 .add_edge(1i32, 2i32, 1.5f64)
683 .expect("Test operation failed");
684 original_graph
685 .add_edge(2i32, 3i32, 2.0f64)
686 .expect("Test operation failed");
687
688 let temp_file = NamedTempFile::new().expect("Test operation failed");
689 write_dot_format(&original_graph, temp_file.path(), true).expect("Test operation failed");
690
691 let read_graph: Graph<i32, f64> =
692 read_dot_format(temp_file.path(), true).expect("Test operation failed");
693
694 assert_eq!(read_graph.node_count(), original_graph.node_count());
695 assert_eq!(read_graph.edge_count(), original_graph.edge_count());
696 }
697
698 #[test]
699 fn test_digraph_write_read_roundtrip() {
700 let mut original_graph: DiGraph<i32, f64> = DiGraph::new();
701 original_graph
702 .add_edge(1i32, 2i32, 1.5f64)
703 .expect("Test operation failed");
704 original_graph
705 .add_edge(2i32, 3i32, 2.0f64)
706 .expect("Test operation failed");
707
708 let temp_file = NamedTempFile::new().expect("Test operation failed");
709 write_dot_format_digraph(&original_graph, temp_file.path(), true)
710 .expect("Test operation failed");
711
712 let read_graph: DiGraph<i32, f64> =
713 read_dot_format_digraph(temp_file.path(), true).expect("Test operation failed");
714
715 assert_eq!(read_graph.node_count(), original_graph.node_count());
716 assert_eq!(read_graph.edge_count(), original_graph.edge_count());
717 }
718
719 #[test]
720 fn test_remove_comments() {
721 assert_eq!(
722 remove_comments("1 -- 2; // this is a comment", false),
723 ("1 -- 2; ".to_string(), false)
724 );
725 assert_eq!(
726 remove_comments("1 -- 2; /* comment */", false),
727 ("1 -- 2; ".to_string(), false)
728 );
729 assert_eq!(
730 remove_comments("no comments here", false),
731 ("no comments here".to_string(), false)
732 );
733 }
734
735 #[test]
736 fn test_parse_weight_from_attributes() {
737 let weight: f64 =
738 parse_weight_from_attributes("[weight=1.5]").expect("Test operation failed");
739 assert_eq!(weight, 1.5);
740
741 let weight: f64 = parse_weight_from_attributes("[label=\"edge\", weight=2.0]")
742 .expect("Test operation failed");
743 assert_eq!(weight, 2.0);
744
745 let weight: f64 =
746 parse_weight_from_attributes("[color=red]").expect("Test operation failed");
747 assert_eq!(weight, 0.0); }
749
750 #[test]
751 fn test_invalid_dot_format() {
752 let mut temp_file = NamedTempFile::new().expect("Test operation failed");
753 writeln!(temp_file, "invalid format").expect("Test operation failed");
754 temp_file.flush().expect("Test operation failed");
755
756 let result: Result<Graph<i32, f64>> = read_dot_format(temp_file.path(), false);
757 assert!(result.is_err());
758 }
759}