rust_igraph/algorithms/io/
pajek.rs1use std::io::{BufRead, BufReader, Read, Write};
29
30use crate::core::{Graph, IgraphError, IgraphResult};
31
32#[derive(Debug, Clone)]
34pub struct PajekGraph {
35 pub graph: Graph,
37 pub labels: Option<Vec<String>>,
40 pub weights: Option<Vec<f64>>,
43}
44
45#[allow(clippy::too_many_lines)]
62pub fn read_pajek<R: Read>(input: R) -> IgraphResult<PajekGraph> {
63 #[derive(PartialEq)]
64 enum Section {
65 None,
66 Vertices,
67 Edges,
68 }
69
70 let reader = BufReader::new(input);
71
72 let mut n_vertices: Option<u32> = None;
73 let mut directed = false;
74 let mut labels: Vec<String> = Vec::new();
75 let mut has_any_label = false;
76 let mut edges: Vec<(u32, u32)> = Vec::new();
77 let mut weights: Vec<f64> = Vec::new();
78 let mut has_any_weight = false;
79 let mut section = Section::None;
80
81 for (line_idx, line_result) in reader.lines().enumerate() {
82 let line = line_result?;
83 let trimmed = line.trim();
84
85 if trimmed.is_empty() || trimmed.starts_with('%') {
86 continue;
87 }
88
89 let lower = trimmed.to_ascii_lowercase();
90
91 if lower.starts_with("*vertices") {
93 let parts: Vec<&str> = trimmed.split_whitespace().collect();
94 if parts.len() < 2 {
95 return Err(IgraphError::Parse {
96 line: line_idx.wrapping_add(1),
97 message: "*Vertices line needs vertex count".into(),
98 });
99 }
100 let n: u32 = parts[1].parse().map_err(|e| IgraphError::Parse {
101 line: line_idx.wrapping_add(1),
102 message: format!("invalid vertex count: {e}"),
103 })?;
104 n_vertices = Some(n);
105 labels = vec![String::new(); n as usize];
106 section = Section::Vertices;
107 continue;
108 }
109
110 if lower.starts_with("*edges") || lower.starts_with("*edgeslist") {
111 directed = false;
112 section = Section::Edges;
113 continue;
114 }
115
116 if lower.starts_with("*arcs") || lower.starts_with("*arcslist") {
117 directed = true;
118 section = Section::Edges;
119 continue;
120 }
121
122 if lower.starts_with('*') {
124 section = Section::None;
125 continue;
126 }
127
128 match section {
129 Section::Vertices => {
130 let (vid, label) = parse_vertex_line(trimmed, line_idx)?;
133 if let Some(n) = n_vertices {
134 if vid == 0 || vid > n {
135 return Err(IgraphError::Parse {
136 line: line_idx.wrapping_add(1),
137 message: format!("vertex id {vid} out of range [1, {n}]"),
138 });
139 }
140 }
141 if let Some(lbl) = label {
142 has_any_label = true;
143 let idx = (vid.wrapping_sub(1)) as usize;
144 if idx < labels.len() {
145 labels[idx] = lbl;
146 }
147 }
148 }
149 Section::Edges => {
150 let parts: Vec<&str> = trimmed.split_whitespace().collect();
152 if parts.len() < 2 {
153 return Err(IgraphError::Parse {
154 line: line_idx.wrapping_add(1),
155 message: "edge line needs at least: FROM TO".into(),
156 });
157 }
158 let from: u32 = parts[0].parse().map_err(|e| IgraphError::Parse {
159 line: line_idx.wrapping_add(1),
160 message: format!("invalid source id: {e}"),
161 })?;
162 let to: u32 = parts[1].parse().map_err(|e| IgraphError::Parse {
163 line: line_idx.wrapping_add(1),
164 message: format!("invalid target id: {e}"),
165 })?;
166
167 if from == 0 || to == 0 {
168 return Err(IgraphError::Parse {
169 line: line_idx.wrapping_add(1),
170 message: "vertex IDs are 1-based in Pajek format".into(),
171 });
172 }
173
174 edges.push((from.wrapping_sub(1), to.wrapping_sub(1)));
175
176 let weight = if parts.len() >= 3 {
177 match parts[2].parse::<f64>() {
178 Ok(w) => {
179 has_any_weight = true;
180 w
181 }
182 Err(_) => 0.0,
183 }
184 } else {
185 0.0
186 };
187 weights.push(weight);
188 }
189 Section::None => {
190 }
192 }
193 }
194
195 let n = n_vertices.ok_or_else(|| IgraphError::Parse {
196 line: 0,
197 message: "no *Vertices line found".into(),
198 })?;
199
200 let mut graph = Graph::new(n, directed)?;
201 graph.add_edges(edges)?;
202
203 Ok(PajekGraph {
204 graph,
205 labels: if has_any_label { Some(labels) } else { None },
206 weights: if has_any_weight { Some(weights) } else { None },
207 })
208}
209
210pub fn write_pajek<W: Write>(
233 graph: &Graph,
234 labels: Option<&[String]>,
235 weights: Option<&[f64]>,
236 writer: &mut W,
237) -> IgraphResult<()> {
238 if let Some(l) = labels {
239 if l.len() != graph.vcount() as usize {
240 return Err(IgraphError::InvalidArgument(format!(
241 "labels length {} does not match vcount {}",
242 l.len(),
243 graph.vcount()
244 )));
245 }
246 }
247 if let Some(w) = weights {
248 if w.len() != graph.ecount() {
249 return Err(IgraphError::InvalidArgument(format!(
250 "weights length {} does not match ecount {}",
251 w.len(),
252 graph.ecount()
253 )));
254 }
255 }
256
257 writeln!(writer, "*Vertices {}", graph.vcount())?;
258 for v in 0..graph.vcount() {
259 let label = match labels {
260 Some(l) => format!("\"{}\"", escape_pajek_string(&l[v as usize])),
261 None => format!("\"{}\"", v + 1),
262 };
263 writeln!(writer, "{} {label}", v + 1)?;
264 }
265
266 if graph.is_directed() {
267 writeln!(writer, "*Arcs")?;
268 } else {
269 writeln!(writer, "*Edges")?;
270 }
271
272 for eid in 0..graph.ecount() {
273 #[allow(clippy::cast_possible_truncation)]
274 let (from, to) = graph.edge(eid as u32)?;
275 match weights {
276 Some(w) => writeln!(writer, "{} {} {}", from + 1, to + 1, w[eid])?,
277 None => writeln!(writer, "{} {}", from + 1, to + 1)?,
278 }
279 }
280
281 Ok(())
282}
283
284fn parse_vertex_line(line: &str, line_idx: usize) -> IgraphResult<(u32, Option<String>)> {
285 let mut chars = line.chars().peekable();
286
287 while chars.peek().is_some_and(|c| c.is_whitespace()) {
289 chars.next();
290 }
291
292 let mut id_str = String::new();
294 while chars.peek().is_some_and(char::is_ascii_digit) {
295 id_str.push(chars.next().unwrap());
296 }
297
298 if id_str.is_empty() {
299 return Err(IgraphError::Parse {
300 line: line_idx.wrapping_add(1),
301 message: "expected vertex ID".into(),
302 });
303 }
304
305 let vid: u32 = id_str.parse().map_err(|e| IgraphError::Parse {
306 line: line_idx.wrapping_add(1),
307 message: format!("invalid vertex id: {e}"),
308 })?;
309
310 while chars.peek().is_some_and(|c| c.is_whitespace()) {
312 chars.next();
313 }
314
315 let label = if chars.peek() == Some(&'"') {
317 chars.next(); let mut lbl = String::new();
319 loop {
320 match chars.next() {
321 Some('"') | None => break,
322 Some('\\') => {
323 if let Some(c) = chars.next() {
324 lbl.push(c);
325 }
326 }
327 Some(c) => lbl.push(c),
328 }
329 }
330 Some(lbl)
331 } else {
332 let mut lbl = String::new();
334 while chars.peek().is_some_and(|c| !c.is_whitespace()) {
335 lbl.push(chars.next().unwrap());
336 }
337 if lbl.is_empty() { None } else { Some(lbl) }
338 };
339
340 Ok((vid, label))
341}
342
343fn escape_pajek_string(s: &str) -> String {
344 let mut out = String::with_capacity(s.len());
345 for c in s.chars() {
346 match c {
347 '"' => out.push_str("\\\""),
348 '\\' => out.push_str("\\\\"),
349 _ => out.push(c),
350 }
351 }
352 out
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_basic_undirected() {
361 let input = b"*Vertices 3\n1 \"A\"\n2 \"B\"\n3 \"C\"\n*Edges\n1 2\n2 3\n1 3\n";
362 let result = read_pajek(&input[..]).unwrap();
363 assert_eq!(result.graph.vcount(), 3);
364 assert_eq!(result.graph.ecount(), 3);
365 assert!(!result.graph.is_directed());
366 let labels = result.labels.unwrap();
367 assert_eq!(labels, vec!["A", "B", "C"]);
368 }
369
370 #[test]
371 fn test_directed_arcs() {
372 let input = b"*Vertices 2\n*Arcs\n1 2\n2 1\n";
373 let result = read_pajek(&input[..]).unwrap();
374 assert!(result.graph.is_directed());
375 assert_eq!(result.graph.ecount(), 2);
376 }
377
378 #[test]
379 fn test_weighted_edges() {
380 let input = b"*Vertices 3\n*Edges\n1 2 1.5\n2 3 2.5\n";
381 let result = read_pajek(&input[..]).unwrap();
382 let w = result.weights.unwrap();
383 assert!((w[0] - 1.5).abs() < 1e-10);
384 assert!((w[1] - 2.5).abs() < 1e-10);
385 }
386
387 #[test]
388 fn test_no_labels() {
389 let input = b"*Vertices 2\n*Edges\n1 2\n";
390 let result = read_pajek(&input[..]).unwrap();
391 assert!(result.labels.is_none());
392 }
393
394 #[test]
395 fn test_no_weights() {
396 let input = b"*Vertices 2\n*Edges\n1 2\n";
397 let result = read_pajek(&input[..]).unwrap();
398 assert!(result.weights.is_none());
399 }
400
401 #[test]
402 fn test_mixed_weights() {
403 let input = b"*Vertices 3\n*Edges\n1 2 3.0\n2 3\n";
404 let result = read_pajek(&input[..]).unwrap();
405 let w = result.weights.unwrap();
406 assert!((w[0] - 3.0).abs() < 1e-10);
407 assert!((w[1] - 0.0).abs() < 1e-10);
408 }
409
410 #[test]
411 fn test_comment_lines_skipped() {
412 let input = b"% comment\n*Vertices 2\n% another\n*Edges\n1 2\n";
413 let result = read_pajek(&input[..]).unwrap();
414 assert_eq!(result.graph.vcount(), 2);
415 assert_eq!(result.graph.ecount(), 1);
416 }
417
418 #[test]
419 fn test_case_insensitive_sections() {
420 let input = b"*vertices 2\n*edges\n1 2\n";
421 let result = read_pajek(&input[..]).unwrap();
422 assert_eq!(result.graph.vcount(), 2);
423 }
424
425 #[test]
426 fn test_no_vertices_error() {
427 let input = b"*Edges\n1 2\n";
428 let result = read_pajek(&input[..]);
429 assert!(result.is_err());
430 }
431
432 #[test]
433 fn test_zero_vertex_id_error() {
434 let input = b"*Vertices 2\n*Edges\n0 1\n";
435 let result = read_pajek(&input[..]);
436 assert!(result.is_err());
437 }
438
439 #[test]
440 fn test_vertex_id_out_of_range_error() {
441 let input = b"*Vertices 2\n1 \"A\"\n5 \"E\"\n*Edges\n1 2\n";
442 let result = read_pajek(&input[..]);
443 assert!(result.is_err());
444 }
445
446 #[test]
447 fn test_self_loop() {
448 let input = b"*Vertices 2\n*Edges\n1 1\n";
449 let result = read_pajek(&input[..]).unwrap();
450 assert_eq!(result.graph.ecount(), 1);
451 }
452
453 #[test]
454 fn test_empty_graph() {
455 let input = b"*Vertices 5\n*Edges\n";
456 let result = read_pajek(&input[..]).unwrap();
457 assert_eq!(result.graph.vcount(), 5);
458 assert_eq!(result.graph.ecount(), 0);
459 }
460
461 #[test]
462 fn test_quoted_label_with_escape() {
463 let input = b"*Vertices 1\n1 \"hello \\\"world\\\"\"\n*Edges\n";
464 let result = read_pajek(&input[..]).unwrap();
465 let labels = result.labels.unwrap();
466 assert_eq!(labels[0], "hello \"world\"");
467 }
468
469 #[test]
470 fn test_write_undirected() {
471 let mut g = Graph::with_vertices(3);
472 g.add_edge(0, 1).unwrap();
473 g.add_edge(1, 2).unwrap();
474
475 let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
476 let mut buf = Vec::new();
477 write_pajek(&g, Some(&labels), None, &mut buf).unwrap();
478 let s = String::from_utf8(buf).unwrap();
479 assert!(s.contains("*Vertices 3"));
480 assert!(s.contains("1 \"A\""));
481 assert!(s.contains("2 \"B\""));
482 assert!(s.contains("3 \"C\""));
483 assert!(s.contains("*Edges"));
484 assert!(s.contains("1 2\n"));
485 assert!(s.contains("2 3\n"));
486 }
487
488 #[test]
489 fn test_write_directed() {
490 let mut g = Graph::new(2, true).unwrap();
491 g.add_edge(0, 1).unwrap();
492
493 let mut buf = Vec::new();
494 write_pajek(&g, None, None, &mut buf).unwrap();
495 let s = String::from_utf8(buf).unwrap();
496 assert!(s.contains("*Arcs"));
497 }
498
499 #[test]
500 fn test_write_weighted() {
501 let mut g = Graph::with_vertices(2);
502 g.add_edge(0, 1).unwrap();
503
504 let weights = vec![4.5];
505 let mut buf = Vec::new();
506 write_pajek(&g, None, Some(&weights), &mut buf).unwrap();
507 let s = String::from_utf8(buf).unwrap();
508 assert!(s.contains("1 2 4.5"));
509 }
510
511 #[test]
512 fn test_write_labels_mismatch_error() {
513 let g = Graph::with_vertices(3);
514 let labels = vec!["A".to_string()];
515 let mut buf = Vec::new();
516 assert!(write_pajek(&g, Some(&labels), None, &mut buf).is_err());
517 }
518
519 #[test]
520 fn test_write_weights_mismatch_error() {
521 let mut g = Graph::with_vertices(2);
522 g.add_edge(0, 1).unwrap();
523 let weights = vec![1.0, 2.0];
524 let mut buf = Vec::new();
525 assert!(write_pajek(&g, None, Some(&weights), &mut buf).is_err());
526 }
527
528 #[test]
529 fn test_round_trip() {
530 let input = b"*Vertices 4\n1 \"Alice\"\n2 \"Bob\"\n3 \"Carol\"\n4 \"Dave\"\n*Edges\n1 2 1.0\n2 3 2.0\n3 4 3.0\n";
531 let result = read_pajek(&input[..]).unwrap();
532
533 let mut buf = Vec::new();
534 write_pajek(
535 &result.graph,
536 result.labels.as_deref(),
537 result.weights.as_deref(),
538 &mut buf,
539 )
540 .unwrap();
541
542 let result2 = read_pajek(&buf[..]).unwrap();
543 assert_eq!(result2.graph.vcount(), result.graph.vcount());
544 assert_eq!(result2.graph.ecount(), result.graph.ecount());
545 assert_eq!(result2.labels, result.labels);
546 }
547
548 #[test]
549 fn test_blank_lines_skipped() {
550 let input = b"\n*Vertices 2\n\n1 \"X\"\n\n*Edges\n\n1 2\n\n";
551 let result = read_pajek(&input[..]).unwrap();
552 assert_eq!(result.graph.vcount(), 2);
553 assert_eq!(result.graph.ecount(), 1);
554 }
555}