1use std::io::{BufRead, BufReader, Read, Write};
26
27use crate::core::attributes::AttributeValue;
28use crate::core::{Graph, IgraphError, IgraphResult};
29
30#[derive(Debug, Clone)]
32pub struct DlGraph {
33 pub graph: Graph,
35 pub labels: Option<Vec<String>>,
37 pub weights: Option<Vec<f64>>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq)]
42enum DlFormat {
43 FullMatrix,
44 EdgeList1,
45 NodeList1,
46}
47
48#[allow(clippy::too_many_lines)]
65pub fn read_dl<R: Read>(input: R, directed: bool) -> IgraphResult<DlGraph> {
66 let reader = BufReader::new(input);
67 let mut lines: Vec<String> = Vec::new();
68
69 for line_result in reader.lines() {
70 let line = line_result?;
71 lines.push(line);
72 }
73
74 let mut pos = 0;
75 let mut n_vertices: Option<u32> = None;
76 let mut format = DlFormat::FullMatrix;
77 let mut labels: Vec<String> = Vec::new();
78 let mut labels_embedded = false;
79
80 pos = skip_empty(&lines, pos);
82 if pos >= lines.len() {
83 return Err(parse_err(0, "empty DL file"));
84 }
85
86 let header_line = lines[pos].trim().to_ascii_lowercase();
87 if !header_line.starts_with("dl") {
88 return Err(parse_err(pos + 1, "DL file must start with 'DL'"));
89 }
90
91 let header_rest = &lines[pos].trim()[2..];
93 if let Some(n) = extract_n(header_rest) {
94 n_vertices = Some(n);
95 }
96 pos += 1;
97
98 loop {
100 pos = skip_empty(&lines, pos);
101 if pos >= lines.len() {
102 break;
103 }
104
105 let trimmed = lines[pos].trim();
106 let lower = trimmed.to_ascii_lowercase();
107
108 if lower.starts_with("data:") || lower == "data" {
109 pos += 1;
110 break;
111 }
112
113 if lower.starts_with("n=") || lower.starts_with("n =") {
114 if n_vertices.is_none() {
115 let val =
116 extract_n(trimmed).ok_or_else(|| parse_err(pos + 1, "invalid n= value"))?;
117 n_vertices = Some(val);
118 }
119 pos += 1;
120 continue;
121 }
122
123 if lower.starts_with("format") {
124 if lower.contains("fullmatrix") || lower.contains("full matrix") {
125 format = DlFormat::FullMatrix;
126 } else if lower.contains("edgelist1")
127 || lower.contains("edge list1")
128 || lower.contains("edgelist 1")
129 {
130 format = DlFormat::EdgeList1;
131 } else if lower.contains("nodelist1")
132 || lower.contains("node list1")
133 || lower.contains("nodelist 1")
134 {
135 format = DlFormat::NodeList1;
136 }
137 pos += 1;
138 continue;
139 }
140
141 if lower.starts_with("labels embedded") {
142 labels_embedded = true;
143 pos += 1;
144 continue;
145 }
146
147 if lower.starts_with("labels:") || lower == "labels" {
148 pos += 1;
149 while pos < lines.len() {
151 let lbl_line = lines[pos].trim();
152 let lbl_lower = lbl_line.to_ascii_lowercase();
153 if lbl_lower.starts_with("data")
154 || lbl_lower.starts_with("format")
155 || lbl_lower.starts_with("labels")
156 {
157 break;
158 }
159 if !lbl_line.is_empty() {
160 for token in split_labels(lbl_line) {
162 labels.push(token);
163 }
164 }
165 pos += 1;
166 }
167 continue;
168 }
169
170 if let Some(n) = extract_n(trimmed) {
172 if n_vertices.is_none() {
173 n_vertices = Some(n);
174 }
175 pos += 1;
176 continue;
177 }
178
179 pos += 1;
180 }
181
182 let n = n_vertices.ok_or_else(|| parse_err(0, "no vertex count (n=) found in DL file"))?;
183
184 let mut edges: Vec<(u32, u32)> = Vec::new();
185 let mut weights: Vec<f64> = Vec::new();
186 let mut has_weights = false;
187
188 match format {
189 DlFormat::FullMatrix => {
190 if labels_embedded {
191 pos = skip_empty(&lines, pos);
193 if pos < lines.len() {
194 let header_labels = split_labels(lines[pos].trim());
195 if labels.is_empty() {
196 labels = header_labels;
197 }
198 pos += 1;
199 }
200 let mut row = 0u32;
201 while pos < lines.len() && row < n {
202 let trimmed = lines[pos].trim();
203 if trimmed.is_empty() {
204 pos += 1;
205 continue;
206 }
207 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
208 if tokens.is_empty() {
209 pos += 1;
210 continue;
211 }
212 let data_start = 1;
214 for (col, &val) in tokens.iter().skip(data_start).enumerate() {
215 if val == "1" {
216 #[allow(clippy::cast_possible_truncation)]
217 edges.push((row, col as u32));
218 }
219 }
220 row += 1;
221 pos += 1;
222 }
223 } else {
224 let mut row = 0u32;
225 while pos < lines.len() && row < n {
226 let trimmed = lines[pos].trim();
227 if trimmed.is_empty() {
228 pos += 1;
229 continue;
230 }
231 for (col, ch) in trimmed.split_whitespace().enumerate() {
232 if ch == "1" {
233 #[allow(clippy::cast_possible_truncation)]
234 edges.push((row, col as u32));
235 }
236 }
237 row += 1;
238 pos += 1;
239 }
240 }
241 }
242 DlFormat::EdgeList1 => {
243 if labels_embedded {
244 while pos < lines.len() {
245 let trimmed = lines[pos].trim();
246 if trimmed.is_empty() {
247 pos += 1;
248 continue;
249 }
250 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
251 if tokens.len() < 2 {
252 pos += 1;
253 continue;
254 }
255 let from_label = tokens[0].to_string();
256 let to_label = tokens[1].to_string();
257 let from_id = get_or_add_label(&mut labels, &from_label);
258 let to_id = get_or_add_label(&mut labels, &to_label);
259 edges.push((from_id, to_id));
260 if tokens.len() >= 3 {
261 if let Ok(w) = tokens[2].parse::<f64>() {
262 has_weights = true;
263 weights.push(w);
264 } else {
265 weights.push(0.0);
266 }
267 } else {
268 weights.push(0.0);
269 }
270 pos += 1;
271 }
272 } else {
273 while pos < lines.len() {
274 let trimmed = lines[pos].trim();
275 if trimmed.is_empty() {
276 pos += 1;
277 continue;
278 }
279 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
280 if tokens.len() < 2 {
281 pos += 1;
282 continue;
283 }
284 let from: u32 = tokens[0]
285 .parse()
286 .map_err(|e| parse_err(pos + 1, &format!("invalid source id: {e}")))?;
287 let to: u32 = tokens[1]
288 .parse()
289 .map_err(|e| parse_err(pos + 1, &format!("invalid target id: {e}")))?;
290 if from == 0 || to == 0 || from > n || to > n {
291 return Err(parse_err(
292 pos + 1,
293 &format!("vertex ID out of range: {from} {to} (n={n})"),
294 ));
295 }
296 edges.push((from - 1, to - 1));
297 if tokens.len() >= 3 {
298 if let Ok(w) = tokens[2].parse::<f64>() {
299 has_weights = true;
300 weights.push(w);
301 } else {
302 weights.push(0.0);
303 }
304 } else {
305 weights.push(0.0);
306 }
307 pos += 1;
308 }
309 }
310 }
311 DlFormat::NodeList1 => {
312 if labels_embedded {
313 while pos < lines.len() {
314 let trimmed = lines[pos].trim();
315 if trimmed.is_empty() {
316 pos += 1;
317 continue;
318 }
319 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
320 if tokens.is_empty() {
321 pos += 1;
322 continue;
323 }
324 let from_label = tokens[0].to_string();
325 let from_id = get_or_add_label(&mut labels, &from_label);
326 for &tok in &tokens[1..] {
327 let to_label = tok.to_string();
328 let to_id = get_or_add_label(&mut labels, &to_label);
329 edges.push((from_id, to_id));
330 }
331 pos += 1;
332 }
333 } else {
334 while pos < lines.len() {
335 let trimmed = lines[pos].trim();
336 if trimmed.is_empty() {
337 pos += 1;
338 continue;
339 }
340 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
341 if tokens.is_empty() {
342 pos += 1;
343 continue;
344 }
345 let from: u32 = tokens[0]
346 .parse()
347 .map_err(|e| parse_err(pos + 1, &format!("invalid source id: {e}")))?;
348 if from == 0 || from > n {
349 return Err(parse_err(
350 pos + 1,
351 &format!("source vertex ID out of range: {from} (n={n})"),
352 ));
353 }
354 for &tok in &tokens[1..] {
355 let to: u32 = tok
356 .parse()
357 .map_err(|e| parse_err(pos + 1, &format!("invalid target id: {e}")))?;
358 if to == 0 || to > n {
359 return Err(parse_err(
360 pos + 1,
361 &format!("target vertex ID out of range: {to} (n={n})"),
362 ));
363 }
364 edges.push((from - 1, to - 1));
365 }
366 pos += 1;
367 }
368 }
369 }
370 }
371
372 let mut graph = Graph::new(n, directed)?;
373 graph.add_edges(edges)?;
374
375 let final_labels = if labels.is_empty() {
376 None
377 } else {
378 Some(labels)
379 };
380 let final_weights = if has_weights { Some(weights) } else { None };
381
382 if let Some(ref lbls) = final_labels {
383 graph.set_vertex_attribute_all(
384 "name",
385 lbls.iter()
386 .map(|l| AttributeValue::String(l.clone()))
387 .collect(),
388 )?;
389 }
390 if let Some(ref wts) = final_weights {
391 graph.set_edge_attribute_all(
392 "weight",
393 wts.iter().map(|&w| AttributeValue::Numeric(w)).collect(),
394 )?;
395 }
396
397 Ok(DlGraph {
398 graph,
399 labels: final_labels,
400 weights: final_weights,
401 })
402}
403
404fn parse_err(line: usize, msg: &str) -> IgraphError {
405 IgraphError::Parse {
406 line,
407 message: msg.to_string(),
408 }
409}
410
411fn skip_empty(lines: &[String], mut pos: usize) -> usize {
412 while pos < lines.len() && lines[pos].trim().is_empty() {
413 pos += 1;
414 }
415 pos
416}
417
418fn extract_n(s: &str) -> Option<u32> {
419 let lower = s.to_ascii_lowercase();
420 for part in lower.split([',', ';']) {
422 let trimmed = part.trim();
423 if let Some(after_n) = trimmed.strip_prefix('n') {
424 let rest = after_n.trim();
425 if let Some(stripped) = rest.strip_prefix('=') {
426 if let Ok(val) = stripped.trim().parse::<u32>() {
427 return Some(val);
428 }
429 }
430 }
431 }
432 None
433}
434
435fn split_labels(s: &str) -> Vec<String> {
436 if s.contains(',') {
437 s.split(',')
438 .map(|t| t.trim().trim_matches('"').to_string())
439 .filter(|t| !t.is_empty())
440 .collect()
441 } else {
442 s.split_whitespace()
443 .map(|t| t.trim_matches('"').to_string())
444 .collect()
445 }
446}
447
448fn get_or_add_label(labels: &mut Vec<String>, name: &str) -> u32 {
449 for (i, lbl) in labels.iter().enumerate() {
450 if lbl == name {
451 #[allow(clippy::cast_possible_truncation)]
452 return i as u32;
453 }
454 }
455 labels.push(name.to_string());
456 #[allow(clippy::cast_possible_truncation)]
457 let id = (labels.len() - 1) as u32;
458 id
459}
460
461pub fn write_dl<W: Write>(
483 graph: &Graph,
484 vertex_labels: Option<&[String]>,
485 edge_weights: Option<&[f64]>,
486 writer: &mut W,
487) -> IgraphResult<()> {
488 if let Some(l) = vertex_labels {
489 if l.len() != graph.vcount() as usize {
490 return Err(IgraphError::InvalidArgument(format!(
491 "vertex_labels length {} does not match vcount {}",
492 l.len(),
493 graph.vcount()
494 )));
495 }
496 }
497 if let Some(w) = edge_weights {
498 if w.len() != graph.ecount() {
499 return Err(IgraphError::InvalidArgument(format!(
500 "edge_weights length {} does not match ecount {}",
501 w.len(),
502 graph.ecount()
503 )));
504 }
505 }
506
507 writeln!(writer, "DL n={}", graph.vcount())?;
508 writeln!(writer, "format = edgelist1")?;
509
510 let has_attr_labels =
511 vertex_labels.is_none() && graph.vertex_attribute_names().contains(&"name");
512 if let Some(labels) = vertex_labels {
513 writeln!(writer, "labels:")?;
514 let joined: Vec<&str> = labels.iter().map(String::as_str).collect();
515 writeln!(writer, "{}", joined.join(","))?;
516 } else if has_attr_labels {
517 writeln!(writer, "labels:")?;
518 let attr_labels: Vec<String> = (0..graph.vcount())
519 .map(|v| {
520 graph
521 .vertex_attribute("name", v)
522 .and_then(AttributeValue::as_str)
523 .unwrap_or("")
524 .to_owned()
525 })
526 .collect();
527 let joined: Vec<&str> = attr_labels.iter().map(String::as_str).collect();
528 writeln!(writer, "{}", joined.join(","))?;
529 }
530
531 writeln!(writer, "data:")?;
532
533 for eid in 0..graph.ecount() {
534 #[allow(clippy::cast_possible_truncation)]
535 let (from, to) = graph.edge(eid as u32)?;
536
537 if let Some(w) = edge_weights {
538 writeln!(writer, "{} {} {}", from + 1, to + 1, w[eid])?;
539 } else {
540 #[allow(clippy::cast_possible_truncation)]
541 let eid_u32 = eid as u32;
542 if let Some(w) = graph
543 .edge_attribute("weight", eid_u32)
544 .and_then(AttributeValue::as_f64)
545 {
546 writeln!(writer, "{} {} {w}", from + 1, to + 1)?;
547 } else {
548 writeln!(writer, "{} {}", from + 1, to + 1)?;
549 }
550 }
551 }
552
553 Ok(())
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn test_edgelist1_basic() {
562 let input = b"DL n=3\nformat = edgelist1\ndata:\n1 2\n2 3\n1 3\n";
563 let result = read_dl(&input[..], true).unwrap();
564 assert_eq!(result.graph.vcount(), 3);
565 assert_eq!(result.graph.ecount(), 3);
566 assert!(result.graph.is_directed());
567 }
568
569 #[test]
570 fn test_edgelist1_undirected() {
571 let input = b"DL n=3\nformat = edgelist1\ndata:\n1 2\n2 3\n";
572 let result = read_dl(&input[..], false).unwrap();
573 assert_eq!(result.graph.vcount(), 3);
574 assert_eq!(result.graph.ecount(), 2);
575 assert!(!result.graph.is_directed());
576 }
577
578 #[test]
579 fn test_edgelist1_with_weights() {
580 let input = b"DL n=3\nformat = edgelist1\ndata:\n1 2 1.5\n2 3 2.5\n";
581 let result = read_dl(&input[..], true).unwrap();
582 let w = result.weights.unwrap();
583 assert!((w[0] - 1.5).abs() < 1e-10);
584 assert!((w[1] - 2.5).abs() < 1e-10);
585 }
586
587 #[test]
588 fn test_edgelist1_with_labels() {
589 let input = b"DL n=3\nformat = edgelist1\nlabels:\nA,B,C\ndata:\n1 2\n2 3\n";
590 let result = read_dl(&input[..], true).unwrap();
591 let labels = result.labels.unwrap();
592 assert_eq!(labels, vec!["A", "B", "C"]);
593 }
594
595 #[test]
596 fn test_edgelist1_labels_embedded() {
597 let input = b"DL n=3\nformat = edgelist1\nlabels embedded:\ndata:\nAlice Bob\nBob Carol\n";
598 let result = read_dl(&input[..], true).unwrap();
599 assert_eq!(result.graph.ecount(), 2);
600 let labels = result.labels.unwrap();
601 assert_eq!(labels[0], "Alice");
602 assert_eq!(labels[1], "Bob");
603 assert_eq!(labels[2], "Carol");
604 }
605
606 #[test]
607 fn test_fullmatrix_basic() {
608 let input = b"DL n=3\nformat = fullmatrix\ndata:\n0 1 0\n0 0 1\n1 0 0\n";
609 let result = read_dl(&input[..], true).unwrap();
610 assert_eq!(result.graph.vcount(), 3);
611 assert_eq!(result.graph.ecount(), 3);
612 }
613
614 #[test]
615 fn test_fullmatrix_default_format() {
616 let input = b"DL n=3\ndata:\n0 1 1\n1 0 0\n0 1 0\n";
618 let result = read_dl(&input[..], true).unwrap();
619 assert_eq!(result.graph.vcount(), 3);
620 assert_eq!(result.graph.ecount(), 4);
621 }
622
623 #[test]
624 fn test_fullmatrix_labels_embedded() {
625 let input = b"DL n=3\nlabels embedded:\ndata:\nA B C\nA 0 1 0\nB 0 0 1\nC 1 0 0\n";
626 let result = read_dl(&input[..], true).unwrap();
627 assert_eq!(result.graph.vcount(), 3);
628 assert_eq!(result.graph.ecount(), 3);
629 let labels = result.labels.unwrap();
630 assert_eq!(labels, vec!["A", "B", "C"]);
631 }
632
633 #[test]
634 fn test_nodelist1_basic() {
635 let input = b"DL n=4\nformat = nodelist1\ndata:\n1 2 3\n2 3\n3 4\n";
636 let result = read_dl(&input[..], true).unwrap();
637 assert_eq!(result.graph.vcount(), 4);
638 assert_eq!(result.graph.ecount(), 4);
639 }
640
641 #[test]
642 fn test_nodelist1_labels_embedded() {
643 let input = b"DL n=3\nformat = nodelist1\nlabels embedded:\ndata:\nA B C\nB C\n";
644 let result = read_dl(&input[..], true).unwrap();
645 assert_eq!(result.graph.ecount(), 3);
646 let labels = result.labels.unwrap();
647 assert_eq!(labels[0], "A");
648 }
649
650 #[test]
651 fn test_case_insensitive() {
652 let input = b"dl N=2\nFORMAT = EDGELIST1\nDATA:\n1 2\n";
653 let result = read_dl(&input[..], true).unwrap();
654 assert_eq!(result.graph.vcount(), 2);
655 assert_eq!(result.graph.ecount(), 1);
656 }
657
658 #[test]
659 fn test_empty_graph() {
660 let input = b"DL n=5\nformat = edgelist1\ndata:\n";
661 let result = read_dl(&input[..], true).unwrap();
662 assert_eq!(result.graph.vcount(), 5);
663 assert_eq!(result.graph.ecount(), 0);
664 }
665
666 #[test]
667 fn test_no_dl_header_error() {
668 let input = b"n=3\ndata:\n1 2\n";
669 let result = read_dl(&input[..], true);
670 assert!(result.is_err());
671 }
672
673 #[test]
674 fn test_no_n_error() {
675 let input = b"DL\nformat = edgelist1\ndata:\n1 2\n";
676 let result = read_dl(&input[..], true);
677 assert!(result.is_err());
678 }
679
680 #[test]
681 fn test_vertex_id_out_of_range() {
682 let input = b"DL n=2\nformat = edgelist1\ndata:\n1 5\n";
683 let result = read_dl(&input[..], true);
684 assert!(result.is_err());
685 }
686
687 #[test]
688 fn test_zero_vertex_id_error() {
689 let input = b"DL n=2\nformat = edgelist1\ndata:\n0 1\n";
690 let result = read_dl(&input[..], true);
691 assert!(result.is_err());
692 }
693
694 #[test]
695 fn test_n_on_same_line() {
696 let input = b"DL n=4\nformat=edgelist1\ndata:\n1 2\n3 4\n";
697 let result = read_dl(&input[..], true).unwrap();
698 assert_eq!(result.graph.vcount(), 4);
699 assert_eq!(result.graph.ecount(), 2);
700 }
701
702 #[test]
703 fn test_labels_whitespace_separated() {
704 let input = b"DL n=3\nformat = edgelist1\nlabels:\nAlpha Beta Gamma\ndata:\n1 2\n";
705 let result = read_dl(&input[..], true).unwrap();
706 let labels = result.labels.unwrap();
707 assert_eq!(labels, vec!["Alpha", "Beta", "Gamma"]);
708 }
709
710 #[test]
713 fn test_write_basic_directed() {
714 let mut g = Graph::new(3, true).unwrap();
715 g.add_edge(0, 1).unwrap();
716 g.add_edge(1, 2).unwrap();
717
718 let mut buf = Vec::new();
719 write_dl(&g, None, None, &mut buf).unwrap();
720 let s = String::from_utf8(buf).unwrap();
721
722 assert!(s.contains("DL n=3"));
723 assert!(s.contains("format = edgelist1"));
724 assert!(s.contains("data:"));
725 assert!(s.contains("1 2\n"));
726 assert!(s.contains("2 3\n"));
727 }
728
729 #[test]
730 fn test_write_with_labels() {
731 let mut g = Graph::with_vertices(3);
732 g.add_edge(0, 1).unwrap();
733
734 let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
735 let mut buf = Vec::new();
736 write_dl(&g, Some(&labels), None, &mut buf).unwrap();
737 let s = String::from_utf8(buf).unwrap();
738
739 assert!(s.contains("labels:"));
740 assert!(s.contains("A,B,C"));
741 }
742
743 #[test]
744 fn test_write_with_weights() {
745 let mut g = Graph::with_vertices(2);
746 g.add_edge(0, 1).unwrap();
747
748 let weights = vec![3.5];
749 let mut buf = Vec::new();
750 write_dl(&g, None, Some(&weights), &mut buf).unwrap();
751 let s = String::from_utf8(buf).unwrap();
752
753 assert!(s.contains("1 2 3.5\n"));
754 }
755
756 #[test]
757 fn test_write_empty_graph() {
758 let g = Graph::with_vertices(0);
759
760 let mut buf = Vec::new();
761 write_dl(&g, None, None, &mut buf).unwrap();
762 let s = String::from_utf8(buf).unwrap();
763
764 assert!(s.contains("DL n=0"));
765 assert!(s.contains("data:"));
766 }
767
768 #[test]
769 fn test_write_no_edges() {
770 let g = Graph::with_vertices(5);
771
772 let mut buf = Vec::new();
773 write_dl(&g, None, None, &mut buf).unwrap();
774 let s = String::from_utf8(buf).unwrap();
775
776 assert!(s.contains("DL n=5"));
777 let after_data = s.split("data:\n").nth(1).unwrap();
778 assert!(after_data.trim().is_empty());
779 }
780
781 #[test]
782 fn test_write_label_mismatch_error() {
783 let g = Graph::with_vertices(3);
784 let labels = vec!["A".to_string()];
785 let mut buf = Vec::new();
786 assert!(write_dl(&g, Some(&labels), None, &mut buf).is_err());
787 }
788
789 #[test]
790 fn test_write_weight_mismatch_error() {
791 let mut g = Graph::with_vertices(2);
792 g.add_edge(0, 1).unwrap();
793 let weights = vec![1.0, 2.0];
794 let mut buf = Vec::new();
795 assert!(write_dl(&g, None, Some(&weights), &mut buf).is_err());
796 }
797
798 #[test]
799 fn test_roundtrip_directed() {
800 let mut g = Graph::new(4, true).unwrap();
801 g.add_edge(0, 1).unwrap();
802 g.add_edge(1, 2).unwrap();
803 g.add_edge(2, 3).unwrap();
804
805 let mut buf = Vec::new();
806 write_dl(&g, None, None, &mut buf).unwrap();
807 let result = read_dl(&buf[..], true).unwrap();
808
809 assert_eq!(result.graph.vcount(), g.vcount());
810 assert_eq!(result.graph.ecount(), g.ecount());
811 assert!(result.graph.is_directed());
812 }
813
814 #[test]
815 fn test_roundtrip_undirected() {
816 let mut g = Graph::with_vertices(3);
817 g.add_edge(0, 1).unwrap();
818 g.add_edge(1, 2).unwrap();
819
820 let mut buf = Vec::new();
821 write_dl(&g, None, None, &mut buf).unwrap();
822 let result = read_dl(&buf[..], false).unwrap();
823
824 assert_eq!(result.graph.vcount(), g.vcount());
825 assert_eq!(result.graph.ecount(), g.ecount());
826 assert!(!result.graph.is_directed());
827 }
828
829 #[test]
830 fn test_roundtrip_with_labels() {
831 let mut g = Graph::with_vertices(3);
832 g.add_edge(0, 1).unwrap();
833 g.add_edge(1, 2).unwrap();
834
835 let labels = vec!["X".to_string(), "Y".to_string(), "Z".to_string()];
836 let mut buf = Vec::new();
837 write_dl(&g, Some(&labels), None, &mut buf).unwrap();
838 let result = read_dl(&buf[..], false).unwrap();
839
840 assert_eq!(result.labels.unwrap(), labels);
841 }
842
843 #[test]
844 fn test_roundtrip_with_weights() {
845 let mut g = Graph::with_vertices(2);
846 g.add_edge(0, 1).unwrap();
847
848 let weights = vec![2.75];
849 let mut buf = Vec::new();
850 write_dl(&g, None, Some(&weights), &mut buf).unwrap();
851 let result = read_dl(&buf[..], false).unwrap();
852
853 let w = result.weights.unwrap();
854 assert!((w[0] - 2.75).abs() < 1e-10);
855 }
856
857 #[test]
858 fn test_roundtrip_with_labels_and_weights() {
859 let mut g = Graph::new(3, true).unwrap();
860 g.add_edge(0, 1).unwrap();
861 g.add_edge(1, 2).unwrap();
862
863 let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
864 let weights = vec![1.5, 2.5];
865 let mut buf = Vec::new();
866 write_dl(&g, Some(&labels), Some(&weights), &mut buf).unwrap();
867 let result = read_dl(&buf[..], true).unwrap();
868
869 assert_eq!(result.graph.vcount(), 3);
870 assert_eq!(result.graph.ecount(), 2);
871 assert_eq!(result.labels.unwrap(), labels);
872 let w = result.weights.unwrap();
873 assert!((w[0] - 1.5).abs() < 1e-10);
874 assert!((w[1] - 2.5).abs() < 1e-10);
875 }
876
877 #[test]
878 fn test_write_self_loop() {
879 let mut g = Graph::with_vertices(2);
880 g.add_edge(0, 0).unwrap();
881
882 let mut buf = Vec::new();
883 write_dl(&g, None, None, &mut buf).unwrap();
884 let s = String::from_utf8(buf).unwrap();
885
886 assert!(s.contains("1 1\n"));
887 }
888
889 #[test]
890 fn test_write_one_based_ids() {
891 let mut g = Graph::with_vertices(4);
892 g.add_edge(2, 3).unwrap();
893
894 let mut buf = Vec::new();
895 write_dl(&g, None, None, &mut buf).unwrap();
896 let s = String::from_utf8(buf).unwrap();
897
898 assert!(s.contains("3 4\n"));
899 }
900
901 #[test]
904 fn test_read_stores_label_attribute() {
905 let input = b"DL n=3\nformat = edgelist1\nlabels:\nAlice,Bob,Carol\ndata:\n1 2\n";
906 let result = read_dl(&input[..], false).unwrap();
907 assert_eq!(
908 result
909 .graph
910 .vertex_attribute("name", 0)
911 .and_then(AttributeValue::as_str),
912 Some("Alice")
913 );
914 assert_eq!(
915 result
916 .graph
917 .vertex_attribute("name", 2)
918 .and_then(AttributeValue::as_str),
919 Some("Carol")
920 );
921 }
922
923 #[test]
924 fn test_read_stores_weight_attribute() {
925 let input = b"DL n=2\nformat = edgelist1\ndata:\n1 2 4.5\n";
926 let result = read_dl(&input[..], false).unwrap();
927 let w = result
928 .graph
929 .edge_attribute("weight", 0)
930 .and_then(AttributeValue::as_f64)
931 .unwrap();
932 assert!((w - 4.5).abs() < 1e-10);
933 }
934
935 #[test]
936 fn test_read_no_label_attribute_when_absent() {
937 let input = b"DL n=2\nformat = edgelist1\ndata:\n1 2\n";
938 let result = read_dl(&input[..], false).unwrap();
939 assert!(result.graph.vertex_attribute("name", 0).is_none());
940 }
941
942 #[test]
943 fn test_write_fallback_to_label_attribute() {
944 let mut g = Graph::with_vertices(2);
945 g.add_edge(0, 1).unwrap();
946 g.set_vertex_attribute("name", 0, AttributeValue::String("X".into()))
947 .unwrap();
948 g.set_vertex_attribute("name", 1, AttributeValue::String("Y".into()))
949 .unwrap();
950
951 let mut buf = Vec::new();
952 write_dl(&g, None, None, &mut buf).unwrap();
953 let s = String::from_utf8(buf).unwrap();
954 assert!(s.contains("labels:"));
955 assert!(s.contains("X,Y"));
956 }
957
958 #[test]
959 fn test_write_fallback_to_weight_attribute() {
960 let mut g = Graph::with_vertices(2);
961 g.add_edge(0, 1).unwrap();
962 g.set_edge_attribute("weight", 0, AttributeValue::Numeric(7.5))
963 .unwrap();
964
965 let mut buf = Vec::new();
966 write_dl(&g, None, None, &mut buf).unwrap();
967 let s = String::from_utf8(buf).unwrap();
968 assert!(s.contains("1 2 7.5"));
969 }
970
971 #[test]
972 fn test_roundtrip_via_attributes() {
973 let input =
974 b"DL n=3\nformat = edgelist1\nlabels:\nAlice,Bob,Carol\ndata:\n1 2 1.5\n2 3 2.5\n";
975 let result = read_dl(&input[..], false).unwrap();
976
977 let mut buf = Vec::new();
978 write_dl(&result.graph, None, None, &mut buf).unwrap();
979
980 let result2 = read_dl(&buf[..], false).unwrap();
981 assert_eq!(result2.graph.vcount(), 3);
982 assert_eq!(result2.graph.ecount(), 2);
983 assert!(result2.labels.is_some());
984 assert!(result2.weights.is_some());
985 }
986
987 #[test]
988 fn test_explicit_params_override_attributes() {
989 let mut g = Graph::with_vertices(2);
990 g.add_edge(0, 1).unwrap();
991 g.set_vertex_attribute("name", 0, AttributeValue::String("attr_A".into()))
992 .unwrap();
993 g.set_vertex_attribute("name", 1, AttributeValue::String("attr_B".into()))
994 .unwrap();
995 g.set_edge_attribute("weight", 0, AttributeValue::Numeric(9.0))
996 .unwrap();
997
998 let labels = vec!["explicit_A".to_string(), "explicit_B".to_string()];
999 let weights = vec![1.0];
1000 let mut buf = Vec::new();
1001 write_dl(&g, Some(&labels), Some(&weights), &mut buf).unwrap();
1002 let s = String::from_utf8(buf).unwrap();
1003 assert!(s.contains("explicit_A,explicit_B"));
1004 assert!(!s.contains("attr_A"));
1005 assert!(s.contains("1 2 1"));
1006 }
1007}