rust_igraph/algorithms/io/
dot.rs1use std::io::Write;
24
25use crate::core::{Graph, IgraphError, IgraphResult};
26
27pub fn write_dot<W: Write>(
48 graph: &Graph,
49 labels: Option<&[String]>,
50 writer: &mut W,
51) -> IgraphResult<()> {
52 if let Some(l) = labels {
53 if l.len() != graph.vcount() as usize {
54 return Err(IgraphError::InvalidArgument(format!(
55 "labels length {} does not match vcount {}",
56 l.len(),
57 graph.vcount()
58 )));
59 }
60 }
61
62 let edge_op = if graph.is_directed() { "->" } else { "--" };
63 let graph_type = if graph.is_directed() {
64 "digraph"
65 } else {
66 "graph"
67 };
68
69 writeln!(writer, "{graph_type} {{")?;
70
71 let mut has_edge = vec![false; graph.vcount() as usize];
73
74 for eid in 0..graph.ecount() {
75 #[allow(clippy::cast_possible_truncation)]
76 let (src, tgt) = graph.edge(eid as u32)?;
77 has_edge[src as usize] = true;
78 has_edge[tgt as usize] = true;
79
80 let src_label = vertex_label(src, labels);
81 let tgt_label = vertex_label(tgt, labels);
82 writeln!(writer, " {src_label} {edge_op} {tgt_label};")?;
83 }
84
85 for v in 0..graph.vcount() {
87 if !has_edge[v as usize] {
88 let lbl = vertex_label(v, labels);
89 writeln!(writer, " {lbl};")?;
90 }
91 }
92
93 writeln!(writer, "}}")?;
94
95 Ok(())
96}
97
98fn vertex_label(v: u32, labels: Option<&[String]>) -> String {
99 match labels {
100 Some(l) => dot_escape(&l[v as usize]),
101 None => v.to_string(),
102 }
103}
104
105fn dot_escape(s: &str) -> String {
106 let is_simple = !s.is_empty()
108 && !s.as_bytes()[0].is_ascii_digit()
109 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
110
111 let reserved = matches!(
113 s.to_ascii_lowercase().as_str(),
114 "graph" | "digraph" | "node" | "edge" | "strict" | "subgraph"
115 );
116
117 if is_simple && !reserved {
118 s.to_owned()
119 } else {
120 let mut out = String::with_capacity(s.len() + 2);
122 out.push('"');
123 for c in s.chars() {
124 match c {
125 '"' => out.push_str("\\\""),
126 '\\' => out.push_str("\\\\"),
127 '\n' => out.push_str("\\n"),
128 _ => out.push(c),
129 }
130 }
131 out.push('"');
132 out
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_undirected_basic() {
142 let mut g = Graph::with_vertices(3);
143 g.add_edge(0, 1).unwrap();
144 g.add_edge(1, 2).unwrap();
145
146 let mut buf = Vec::new();
147 write_dot(&g, None, &mut buf).unwrap();
148 let s = String::from_utf8(buf).unwrap();
149 assert!(s.starts_with("graph {\n"));
150 assert!(s.contains("0 -- 1;"));
151 assert!(s.contains("1 -- 2;"));
152 assert!(s.ends_with("}\n"));
153 }
154
155 #[test]
156 fn test_directed_basic() {
157 let mut g = Graph::new(3, true).unwrap();
158 g.add_edge(0, 1).unwrap();
159 g.add_edge(1, 2).unwrap();
160
161 let mut buf = Vec::new();
162 write_dot(&g, None, &mut buf).unwrap();
163 let s = String::from_utf8(buf).unwrap();
164 assert!(s.starts_with("digraph {\n"));
165 assert!(s.contains("0 -> 1;"));
166 assert!(s.contains("1 -> 2;"));
167 }
168
169 #[test]
170 fn test_with_labels() {
171 let mut g = Graph::with_vertices(3);
172 g.add_edge(0, 1).unwrap();
173
174 let labels = vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()];
175 let mut buf = Vec::new();
176 write_dot(&g, Some(&labels), &mut buf).unwrap();
177 let s = String::from_utf8(buf).unwrap();
178 assert!(s.contains("Alice -- Bob;"));
179 assert!(s.contains("Carol;"));
181 }
182
183 #[test]
184 fn test_isolated_vertices() {
185 let g = Graph::with_vertices(3);
186
187 let mut buf = Vec::new();
188 write_dot(&g, None, &mut buf).unwrap();
189 let s = String::from_utf8(buf).unwrap();
190 assert!(s.contains(" 0;\n"));
191 assert!(s.contains(" 1;\n"));
192 assert!(s.contains(" 2;\n"));
193 }
194
195 #[test]
196 fn test_empty_graph() {
197 let g = Graph::with_vertices(0);
198
199 let mut buf = Vec::new();
200 write_dot(&g, None, &mut buf).unwrap();
201 let s = String::from_utf8(buf).unwrap();
202 assert_eq!(s, "graph {\n}\n");
203 }
204
205 #[test]
206 fn test_reserved_word_label_escaped() {
207 let mut g = Graph::with_vertices(2);
208 g.add_edge(0, 1).unwrap();
209
210 let labels = vec!["graph".to_string(), "node".to_string()];
211 let mut buf = Vec::new();
212 write_dot(&g, Some(&labels), &mut buf).unwrap();
213 let s = String::from_utf8(buf).unwrap();
214 assert!(s.contains("\"graph\" -- \"node\";"));
215 }
216
217 #[test]
218 fn test_label_with_spaces_escaped() {
219 let mut g = Graph::with_vertices(2);
220 g.add_edge(0, 1).unwrap();
221
222 let labels = vec!["hello world".to_string(), "foo bar".to_string()];
223 let mut buf = Vec::new();
224 write_dot(&g, Some(&labels), &mut buf).unwrap();
225 let s = String::from_utf8(buf).unwrap();
226 assert!(s.contains("\"hello world\" -- \"foo bar\";"));
227 }
228
229 #[test]
230 fn test_label_with_quotes_escaped() {
231 let mut g = Graph::with_vertices(2);
232 g.add_edge(0, 1).unwrap();
233
234 let labels = vec!["say \"hi\"".to_string(), "ok".to_string()];
235 let mut buf = Vec::new();
236 write_dot(&g, Some(&labels), &mut buf).unwrap();
237 let s = String::from_utf8(buf).unwrap();
238 assert!(s.contains("\"say \\\"hi\\\"\" -- ok;"));
239 }
240
241 #[test]
242 fn test_label_starting_with_digit() {
243 let mut g = Graph::with_vertices(2);
244 g.add_edge(0, 1).unwrap();
245
246 let labels = vec!["123abc".to_string(), "valid_name".to_string()];
247 let mut buf = Vec::new();
248 write_dot(&g, Some(&labels), &mut buf).unwrap();
249 let s = String::from_utf8(buf).unwrap();
250 assert!(s.contains("\"123abc\" -- valid_name;"));
251 }
252
253 #[test]
254 fn test_self_loop() {
255 let mut g = Graph::with_vertices(1);
256 g.add_edge(0, 0).unwrap();
257
258 let mut buf = Vec::new();
259 write_dot(&g, None, &mut buf).unwrap();
260 let s = String::from_utf8(buf).unwrap();
261 assert!(s.contains("0 -- 0;"));
262 }
263
264 #[test]
265 fn test_labels_mismatch_error() {
266 let g = Graph::with_vertices(3);
267 let labels = vec!["A".to_string()];
268 let mut buf = Vec::new();
269 assert!(write_dot(&g, Some(&labels), &mut buf).is_err());
270 }
271}