igraph/foreign.rs
1//! Reading and writing graphs in foreign file formats (`igraph_foreign.h`).
2//!
3//! igraph can exchange graphs with other software through a number of
4//! textual (and one binary) file formats. This module wraps all of them, in
5//! two flavours:
6//!
7//! - **file based**: `Graph::read_graph_*(path, ...)` and
8//! `graph.write_graph_*(path, ...)` take anything implementing
9//! [`AsRef<Path>`](std::path::Path) and open/close the file themselves
10//! (the file handle is always closed, also on errors, and write errors
11//! detected when flushing are reported);
12//! - **in memory**: `Graph::read_graph_*_from_str(&str, ...)` parses a string
13//! and `graph.write_graph_*_to_string(...)` returns the serialization as a
14//! [`String`], without touching the file system (they use the POSIX
15//! `fmemopen`/`open_memstream` streams under the hood). igraph copies
16//! string attributes byte by byte, so a `_to_string` writer replaces
17//! invalid UTF-8 (only possible in attributes read from files in another
18//! encoding, e.g. Latin-1 Pajek labels) with U+FFFD: use the file based
19//! writer to keep the exact bytes.
20//!
21//! On top of that, [`GraphFormat`] together with [`Graph::read_graph`] and
22//! [`Graph::write_graph`] offers a format-agnostic entry point, able to guess
23//! the format from the file extension ([`GraphFormat::from_path`]).
24//!
25//! # Example
26//!
27//! ```
28//! use igraph::{foreign::GmlWriteOptions, prelude::*};
29//!
30//! // A directed triangle with a pendant vertex, serialized as an edge list...
31//! let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3)], 4, true).unwrap();
32//! let text = g.write_graph_edgelist_to_string().unwrap();
33//! assert_eq!(text, "0 1\n1 2\n2 0\n2 3\n");
34//!
35//! // ...and parsed back: exactly the same graph.
36//! let h = Graph::read_graph_edgelist_from_str(&text, 0, true).unwrap();
37//! assert!(g.is_same_graph(&h).unwrap());
38//!
39//! // GML round trip of Zachary's karate club, through a temporary file.
40//! let karate = Graph::famous("Zachary").unwrap();
41//! let path = std::env::temp_dir().join(format!("igraph-doc-foreign-{}.gml", std::process::id()));
42//! karate.write_graph_gml(&path, &GmlWriteOptions::default()).unwrap();
43//! let k = Graph::read_graph_gml(&path).unwrap();
44//! std::fs::remove_file(&path).unwrap();
45//! assert!(!k.is_directed());
46//! assert_eq!(k, karate); // GML keeps vertex ids and edges
47//! ```
48//!
49//! # Supported formats
50//!
51//! | Format | Read | Write | Attributes (after [`attributes::enable`](crate::attributes::enable)) | Notes |
52//! |---|---|---|---|---|
53//! | Edge list | [`read_graph_edgelist`](Graph::read_graph_edgelist) | [`write_graph_edgelist`](Graph::write_graph_edgelist) | none | whitespace separated 0-based vertex ids |
54//! | NCOL | [`read_graph_ncol`](Graph::read_graph_ncol) | [`write_graph_ncol`](Graph::write_graph_ncol) | vertex `name`, edge `weight` | symbolic (named) weighted edge list of the LGL software |
55//! | LGL | [`read_graph_lgl`](Graph::read_graph_lgl) | [`write_graph_lgl`](Graph::write_graph_lgl) | vertex `name`, edge `weight` | adjacency-list-like, `# vertex` headers |
56//! | Pajek | [`read_graph_pajek`](Graph::read_graph_pajek) | [`write_graph_pajek`](Graph::write_graph_pajek) | `name`, `x`/`y`/`z`, `color`, bipartite `type`, edge `weight`, ... | `.net` files, 1-based ids |
57//! | GraphML | [`read_graph_graphml`](Graph::read_graph_graphml) | [`write_graph_graphml`](Graph::write_graph_graphml), [`write_graph_graphml_with`](Graph::write_graph_graphml_with) | all (typed, with defaults), node ids as vertex `id` | XML; reading requires igraph built with libxml2; the in-memory writer [`write_graph_graphml_to_string`](Graph::write_graph_graphml_to_string) takes the `prefixattr` flag of `write_graph_graphml_with` |
58//! | GML | [`read_graph_gml`](Graph::read_graph_gml) | [`write_graph_gml`](Graph::write_graph_gml) | numeric and string ones, numeric vertex `id` | see [`GmlWriteOptions`] |
59//! | DIMACS flow | [`read_graph_dimacs_flow`](Graph::read_graph_dimacs_flow) | [`write_graph_dimacs_flow`](Graph::write_graph_dimacs_flow) | none (capacities in [`DimacsFlow`]) | max-flow / edge problems |
60//! | graph database | [`read_graph_graphdb`](Graph::read_graph_graphdb) | — | none | binary, see [`read_graph_graphdb_from_bytes`](Graph::read_graph_graphdb_from_bytes) |
61//! | UCINET DL | [`read_graph_dl`](Graph::read_graph_dl) | — | vertex `name`, edge `weight` | full matrix, edge list and node list forms |
62//! | Graphviz DOT | — | [`write_graph_dot`](Graph::write_graph_dot) | all | output only |
63//! | LEDA | — | [`write_graph_leda`](Graph::write_graph_leda) | one vertex and one edge attribute | output only |
64//!
65//! Every file based function has an in-memory twin with the `_from_str` /
66//! `_to_string` suffix (`_from_bytes` for the binary graph database format),
67//! taking the same arguments but the path. The one exception is GraphML:
68//! [`write_graph_graphml_to_string(prefixattr)`](Graph::write_graph_graphml_to_string)
69//! is the twin of
70//! [`write_graph_graphml_with(path, prefixattr)`](Graph::write_graph_graphml_with)
71//! (pass `false` for the behaviour of
72//! [`write_graph_graphml(path)`](Graph::write_graph_graphml)).
73//! [`SafeLocale`] and [`with_safe_locale`] bind igraph's locale helpers.
74//!
75//! # Attributes
76//!
77//! Many formats carry vertex names, edge weights and other attributes. igraph
78//! stores them through a pluggable, **process-wide** *attribute handler*,
79//! which is off by default and is turned on by
80//! [`attributes::enable`](crate::attributes::enable) (see the
81//! [`attributes`](crate::attributes) module for the typed accessors):
82//!
83//! - **without** the handler, readers parse attributes (validating their
84//! syntax) but discard them: only the structure of the graph is returned.
85//! The options asking to store names or weights ([`NcolLglOptions`]) are
86//! accepted and harmless. Writers see no attribute: those asked to export a
87//! named one (e.g. the `names`/`weights` arguments of
88//! [`write_graph_ncol`](Graph::write_graph_ncol)) emit an igraph *warning*
89//! (see [`take_warnings`](crate::error::take_warnings)) and fall back to
90//! plain vertex ids, and GraphML/GML/DOT files are written without
91//! attributes;
92//! - **with** the handler (call [`enable`](crate::attributes::enable)
93//! *before* reading), readers store what they find in the file as graph,
94//! vertex and edge attributes (the "Attributes" column above), and the
95//! writers export the attributes of the graph. Values missing for some
96//! elements are NaN for numeric attributes, `""` for strings and `false`
97//! for booleans, unless the format has its own defaults (a missing
98//! NCOL/LGL weight is 1, a GraphML `<key>` may declare a `<default>`,
99//! Pajek parameters take Pajek's defaults, see the readers); GraphML and
100//! GML omit NaN values when writing.
101//!
102//! What each format keeps, with the handler on (the individual readers and
103//! writers have the details):
104//!
105//! | Format | Read into attributes | Written from attributes |
106//! |---|---|---|
107//! | Edge list, DIMACS, graph database | nothing | nothing |
108//! | NCOL, LGL | vertex `name` (string), edge `weight` (numeric), as selected by [`NcolLglOptions`] | the vertex / edge attributes named by the `names` / `weights` arguments |
109//! | UCINET DL | vertex `name` (labels), edge `weight` (values) | — |
110//! | Pajek | vertex `name`, `x`/`y`/`z`, `color`, shapes and the other Pajek parameters, bipartite `type` (boolean), edge `weight` and edge parameters | the attributes named after Pajek parameters; other attributes are ignored |
111//! | GraphML | every `<key>`, typed (boolean, numeric, string), for graph, vertices and edges; the node `id`s as the string vertex attribute `id` | every graph, vertex and edge attribute (typed keys), optionally prefixed (`g_`/`v_`/`e_`, [`write_graph_graphml_with`](Graph::write_graph_graphml_with)) |
112//! | GML | every numeric or string field of the graph, node and edge records, including the numeric node `id` | numeric and string attributes (booleans as 0/1); a numeric vertex `id` supplies the node ids |
113//! | DOT | — | every graph, vertex and edge attribute |
114//! | LEDA | — | one vertex and one edge attribute, named in the call |
115//!
116//! Two GraphML caveats: the node ids land in a string `id` vertex
117//! attribute (unless a vertex key already defines an attribute named `id`), which the
118//! GraphML writer then exports as an ordinary `<key>`, so a second round
119//! trip carries it as data; and when the `<edge>` elements have `id`s, igraph
120//! 1.0.0 and 1.0.1 also create a string `id` *edge* attribute but, because
121//! of a bug in `src/io/graphml.c`, fill it with the *node* ids (in node
122//! order, padded with `""` if there are more edges than nodes): don't rely on it
123//! (delete it with
124//! [`remove_edge_attr`](Graph::remove_edge_attr) if it gets in the way). See
125//! [`read_graph_graphml`](Graph::read_graph_graphml).
126//!
127//! ```
128//! use igraph::{attributes, foreign::NcolLglOptions, prelude::*};
129//!
130//! attributes::enable().unwrap(); // before reading!
131//! let text = "rome paris 1420\nparis london 460\nlondon rome\n";
132//! let g = Graph::read_graph_ncol_from_str(text, &[], &NcolLglOptions::default()).unwrap();
133//! assert_eq!(g.vertex_attr_str_values("name", ..).unwrap(), ["rome", "paris", "london"]);
134//! // A missing weight defaults to 1 when at least one edge has an explicit weight.
135//! assert_eq!(g.edge_attr_numeric_values("weight", ..).unwrap(), [1420.0, 460.0, 1.0]);
136//! // Writers use the attributes they are asked for.
137//! let out = g.write_graph_ncol_to_string(Some("name"), Some("weight")).unwrap();
138//! assert_eq!(out, "rome paris 1420\nparis london 460\nrome london 1\n");
139//! ```
140//!
141//! Data that igraph returns through explicit output arguments does not need
142//! the handler, e.g. the capacities and source/target vertices of a DIMACS
143//! file, returned in [`DimacsFlow`]. When only the vertex naming of an NCOL
144//! file matters, the `predefnames` argument of
145//! [`read_graph_ncol`](Graph::read_graph_ncol) fixes it without attributes:
146//! vertex `i` then is the `i`-th predefined name.
147//!
148//! # Locale and threads
149//!
150//! The parsers and writers assume that the C locale uses a decimal *point*.
151//! Rust programs start in the `"C"` locale, so nothing needs to be done unless
152//! some library called `setlocale`; in that case wrap the I/O with
153//! [`SafeLocale`] or [`with_safe_locale`].
154//!
155//! All the functions can be called from several threads at once (but see
156//! [`SafeLocale`] for platforms without per-thread locales). The GML
157//! reader of igraph 1.0.0 and 1.0.1 is not reentrant (it uses static
158//! buffers), and so is the default GML `Creator` line (it uses `ctime`): these
159//! wrappers serialize them internally with a process-wide lock.
160//!
161//! # See also
162//!
163//! - [`Graph::from_edges`] and [`Graph::famous`] (in
164//! [`constructors`](crate::constructors)) to build graphs in code;
165//! - [`Graph::get_adjacency`] and the rest of the
166//! [`conversion`](crate::conversion) module to export graphs as matrices;
167//! - [`Graph::is_same_graph`] to check that a round trip kept the vertex ids,
168//! [`Graph::isomorphic`] when a format (NCOL, LGL, bipartite Pajek)
169//! relabels the vertices;
170//! - [`Graph::maxflow`] and [`Graph::st_mincut`] to solve the problems read
171//! from DIMACS files.
172//!
173//! The C documentation of the whole chapter is at
174//! <https://igraph.org/c/html/latest/igraph-Foreign.html>.
175
176use crate::{
177 constants::AddWeights,
178 error::{Error, ErrorKind, Result},
179 ffi::*,
180 graph::{Graph, VertexId},
181 igraph_call,
182 strvector::StrVector,
183 vector::{Vector, VectorInt},
184};
185use std::{
186 ffi::{CStr, CString, c_char, c_void},
187 marker::PhantomData,
188 path::Path,
189 sync::Mutex,
190};
191
192/// Serializes the non-reentrant parts of igraph's GML code (still present in
193/// igraph 1.0.0 and 1.0.1):
194///
195/// - `igraph_read_graph_gml` keeps intermediate strings in function-local
196/// `static` buffers (`strid` and `igraph_i_gml_tostring` in
197/// `src/io/gml.c`), so concurrent calls from different threads corrupt each
198/// other (and may crash);
199/// - `igraph_write_graph_gml` with a `NULL` creator formats the current time
200/// with `ctime`, which returns (and igraph then modifies in place) a
201/// process-wide static buffer.
202///
203/// All the other readers and writers are reentrant.
204static GML_LOCK: Mutex<()> = Mutex::new(());
205
206/// Acquires [`GML_LOCK`].
207fn gml_lock() -> std::sync::MutexGuard<'static, ()> {
208 // A poisoned lock only means another thread panicked: the C state is fine.
209 GML_LOCK.lock().unwrap_or_else(|e| e.into_inner())
210}
211
212/// Runs the GML reader while holding [`GML_LOCK`].
213fn read_gml(source: Source<'_>) -> Result<Graph> {
214 let _guard = gml_lock();
215 read_with(source, |g, f| unsafe { igraph_read_graph_gml(g, f) })
216}
217
218/// Converts a Rust count/index into an `igraph_int_t`, rejecting values that
219/// would wrap around to negative numbers.
220fn to_int(value: usize, what: &str) -> Result<igraph_int_t> {
221 igraph_int_t::try_from(value)
222 .map_err(|_| Error::invalid(format!("{what} {value} is too large")))
223}
224
225/// Encodes `&` (unless `only_quot`) and `"` as XML entities, as igraph does
226/// for GML string values.
227fn gml_entity_encode(s: &str, only_quot: bool) -> String {
228 let mut out = String::with_capacity(s.len());
229 for c in s.chars() {
230 match c {
231 '&' if !only_quot => out.push_str("&"),
232 '"' => out.push_str("""),
233 c => out.push(c),
234 }
235 }
236 out
237}
238
239// ---------------------------------------------------------------------------
240// C stream plumbing
241// ---------------------------------------------------------------------------
242
243/// Converts a path into a NUL terminated C string.
244fn path_to_cstring(path: &Path) -> Result<CString> {
245 #[cfg(unix)]
246 let bytes = {
247 use std::os::unix::ffi::OsStrExt;
248 path.as_os_str().as_bytes().to_vec()
249 };
250 #[cfg(not(unix))]
251 let bytes = path
252 .to_str()
253 .ok_or_else(|| Error::invalid(format!("path {} is not valid UTF-8", path.display())))?
254 .as_bytes()
255 .to_vec();
256 CString::new(bytes).map_err(|_| {
257 Error::invalid(format!(
258 "path {} contains an interior NUL byte",
259 path.display()
260 ))
261 })
262}
263
264/// Converts an optional attribute name into an optional C string.
265fn opt_cstring(name: Option<&str>, what: &str) -> Result<Option<CString>> {
266 name.map(|s| {
267 CString::new(s).map_err(|_| Error::invalid(format!("{what} contains an interior NUL byte")))
268 })
269 .transpose()
270}
271
272/// Pointer of an optional C string (`NULL` for `None`).
273fn opt_ptr(s: &Option<CString>) -> *const c_char {
274 s.as_ref().map_or(std::ptr::null(), |c| c.as_ptr())
275}
276
277/// An owned C `FILE *`, closed on drop. The lifetime ties it to the memory
278/// buffer it may read from (for `fmemopen` streams).
279struct CFile<'a> {
280 ptr: *mut FILE,
281 _buffer: PhantomData<&'a [u8]>,
282}
283
284impl<'a> CFile<'a> {
285 /// Opens a file with `fopen`.
286 fn open(path: &Path, mode: &CStr) -> Result<CFile<'static>> {
287 let c_path = path_to_cstring(path)?;
288 let ptr = unsafe { fopen(c_path.as_ptr(), mode.as_ptr()) };
289 if ptr.is_null() {
290 let os = std::io::Error::last_os_error();
291 return Err(Error::new(
292 ErrorKind::File,
293 format!("cannot open {}: {os}", path.display()),
294 ));
295 }
296 Ok(CFile {
297 ptr,
298 _buffer: PhantomData,
299 })
300 }
301
302 /// Opens a read-only stream over an in-memory buffer (`fmemopen`).
303 fn from_bytes(data: &'a [u8]) -> Result<CFile<'a>> {
304 let ptr = if data.is_empty() {
305 // `fmemopen` may reject zero-sized buffers: an empty temporary
306 // file is an equivalent empty stream.
307 unsafe { tmpfile() }
308 } else {
309 // In read mode the buffer is never written to.
310 unsafe { fmemopen(data.as_ptr() as *mut c_void, data.len(), c"rb".as_ptr()) }
311 };
312 if ptr.is_null() {
313 let os = std::io::Error::last_os_error();
314 return Err(Error::new(
315 ErrorKind::File,
316 format!("cannot open a memory stream: {os}"),
317 ));
318 }
319 Ok(CFile {
320 ptr,
321 _buffer: PhantomData,
322 })
323 }
324
325 /// Closes the stream with `fclose`, reporting failures: earlier write
326 /// errors that igraph did not check (its error indicator is set) and
327 /// buffered writes that could not be completed.
328 fn close(mut self) -> Result<()> {
329 let ptr = std::mem::replace(&mut self.ptr, std::ptr::null_mut());
330 let had_error = unsafe { ferror(ptr) } != 0;
331 if unsafe { fclose(ptr) } != 0 || had_error {
332 let os = std::io::Error::last_os_error();
333 return Err(Error::new(
334 ErrorKind::File,
335 format!("cannot close the stream: {os}"),
336 ));
337 }
338 Ok(())
339 }
340}
341
342impl Drop for CFile<'_> {
343 fn drop(&mut self) {
344 if !self.ptr.is_null() {
345 unsafe { fclose(self.ptr) };
346 }
347 }
348}
349
350/// A growable in-memory output stream (`open_memstream`).
351struct MemSink {
352 file: *mut FILE,
353 /// Heap cell (from `Box::into_raw`) whose two fields `open_memstream`
354 /// updates with the buffer address and size. It is only accessed through
355 /// this raw pointer, so the pointers held by the C library stay valid
356 /// until `Drop` reclaims it.
357 loc: *mut (*mut c_char, usize),
358}
359
360impl MemSink {
361 fn new() -> Result<Self> {
362 let loc = Box::into_raw(Box::new((std::ptr::null_mut::<c_char>(), 0usize)));
363 // SAFETY: `loc` is a valid, uniquely owned allocation.
364 let file = unsafe { open_memstream(&raw mut (*loc).0, &raw mut (*loc).1) };
365 if file.is_null() {
366 let os = std::io::Error::last_os_error();
367 // SAFETY: the stream was not created, nobody else points to `loc`.
368 drop(unsafe { Box::from_raw(loc) });
369 return Err(Error::new(
370 ErrorKind::File,
371 format!("cannot open a memory stream: {os}"),
372 ));
373 }
374 Ok(Self { file, loc })
375 }
376
377 /// Closes the stream and returns what was written.
378 ///
379 /// Bytes that are not valid UTF-8 (possible only in string attributes
380 /// that igraph read from non-UTF-8 files) are replaced by U+FFFD.
381 fn finish(mut self) -> Result<String> {
382 let file = std::mem::replace(&mut self.file, std::ptr::null_mut());
383 let had_error = unsafe { ferror(file) } != 0;
384 if unsafe { fclose(file) } != 0 || had_error {
385 return Err(Error::new(
386 ErrorKind::File,
387 "cannot finalize the memory stream",
388 ));
389 }
390 // SAFETY: `fclose` stored the final buffer address and size.
391 let (buf, size) = unsafe { *self.loc };
392 if buf.is_null() {
393 return Ok(String::new());
394 }
395 let bytes = unsafe { std::slice::from_raw_parts(buf as *const u8, size) };
396 Ok(String::from_utf8_lossy(bytes).into_owned())
397 // `self` is dropped here, freeing the buffer.
398 }
399}
400
401impl Drop for MemSink {
402 fn drop(&mut self) {
403 if !self.file.is_null() {
404 unsafe { fclose(self.file) };
405 }
406 // SAFETY: the stream is closed, so the C library no longer uses
407 // `loc`; the buffer it holds was allocated by the C library and must
408 // be freed with `free` (`free(NULL)` is a no-op).
409 let loc = unsafe { Box::from_raw(self.loc) };
410 unsafe { free(loc.0 as *mut c_void) };
411 }
412}
413
414unsafe extern "C" {
415 /// `ferror` from `<stdio.h>` (not in the generated bindings): whether
416 /// the error indicator of the stream is set.
417 fn ferror(stream: *mut FILE) -> std::ffi::c_int;
418}
419
420/// Where a reader takes its input from.
421enum Source<'a> {
422 Path(&'a Path),
423 Bytes(&'a [u8]),
424}
425
426impl<'a> Source<'a> {
427 fn open(&self) -> Result<CFile<'a>> {
428 match *self {
429 Source::Path(p) => CFile::open(p, c"rb"),
430 Source::Bytes(b) => CFile::from_bytes(b),
431 }
432 }
433}
434
435/// Runs a reader (a C function initializing a graph from a stream).
436fn read_with(
437 source: Source<'_>,
438 f: impl FnOnce(*mut igraph_t, *mut FILE) -> igraph_error_t,
439) -> Result<Graph> {
440 let file = source.open()?;
441 let graph = Graph::init_with(|g| f(g, file.ptr))?;
442 // Closing a read stream can't lose data: ignore its outcome.
443 let _ = file.close();
444 Ok(graph)
445}
446
447/// Runs a writer on a newly created (truncated) file.
448fn write_to_path(path: &Path, f: impl FnOnce(*mut FILE) -> igraph_error_t) -> Result<()> {
449 let file = CFile::open(path, c"wb")?;
450 igraph_call!(f(file.ptr))?;
451 file.close()
452}
453
454/// Runs a writer on an in-memory stream and returns the output.
455fn write_to_string(f: impl FnOnce(*mut FILE) -> igraph_error_t) -> Result<String> {
456 let sink = MemSink::new()?;
457 igraph_call!(f(sink.file))?;
458 sink.finish()
459}
460
461// ---------------------------------------------------------------------------
462// Options and results
463// ---------------------------------------------------------------------------
464
465/// Options of the NCOL and LGL readers ([`Graph::read_graph_ncol`],
466/// [`Graph::read_graph_lgl`]).
467///
468/// The defaults are: store names, add weights only if present in the file,
469/// undirected graph (the LGL software only handles undirected graphs).
470///
471/// `names` and `weights` only matter once the attribute handler is enabled
472/// with [`attributes::enable`](crate::attributes::enable) (see the
473/// [module docs](self#attributes)); without it they are harmless.
474///
475/// # Examples
476/// ```
477/// use igraph::{foreign::NcolLglOptions, prelude::*};
478/// let opts = NcolLglOptions::default().with_directed(true).with_weights(AddWeights::No);
479/// assert!(opts.names && opts.directed);
480/// let g = Graph::read_graph_ncol_from_str("a b 2\nb c 3\n", &[], &opts).unwrap();
481/// assert!(g.is_directed());
482/// assert_eq!(g.edge_list(), vec![(0, 1), (1, 2)]);
483/// ```
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub struct NcolLglOptions {
486 /// Whether to store the symbolic vertex names as the `name` string vertex
487 /// attribute.
488 pub names: bool,
489 /// Whether to store the edge weights as the `weight` numeric edge
490 /// attribute: [`AddWeights::Yes`] always (edges without a weight get 1),
491 /// [`AddWeights::IfPresent`] only if at least one weight is given in the
492 /// file (again with 1 for the others), [`AddWeights::No`] never.
493 pub weights: AddWeights,
494 /// Whether to create a directed graph: the formats carry no information
495 /// about directedness.
496 pub directed: bool,
497}
498
499impl Default for NcolLglOptions {
500 fn default() -> Self {
501 Self {
502 names: true,
503 weights: AddWeights::IfPresent,
504 directed: false,
505 }
506 }
507}
508
509impl NcolLglOptions {
510 /// Sets [`directed`](Self::directed).
511 pub fn with_directed(mut self, directed: bool) -> Self {
512 self.directed = directed;
513 self
514 }
515
516 /// Sets [`names`](Self::names).
517 pub fn with_names(mut self, names: bool) -> Self {
518 self.names = names;
519 self
520 }
521
522 /// Sets [`weights`](Self::weights).
523 pub fn with_weights(mut self, weights: AddWeights) -> Self {
524 self.weights = weights;
525 self
526 }
527}
528
529/// Options of the GML writer ([`Graph::write_graph_gml`]).
530///
531/// The default writes igraph's own vertex ids (or the numeric `id` vertex
532/// attribute, if present), encodes all special characters as entities, and
533/// writes a `Creator` line mentioning the igraph version and the current date
534/// and time (use [`with_creator`](Self::with_creator)`("")` for reproducible
535/// output).
536///
537/// # Examples
538/// ```
539/// use igraph::{foreign::GmlWriteOptions, prelude::*};
540/// let g = Graph::from_edges(&[(0, 1)], 2, true).unwrap();
541/// let opts = GmlWriteOptions::default().with_creator("").with_ids(&[7.0, 9.0]);
542/// let gml = g.write_graph_gml_to_string(&opts).unwrap();
543/// let lines: Vec<&str> = gml.lines().map(str::trim).collect();
544/// assert_eq!(
545/// lines,
546/// [
547/// "Version 1", "graph", "[", "directed 1",
548/// "node", "[", "id 7", "]",
549/// "node", "[", "id 9", "]",
550/// "edge", "[", "source 7", "target 9", "]",
551/// "]",
552/// ]
553/// );
554/// ```
555#[derive(Debug, Clone, Copy, PartialEq, Default)]
556pub struct GmlWriteOptions<'a> {
557 /// Encode only `"` characters as entities, nothing else
558 /// (`IGRAPH_WRITE_GML_ENCODE_ONLY_QUOT_SW`); useful to re-export files
559 /// whose entities igraph passed through undecoded.
560 pub encode_only_quot: bool,
561 /// Numeric vertex ids to write in the `id` fields instead of igraph's
562 /// vertex ids (one per vertex). With `None`, a numeric `id` vertex
563 /// attribute is used if there is one (the GML reader creates it when the
564 /// [attribute handler](crate::attributes::enable) is on, so a GML round
565 /// trip keeps the original ids), otherwise igraph's vertex ids. If some
566 /// value is not an integer (or is NaN/infinite), or some value is
567 /// repeated, igraph emits a warning (see
568 /// [`take_warnings`](crate::error::take_warnings)) and ignores all of
569 /// them, writing its own vertex ids instead.
570 pub ids: Option<&'a [f64]>,
571 /// Text of the `Creator` line: `None` writes the igraph version plus the
572 /// current date and time, `Some("")` omits the line, anything else is
573 /// written as a GML string, with `"` (and `&`, unless
574 /// [`encode_only_quot`](Self::encode_only_quot)) encoded as XML entities.
575 pub creator: Option<&'a str>,
576}
577
578impl<'a> GmlWriteOptions<'a> {
579 /// Sets [`encode_only_quot`](Self::encode_only_quot).
580 pub fn with_encode_only_quot(mut self, only_quot: bool) -> Self {
581 self.encode_only_quot = only_quot;
582 self
583 }
584
585 /// Sets [`ids`](Self::ids).
586 pub fn with_ids(mut self, ids: &'a [f64]) -> Self {
587 self.ids = Some(ids);
588 self
589 }
590
591 /// Sets [`creator`](Self::creator).
592 pub fn with_creator(mut self, creator: &'a str) -> Self {
593 self.creator = Some(creator);
594 self
595 }
596}
597
598/// The problem described by a DIMACS file, see [`DimacsFlow`].
599#[derive(Debug, Clone, PartialEq)]
600pub enum DimacsProblem {
601 /// A maximum flow problem (`p max`).
602 Max {
603 /// The source vertex (0-based igraph id; DIMACS ids start from 1),
604 /// `None` if the file has no `n <id> s` line.
605 source: Option<VertexId>,
606 /// The target vertex (0-based igraph id), `None` if the file has no
607 /// `n <id> t` line.
608 target: Option<VertexId>,
609 /// The capacity of each edge, in edge id order.
610 capacity: Vec<f64>,
611 },
612 /// An "edge" problem (`p edge`), e.g. a graph coloring instance (see
613 /// [`Graph::vertex_coloring_greedy`] to solve one).
614 Edge {
615 /// The integer label of each vertex: its 1-based DIMACS index (igraph
616 /// 1.0.0 and 1.0.1 cannot parse the `n` lines that would change it).
617 labels: Vec<i64>,
618 },
619}
620
621/// The content of a DIMACS flow file, as returned by
622/// [`Graph::read_graph_dimacs_flow`].
623///
624/// For a [`DimacsProblem::Max`] instance, pass the capacities to
625/// [`Graph::maxflow`], [`Graph::maxflow_value`] or [`Graph::st_mincut`] to
626/// solve it.
627#[derive(Debug, Clone, PartialEq)]
628pub struct DimacsFlow {
629 /// The graph.
630 pub graph: Graph,
631 /// The problem type string of the `p` line (`"max"` or `"edge"`).
632 pub problem_name: String,
633 /// The problem data.
634 pub problem: DimacsProblem,
635}
636
637/// Graph file formats known to igraph, for the format-agnostic
638/// [`Graph::read_graph`] and [`Graph::write_graph`].
639///
640/// The variants list the file extensions recognized by
641/// [`from_path`](Self::from_path); [`can_read`](Self::can_read) and
642/// [`can_write`](Self::can_write) tell which direction the generic functions
643/// support.
644#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
645pub enum GraphFormat {
646 /// Plain edge list of 0-based vertex ids (`.txt`, `.edgelist`, `.edges`, `.el`).
647 Edgelist,
648 /// NCOL symbolic edge list (`.ncol`).
649 Ncol,
650 /// LGL format (`.lgl`).
651 Lgl,
652 /// Pajek (`.net`, `.pajek`).
653 Pajek,
654 /// GraphML (`.graphml`, `.xml`).
655 GraphMl,
656 /// GML (`.gml`).
657 Gml,
658 /// DIMACS flow format (`.dimacs`, `.max`); only [`Graph::read_graph`]
659 /// supports it generically (it discards the problem data).
660 DimacsFlow,
661 /// Binary ARG graph database format (`.graphdb`, and the `.A00`...`.A99`,
662 /// `.B00`...`.B99` suffixes of the database's graph pairs); read only.
663 GraphDb,
664 /// UCINET DL (`.dl`); read only.
665 Dl,
666 /// Graphviz DOT (`.dot`, `.gv`); write only.
667 Dot,
668 /// LEDA native format (`.gw`, `.leda`); write only.
669 Leda,
670}
671
672impl GraphFormat {
673 /// Guesses the format from the extension of `path` (case insensitive),
674 /// or `None` if the extension is unknown.
675 ///
676 /// ```
677 /// use igraph::foreign::GraphFormat;
678 /// assert_eq!(GraphFormat::from_path("karate.GML"), Some(GraphFormat::Gml));
679 /// assert_eq!(GraphFormat::from_path("/tmp/g.net"), Some(GraphFormat::Pajek));
680 /// assert_eq!(GraphFormat::from_path("noext"), None);
681 /// ```
682 pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
683 let ext = path.as_ref().extension()?.to_str()?.to_ascii_lowercase();
684 Some(match ext.as_str() {
685 "txt" | "edgelist" | "edges" | "el" => Self::Edgelist,
686 "ncol" => Self::Ncol,
687 "lgl" => Self::Lgl,
688 "net" | "pajek" => Self::Pajek,
689 "graphml" | "xml" => Self::GraphMl,
690 "gml" => Self::Gml,
691 "dimacs" | "max" => Self::DimacsFlow,
692 "graphdb" => Self::GraphDb,
693 e if e.len() == 3
694 && (e.starts_with('a') || e.starts_with('b'))
695 && e[1..].chars().all(|c| c.is_ascii_digit()) =>
696 {
697 Self::GraphDb
698 }
699 "dl" => Self::Dl,
700 "dot" | "gv" => Self::Dot,
701 "gw" | "leda" => Self::Leda,
702 _ => return None,
703 })
704 }
705
706 /// Whether igraph can read this format.
707 pub fn can_read(self) -> bool {
708 !matches!(self, Self::Dot | Self::Leda)
709 }
710
711 /// Whether igraph can write this format (DIMACS needs extra data: use
712 /// [`Graph::write_graph_dimacs_flow`] directly).
713 pub fn can_write(self) -> bool {
714 !matches!(self, Self::GraphDb | Self::Dl | Self::DimacsFlow)
715 }
716}
717
718// ---------------------------------------------------------------------------
719// Readers
720// ---------------------------------------------------------------------------
721
722impl igraph_t {
723 /// Reads a graph from a file in the given `format`, with default options.
724 ///
725 /// `directed` is used by the formats that don't encode directedness
726 /// (edge list, NCOL, LGL, DIMACS, graph database, DL); Pajek, GraphML and
727 /// GML files specify it themselves. Use [`GraphFormat::from_path`] to
728 /// guess the format from the extension. The defaults are: no extra
729 /// isolated vertices for edge lists, [`NcolLglOptions::default`] (plus
730 /// `directed`) for NCOL/LGL, the first graph of a GraphML document; the
731 /// problem data of a DIMACS file is discarded (use
732 /// [`read_graph_dimacs_flow`](Self::read_graph_dimacs_flow) to get it).
733 ///
734 /// # Errors
735 /// [`ErrorKind::Unimplemented`] if the format can't be read (DOT, LEDA),
736 /// plus the errors of the specific reader.
737 ///
738 /// # Examples
739 /// ```
740 /// use igraph::{foreign::GraphFormat, prelude::*};
741 /// let path = std::env::temp_dir().join(format!("igraph-doc-read-{}.net", std::process::id()));
742 /// std::fs::write(&path, "*Vertices 3\n*Edges\n1 2\n2 3\n").unwrap();
743 /// let format = GraphFormat::from_path(&path).unwrap();
744 /// let g = Graph::read_graph(&path, format, false).unwrap();
745 /// std::fs::remove_file(&path).unwrap();
746 /// assert_eq!(g.edge_list(), vec![(0, 1), (1, 2)]);
747 /// ```
748 pub fn read_graph(
749 path: impl AsRef<Path>,
750 format: GraphFormat,
751 directed: bool,
752 ) -> Result<Graph> {
753 let path = path.as_ref();
754 match format {
755 GraphFormat::Edgelist => Self::read_graph_edgelist(path, 0, directed),
756 GraphFormat::Ncol => Self::read_graph_ncol(
757 path,
758 &[],
759 &NcolLglOptions::default().with_directed(directed),
760 ),
761 GraphFormat::Lgl => {
762 Self::read_graph_lgl(path, &NcolLglOptions::default().with_directed(directed))
763 }
764 GraphFormat::Pajek => Self::read_graph_pajek(path),
765 GraphFormat::GraphMl => Self::read_graph_graphml(path, 0),
766 GraphFormat::Gml => Self::read_graph_gml(path),
767 GraphFormat::DimacsFlow => Ok(Self::read_graph_dimacs_flow(path, directed)?.graph),
768 GraphFormat::GraphDb => Self::read_graph_graphdb(path, directed),
769 GraphFormat::Dl => Self::read_graph_dl(path, directed),
770 GraphFormat::Dot | GraphFormat::Leda => Err(Error::new(
771 ErrorKind::Unimplemented,
772 format!("igraph cannot read the {format:?} format"),
773 )),
774 }
775 }
776
777 /// Reads an edge list file: an even number of non-negative integers
778 /// (0-based vertex ids) separated by whitespace, conventionally one
779 /// `from to` pair per line.
780 ///
781 /// The graph has `max(n, largest id + 1)` vertices, so `n = 0` is always
782 /// safe; a larger `n` adds isolated vertices. See
783 /// [`read_graph_ncol`](Self::read_graph_ncol) for files with symbolic
784 /// vertex names. Time complexity: O(|V| + |E|).
785 ///
786 /// Binds [`igraph_read_graph_edgelist`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_edgelist).
787 ///
788 /// # Errors
789 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
790 /// for syntax errors (non-integers, odd number of ids...).
791 pub fn read_graph_edgelist(path: impl AsRef<Path>, n: usize, directed: bool) -> Result<Graph> {
792 let n = to_int(n, "vertex count")?;
793 read_with(Source::Path(path.as_ref()), |g, f| unsafe {
794 igraph_read_graph_edgelist(g, f, n, directed)
795 })
796 }
797
798 /// Parses an edge list from a string, see
799 /// [`read_graph_edgelist`](Self::read_graph_edgelist).
800 ///
801 /// # Examples
802 /// ```
803 /// use igraph::prelude::*;
804 /// let g = Graph::read_graph_edgelist_from_str("0 1\n1 2\n2 0\n", 5, false).unwrap();
805 /// assert_eq!((g.vcount(), g.ecount()), (5, 3)); // two isolated vertices from `n`
806 /// let bad = Graph::read_graph_edgelist_from_str("0 1 2", 0, false).unwrap_err();
807 /// assert_eq!(bad.kind(), ErrorKind::Parse);
808 /// ```
809 pub fn read_graph_edgelist_from_str(text: &str, n: usize, directed: bool) -> Result<Graph> {
810 let n = to_int(n, "vertex count")?;
811 read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
812 igraph_read_graph_edgelist(g, f, n, directed)
813 })
814 }
815
816 fn read_ncol_impl(
817 source: Source<'_>,
818 predefnames: &[&str],
819 options: &NcolLglOptions,
820 ) -> Result<Graph> {
821 if predefnames.iter().any(|s| s.contains('\0')) {
822 return Err(Error::invalid(
823 "predefined vertex names must not contain NUL bytes",
824 ));
825 }
826 let names: Option<StrVector> =
827 (!predefnames.is_empty()).then(|| predefnames.iter().collect());
828 let names_ptr = names
829 .as_ref()
830 .map_or(std::ptr::null(), |n| n as *const StrVector);
831 read_with(source, |g, f| unsafe {
832 igraph_read_graph_ncol(
833 g,
834 f,
835 names_ptr,
836 options.names,
837 options.weights.into(),
838 options.directed,
839 )
840 })
841 }
842
843 /// Reads an NCOL file, the symbolic weighted edge list format of the
844 /// Large Graph Layout software.
845 ///
846 /// Each line is `name1 name2 [weight]`: two vertex names without
847 /// whitespace, optionally followed by a (possibly negative, possibly
848 /// scientific notation) weight. Vertex ids are assigned in the order the
849 /// names first appear, after the names listed in `predefnames` (which get
850 /// ids `0..predefnames.len()`; unknown names found in the file extend
851 /// them, and duplicate predefined names are accepted, each with an igraph
852 /// warning). An empty `predefnames` means none.
853 /// Multi-edges and loops are accepted. Time complexity:
854 /// O(|V| + |E| log |V|) ignoring parsing.
855 ///
856 /// With the [attribute handler](crate::attributes::enable) on, the names
857 /// are stored in the `name` vertex attribute and the weights in the
858 /// `weight` edge attribute, as requested by `options`; otherwise they are
859 /// discarded ([module docs](self#attributes)).
860 ///
861 /// Binds [`igraph_read_graph_ncol`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_ncol).
862 ///
863 /// # Errors
864 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
865 /// for syntax errors.
866 pub fn read_graph_ncol(
867 path: impl AsRef<Path>,
868 predefnames: &[&str],
869 options: &NcolLglOptions,
870 ) -> Result<Graph> {
871 Self::read_ncol_impl(Source::Path(path.as_ref()), predefnames, options)
872 }
873
874 /// Parses an NCOL graph from a string, see
875 /// [`read_graph_ncol`](Self::read_graph_ncol).
876 ///
877 /// # Examples
878 /// ```
879 /// use igraph::{foreign::NcolLglOptions, prelude::*};
880 /// let text = "alice bob 2.5\nbob carol\ncarol alice -1e2\n";
881 /// // Fix the vertex ids through the predefined names: carol=0, bob=1, alice=2.
882 /// let g = Graph::read_graph_ncol_from_str(text, &["carol", "bob", "alice"], &NcolLglOptions::default())
883 /// .unwrap();
884 /// assert_eq!(g.edge_list(), vec![(1, 2), (0, 1), (0, 2)]);
885 /// ```
886 pub fn read_graph_ncol_from_str(
887 text: &str,
888 predefnames: &[&str],
889 options: &NcolLglOptions,
890 ) -> Result<Graph> {
891 Self::read_ncol_impl(Source::Bytes(text.as_bytes()), predefnames, options)
892 }
893
894 fn read_lgl_impl(source: Source<'_>, options: &NcolLglOptions) -> Result<Graph> {
895 read_with(source, |g, f| unsafe {
896 igraph_read_graph_lgl(
897 g,
898 f,
899 options.names,
900 options.weights.into(),
901 options.directed,
902 )
903 })
904 }
905
906 /// Reads an LGL file (Large Graph Layout format).
907 ///
908 /// The file is a sequence of blocks: a line `# name` introduces a vertex,
909 /// and each following line `other [weight]` adds an edge from it to
910 /// `other`, until the next `#` line. A `#` line with no following lines
911 /// defines an isolated vertex. Vertex ids are assigned in the order the
912 /// names first appear. Time complexity: O(|V| + |E| log |V|) ignoring
913 /// parsing.
914 ///
915 /// With the [attribute handler](crate::attributes::enable) on, the names
916 /// are stored in the `name` vertex attribute and the weights in the
917 /// `weight` edge attribute, as requested by `options`; otherwise they are
918 /// discarded ([module docs](self#attributes)).
919 ///
920 /// Binds [`igraph_read_graph_lgl`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_lgl).
921 ///
922 /// # Errors
923 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
924 /// for syntax errors.
925 pub fn read_graph_lgl(path: impl AsRef<Path>, options: &NcolLglOptions) -> Result<Graph> {
926 Self::read_lgl_impl(Source::Path(path.as_ref()), options)
927 }
928
929 /// Parses an LGL graph from a string, see [`read_graph_lgl`](Self::read_graph_lgl).
930 ///
931 /// # Examples
932 /// ```
933 /// use igraph::{foreign::NcolLglOptions, prelude::*};
934 /// let text = "# a\nb\nc 2.0\n# b\nc\n# lonely\n";
935 /// let g = Graph::read_graph_lgl_from_str(text, &NcolLglOptions::default()).unwrap();
936 /// assert_eq!(g.vcount(), 4);
937 /// assert_eq!(g.edge_list(), vec![(0, 1), (0, 2), (1, 2)]);
938 /// ```
939 pub fn read_graph_lgl_from_str(text: &str, options: &NcolLglOptions) -> Result<Graph> {
940 Self::read_lgl_impl(Source::Bytes(text.as_bytes()), options)
941 }
942
943 /// Reads a Pajek `.net` file.
944 ///
945 /// Only a subset of the format is supported: `.paj` project files,
946 /// temporal networks, graphs mixing directed and undirected edges,
947 /// permutations/hierarchies/clusters/vectors and multi-relational networks
948 /// are not. `*Arcs` sections create a directed graph, `*Edges` an
949 /// undirected one; `*Arcslist`/`*Edgeslist` and matrix sections are
950 /// understood, as well as bipartite (two-mode) networks. Vertex ids in the
951 /// file are 1-based. Time complexity: O(|V| + |E|).
952 ///
953 /// With the [attribute handler](crate::attributes::enable) on, vertex
954 /// labels become the `name` vertex attribute, coordinates `x`, `y` (and
955 /// `z`), edge weights (including the entries of `*Matrix` sections) the
956 /// `weight` edge attribute, and the other Pajek parameters get
957 /// descriptive names (`ic`/`c` → `color`, `bc` → `framecolor`,
958 /// `x_fact` → `xfact`, `l` → `label`, `w` → `edgewidth`, ...; unknown ones
959 /// are kept as string attributes). A parameter given for some elements
960 /// only takes Pajek's default elsewhere (e.g. `color` is `LightOrange`
961 /// for vertices and `MidnightBlue` for edges; plain numbers such as the
962 /// coordinates and weights are NaN). Two-mode networks get the boolean
963 /// `type` vertex attribute (`false` for the first mode), as expected by
964 /// the [`bipartite`](crate::bipartite) functions. Without the handler all
965 /// of this is discarded ([module docs](self#attributes)).
966 ///
967 /// Binds [`igraph_read_graph_pajek`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_pajek).
968 ///
969 /// # Errors
970 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
971 /// for syntax errors.
972 pub fn read_graph_pajek(path: impl AsRef<Path>) -> Result<Graph> {
973 read_with(Source::Path(path.as_ref()), |g, f| unsafe {
974 igraph_read_graph_pajek(g, f)
975 })
976 }
977
978 /// Parses a Pajek graph from a string, see [`read_graph_pajek`](Self::read_graph_pajek).
979 ///
980 /// # Examples
981 /// ```
982 /// use igraph::prelude::*;
983 /// let text = "*Vertices 4\n1 \"A\"\n2 \"B\"\n3 \"C\"\n4 \"D\"\n*Arcs\n1 2 0.5\n2 3\n3 1\n";
984 /// let g = Graph::read_graph_pajek_from_str(text).unwrap();
985 /// assert!(g.is_directed());
986 /// assert_eq!((g.vcount(), g.ecount()), (4, 3));
987 /// assert_eq!(g.edge(0).unwrap(), (0, 1));
988 /// ```
989 pub fn read_graph_pajek_from_str(text: &str) -> Result<Graph> {
990 read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
991 igraph_read_graph_pajek(g, f)
992 })
993 }
994
995 /// Reads a GraphML file.
996 ///
997 /// Only basic GraphML is supported: no nested graphs, no hyperedges.
998 /// Directedness comes from the `edgedefault` attribute of the `graph`
999 /// element. If the file contains several graphs, `index` selects which
1000 /// one to load (0 for the first); note that igraph 1.0.0 and 1.0.1 fail
1001 /// with "Graph index was too large" ([`ErrorKind::InvalidValue`]) for any
1002 /// index but 0, even when the document does contain more graphs.
1003 /// Vertices get ids in order of appearance, edges keep the document
1004 /// order.
1005 ///
1006 /// With the [attribute handler](crate::attributes::enable) on, every
1007 /// `<key>` becomes a graph, vertex or edge attribute named after its
1008 /// `attr.name` (or its `id` if `attr.name` is missing): `boolean` keys
1009 /// give boolean attributes, `int`/`long`/`float`/`double` numeric ones,
1010 /// `string` string ones (UTF-8). Missing values take the key's
1011 /// `<default>`, or NaN/`""`/`false`. The GraphML `id`s of the nodes are
1012 /// kept in the string `id` vertex attribute (unless a vertex key already
1013 /// defines an attribute named `id`, in which case igraph only warns).
1014 /// When the edges have ids, igraph 1.0.0 and 1.0.1 also create a string
1015 /// `id` *edge* attribute (unless an edge key defines one) but, because of
1016 /// a bug in `src/io/graphml.c`, fill it with the *node* ids, in order,
1017 /// instead of the edge ids (padded with `""` when there are more edges
1018 /// than nodes): don't rely on it. Without the handler all of this is discarded
1019 /// ([module docs](self#attributes)).
1020 ///
1021 /// Binds [`igraph_read_graph_graphml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_graphml).
1022 ///
1023 /// # Errors
1024 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1025 /// for malformed files, [`ErrorKind::InvalidValue`] if `index` is too
1026 /// large, [`ErrorKind::Unimplemented`] if the igraph library was compiled
1027 /// without GraphML (libxml2) support.
1028 pub fn read_graph_graphml(path: impl AsRef<Path>, index: usize) -> Result<Graph> {
1029 let index = to_int(index, "graph index")?;
1030 read_with(Source::Path(path.as_ref()), |g, f| unsafe {
1031 igraph_read_graph_graphml(g, f, index)
1032 })
1033 }
1034
1035 /// Parses a GraphML document from a string, see
1036 /// [`read_graph_graphml`](Self::read_graph_graphml).
1037 ///
1038 /// # Examples
1039 /// ```
1040 /// use igraph::{attributes, prelude::*};
1041 /// attributes::enable().unwrap();
1042 /// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1043 /// <graphml xmlns="http://graphml.graphdrawing.org/xmlns">
1044 /// <key id="d0" for="node" attr.name="age" attr.type="int"><default>20</default></key>
1045 /// <key id="d1" for="edge" attr.name="since" attr.type="double"/>
1046 /// <graph edgedefault="directed">
1047 /// <node id="ann"><data key="d0">30</data></node>
1048 /// <node id="bob"/>
1049 /// <edge source="bob" target="ann"><data key="d1">2019</data></edge>
1050 /// </graph>
1051 /// </graphml>"#;
1052 /// let g = Graph::read_graph_graphml_from_str(xml, 0).unwrap();
1053 /// assert!(g.is_directed());
1054 /// assert_eq!(g.edge_list(), vec![(1, 0)]);
1055 /// assert_eq!(g.vertex_attr_str_values("id", ..).unwrap(), ["ann", "bob"]);
1056 /// assert_eq!(g.vertex_attr_numeric_values("age", ..).unwrap(), [30.0, 20.0]); // bob: default
1057 /// assert_eq!(g.edge_attr_numeric("since", 0).unwrap(), 2019.0);
1058 /// ```
1059 pub fn read_graph_graphml_from_str(text: &str, index: usize) -> Result<Graph> {
1060 let index = to_int(index, "graph index")?;
1061 read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
1062 igraph_read_graph_graphml(g, f, index)
1063 })
1064 }
1065
1066 fn read_dimacs_impl(source: Source<'_>, directed: bool) -> Result<DimacsFlow> {
1067 let mut problem = StrVector::new();
1068 let mut labels = VectorInt::new();
1069 let mut capacity = Vector::new();
1070 let (mut s, mut t) = (-1, -1);
1071 let graph = read_with(source, |g, f| unsafe {
1072 igraph_read_graph_dimacs_flow(
1073 g,
1074 f,
1075 &mut problem,
1076 &mut labels,
1077 &mut s,
1078 &mut t,
1079 &mut capacity,
1080 directed,
1081 )
1082 })?;
1083 let problem_name = problem.get(0).unwrap_or_default().to_owned();
1084 let problem = if problem_name == "edge" {
1085 DimacsProblem::Edge {
1086 labels: labels.into(),
1087 }
1088 } else {
1089 // igraph reports a missing `n` line as id -2 (0 - 1 - 1) and
1090 // doesn't check the ids it read against the vertex count.
1091 let n = graph.vcount() as VertexId;
1092 let check = |id: igraph_int_t, what: &str| -> Result<Option<VertexId>> {
1093 match id {
1094 -2 => Ok(None),
1095 id if (0..n).contains(&id) => Ok(Some(id)),
1096 id => Err(Error::new(
1097 ErrorKind::Parse,
1098 format!(
1099 "DIMACS {what} vertex {} out of range (the graph has {n} vertices)",
1100 id + 1
1101 ),
1102 )),
1103 }
1104 };
1105 DimacsProblem::Max {
1106 source: check(s, "source")?,
1107 target: check(t, "target")?,
1108 capacity: capacity.into(),
1109 }
1110 };
1111 Ok(DimacsFlow {
1112 graph,
1113 problem_name,
1114 problem,
1115 })
1116 }
1117
1118 /// Reads a DIMACS network flow file.
1119 ///
1120 /// DIMACS is a line oriented format; the first character of each line
1121 /// gives its type: `c` comment, `p` the problem line (`p max|edge
1122 /// <vertices> <edges>`, before any node or arc line), `n` node lines and
1123 /// `a` arc lines (`a from to capacity`, max-flow problems) or `e` edge
1124 /// lines (`e from to`, edge problems). In max-flow problems exactly two
1125 /// node lines `n <id> s|t` mark the source and the target. In edge
1126 /// problems the labels are the 1-based vertex indices: `n <id> <label>`
1127 /// lines are meant to change them, but igraph 1.0.0 and 1.0.1 reject them
1128 /// with a parse error. Vertex ids in the file start from 1, the returned ids
1129 /// from 0. A max-flow file without the source (or target) `n` line is
1130 /// accepted, with `None` in [`DimacsProblem::Max`]. Time complexity:
1131 /// O(|V| + |E| + c), c being the file size. The capacities are returned
1132 /// in [`DimacsProblem::Max`], never as attributes.
1133 ///
1134 /// See [`Graph::maxflow`] and [`Graph::st_mincut`] to solve the problem.
1135 ///
1136 /// Binds [`igraph_read_graph_dimacs_flow`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_dimacs_flow).
1137 ///
1138 /// # Errors
1139 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1140 /// or [`ErrorKind::InvalidValue`] for malformed files, including a source
1141 /// or target `n` line naming a vertex that doesn't exist.
1142 pub fn read_graph_dimacs_flow(path: impl AsRef<Path>, directed: bool) -> Result<DimacsFlow> {
1143 Self::read_dimacs_impl(Source::Path(path.as_ref()), directed)
1144 }
1145
1146 /// Parses a DIMACS flow problem from a string, see
1147 /// [`read_graph_dimacs_flow`](Self::read_graph_dimacs_flow).
1148 ///
1149 /// # Examples
1150 /// ```
1151 /// use igraph::{foreign::DimacsProblem, prelude::*};
1152 /// let text = "c a tiny network\np max 3 2\nn 1 s\nn 3 t\na 1 2 4\na 2 3 7\n";
1153 /// let flow = Graph::read_graph_dimacs_flow_from_str(text, true).unwrap();
1154 /// assert_eq!(flow.problem_name, "max");
1155 /// assert_eq!(flow.graph.edge_list(), vec![(0, 1), (1, 2)]);
1156 /// let DimacsProblem::Max { source: Some(s), target: Some(t), capacity } = flow.problem else {
1157 /// panic!("a max-flow problem with both terminals was expected");
1158 /// };
1159 /// assert_eq!((s, t, capacity.as_slice()), (0, 2, &[4.0, 7.0][..]));
1160 /// // Solve it: the bottleneck is the first arc.
1161 /// assert_eq!(flow.graph.maxflow_value(s, t, Some(&capacity)).unwrap(), 4.0);
1162 /// ```
1163 pub fn read_graph_dimacs_flow_from_str(text: &str, directed: bool) -> Result<DimacsFlow> {
1164 Self::read_dimacs_impl(Source::Bytes(text.as_bytes()), directed)
1165 }
1166
1167 /// Reads a graph in the binary format of the ARG graph database (used
1168 /// to benchmark isomorphism algorithms).
1169 ///
1170 /// The file is a sequence of 16-bit little-endian words: the number of
1171 /// vertices, then for each vertex the number of its out-edges followed by
1172 /// their (0-based) targets. Only unlabelled graphs are supported. Time
1173 /// complexity: O(|V| + |E|).
1174 ///
1175 /// The database is a benchmark for isomorphism algorithms: see
1176 /// [`Graph::isomorphic`] and the rest of the
1177 /// [`isomorphism`](crate::isomorphism) module.
1178 ///
1179 /// Binds [`igraph_read_graph_graphdb`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_graphdb).
1180 ///
1181 /// # Errors
1182 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1183 /// for truncated files or trailing bytes.
1184 pub fn read_graph_graphdb(path: impl AsRef<Path>, directed: bool) -> Result<Graph> {
1185 read_with(Source::Path(path.as_ref()), |g, f| unsafe {
1186 igraph_read_graph_graphdb(g, f, directed)
1187 })
1188 }
1189
1190 /// Parses a graph in the binary graph database format from bytes, see
1191 /// [`read_graph_graphdb`](Self::read_graph_graphdb).
1192 ///
1193 /// # Examples
1194 /// ```
1195 /// use igraph::prelude::*;
1196 /// // 4 vertices; 0 -> [2]; 1 -> [0]; 2 -> []; 3 -> [0, 2] (igraph's unit test file).
1197 /// let words: [u16; 9] = [4, 1, 2, 1, 0, 0, 2, 0, 2];
1198 /// let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
1199 /// let g = Graph::read_graph_graphdb_from_bytes(&bytes, true).unwrap();
1200 /// assert_eq!(g.edge_list(), vec![(0, 2), (1, 0), (3, 0), (3, 2)]);
1201 /// ```
1202 pub fn read_graph_graphdb_from_bytes(data: &[u8], directed: bool) -> Result<Graph> {
1203 read_with(Source::Bytes(data), |g, f| unsafe {
1204 igraph_read_graph_graphdb(g, f, directed)
1205 })
1206 }
1207
1208 /// Reads a GML file.
1209 ///
1210 /// Any syntactically correct GML is parsed, but only a subset is used:
1211 /// the first `graph` record, its `directed` flag, `node` records (with
1212 /// their `id`) and `edge` records (with `source` and `target`); other top
1213 /// level records are ignored. `inf`, `-inf` and `nan` are accepted as
1214 /// reals (case insensitively). Time complexity: proportional to the file
1215 /// length.
1216 ///
1217 /// With the [attribute handler](crate::attributes::enable) on, every
1218 /// field of simple type (integer, real, string) of the graph, node and
1219 /// edge records becomes a numeric or string attribute (including the
1220 /// numeric `id` of the nodes, and `comment` fields); composite fields
1221 /// (records) are ignored with a warning, and missing values are NaN or
1222 /// `""`. Only the `quot`, `amp`, `apos`, `lt` and `gt` entities are
1223 /// decoded. Without the handler all of this is discarded
1224 /// ([module docs](self#attributes)).
1225 ///
1226 /// The C parser of igraph 1.0.0 and 1.0.1 is not reentrant (it uses
1227 /// static buffers), so this wrapper serializes GML reads across threads
1228 /// with a process-wide lock; all the other readers run fully in parallel.
1229 ///
1230 /// Binds [`igraph_read_graph_gml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_gml).
1231 ///
1232 /// # Errors
1233 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1234 /// for syntax errors and for structural problems: no `graph` record,
1235 /// duplicate or non-integer node ids, edges without `source`/`target` or
1236 /// referring to unknown node ids.
1237 pub fn read_graph_gml(path: impl AsRef<Path>) -> Result<Graph> {
1238 read_gml(Source::Path(path.as_ref()))
1239 }
1240
1241 /// Parses a GML graph from a string, see [`read_graph_gml`](Self::read_graph_gml).
1242 ///
1243 /// # Examples
1244 /// ```
1245 /// use igraph::prelude::*;
1246 /// let text = r#"graph [
1247 /// directed 0
1248 /// node [ id 10 label "x" ]
1249 /// node [ id 20 ]
1250 /// node [ id 30 ]
1251 /// edge [ source 10 target 20 ]
1252 /// edge [ source 30 target 10 weight 2.5 ]
1253 /// ]"#;
1254 /// let g = Graph::read_graph_gml_from_str(text).unwrap();
1255 /// assert!(!g.is_directed());
1256 /// // GML ids are mapped to consecutive vertex ids in order of appearance.
1257 /// assert_eq!(g.edge_list(), vec![(0, 1), (0, 2)]);
1258 /// ```
1259 pub fn read_graph_gml_from_str(text: &str) -> Result<Graph> {
1260 read_gml(Source::Bytes(text.as_bytes()))
1261 }
1262
1263 /// Reads a file in the DL format of UCINET.
1264 ///
1265 /// All the forms of the format are supported: full matrix, edge list
1266 /// (`format = edgelist1`) and node list (`format = nodelist1`), with or
1267 /// without labels. Labels are case sensitive. With the
1268 /// [attribute handler](crate::attributes::enable) on, labels are stored
1269 /// in the `name` vertex attribute and edge values in the `weight` edge
1270 /// attribute; otherwise they are discarded
1271 /// ([module docs](self#attributes)). Time complexity: linear in the
1272 /// number of vertices and edges, quadratic in the number of vertices for
1273 /// the full matrix form.
1274 ///
1275 /// Binds [`igraph_read_graph_dl`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_read_graph_dl).
1276 ///
1277 /// # Errors
1278 /// [`ErrorKind::File`] if the file can't be opened, [`ErrorKind::Parse`]
1279 /// for syntax errors.
1280 pub fn read_graph_dl(path: impl AsRef<Path>, directed: bool) -> Result<Graph> {
1281 read_with(Source::Path(path.as_ref()), |g, f| unsafe {
1282 igraph_read_graph_dl(g, f, directed)
1283 })
1284 }
1285
1286 /// Parses a UCINET DL graph from a string, see [`read_graph_dl`](Self::read_graph_dl).
1287 ///
1288 /// # Examples
1289 /// ```
1290 /// use igraph::prelude::*;
1291 /// // `fullmatrix1.dl` from igraph's examples.
1292 /// let text = "DL N = 5\nData:\n0 1 1 1 1\n1 0 1 0 0\n1 1 0 0 1\n1 0 0 0 0\n1 0 1 0 0\n";
1293 /// let g = Graph::read_graph_dl_from_str(text, true).unwrap();
1294 /// assert_eq!((g.vcount(), g.ecount()), (5, 12));
1295 /// assert_eq!(g.edge(0).unwrap(), (0, 1));
1296 /// ```
1297 pub fn read_graph_dl_from_str(text: &str, directed: bool) -> Result<Graph> {
1298 read_with(Source::Bytes(text.as_bytes()), |g, f| unsafe {
1299 igraph_read_graph_dl(g, f, directed)
1300 })
1301 }
1302}
1303
1304// ---------------------------------------------------------------------------
1305// Writers
1306// ---------------------------------------------------------------------------
1307
1308impl igraph_t {
1309 /// Writes the graph to a file in the given `format`, with default options
1310 /// (NCOL/LGL/LEDA without the optional named attributes, GraphML without
1311 /// prefixes, GML with the default [`GmlWriteOptions`], LGL without
1312 /// isolated vertices). GraphML, GML and DOT export all the attributes, and
1313 /// Pajek those naming Pajek parameters, when the
1314 /// [attribute handler](crate::attributes::enable) is on.
1315 ///
1316 /// # Errors
1317 /// [`ErrorKind::Unimplemented`] if the format can't be written generically
1318 /// (graph database, DL, DIMACS), plus the errors of the specific writer.
1319 ///
1320 /// # Examples
1321 /// ```
1322 /// use igraph::{foreign::GraphFormat, prelude::*};
1323 /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
1324 /// let path = std::env::temp_dir().join(format!("igraph-doc-write-{}.dot", std::process::id()));
1325 /// g.write_graph(&path, GraphFormat::from_path(&path).unwrap()).unwrap();
1326 /// let dot = std::fs::read_to_string(&path).unwrap();
1327 /// std::fs::remove_file(&path).unwrap();
1328 /// assert!(dot.contains("graph {") && dot.contains("1 -- 0;") && dot.contains("2 -- 1;"));
1329 /// ```
1330 pub fn write_graph(&self, path: impl AsRef<Path>, format: GraphFormat) -> Result<()> {
1331 let path = path.as_ref();
1332 match format {
1333 GraphFormat::Edgelist => self.write_graph_edgelist(path),
1334 GraphFormat::Ncol => self.write_graph_ncol(path, None, None),
1335 GraphFormat::Lgl => self.write_graph_lgl(path, None, None, false),
1336 GraphFormat::Pajek => self.write_graph_pajek(path),
1337 GraphFormat::GraphMl => self.write_graph_graphml(path),
1338 GraphFormat::Gml => self.write_graph_gml(path, &GmlWriteOptions::default()),
1339 GraphFormat::Dot => self.write_graph_dot(path),
1340 GraphFormat::Leda => self.write_graph_leda(path, None, None),
1341 GraphFormat::DimacsFlow | GraphFormat::GraphDb | GraphFormat::Dl => Err(Error::new(
1342 ErrorKind::Unimplemented,
1343 format!("the {format:?} format can't be written generically"),
1344 )),
1345 }
1346 }
1347
1348 /// Writes the edge list of the graph to a file: one `from to` line per
1349 /// edge (0-based ids, a single space as separator). Lines are sorted by
1350 /// the first endpoint, so edge ids are preserved by a round trip only if
1351 /// the edges were already sorted that way; isolated vertices (with
1352 /// ids above the largest endpoint) are lost unless the vertex count is
1353 /// passed back to [`read_graph_edgelist`](Self::read_graph_edgelist).
1354 /// Time complexity: O(|E|).
1355 ///
1356 /// Binds [`igraph_write_graph_edgelist`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_edgelist).
1357 ///
1358 /// # Errors
1359 /// [`ErrorKind::File`] if the file can't be created or written.
1360 pub fn write_graph_edgelist(&self, path: impl AsRef<Path>) -> Result<()> {
1361 write_to_path(path.as_ref(), |f| unsafe {
1362 igraph_write_graph_edgelist(self, f)
1363 })
1364 }
1365
1366 /// The edge list serialization as a string, see
1367 /// [`write_graph_edgelist`](Self::write_graph_edgelist).
1368 ///
1369 /// # Examples
1370 /// ```
1371 /// use igraph::prelude::*;
1372 /// let g = Graph::from_edges(&[(2, 1), (0, 1)], 3, true).unwrap();
1373 /// // Sorted by the source vertex, not by edge id.
1374 /// assert_eq!(g.write_graph_edgelist_to_string().unwrap(), "0 1\n2 1\n");
1375 /// ```
1376 pub fn write_graph_edgelist_to_string(&self) -> Result<String> {
1377 write_to_string(|f| unsafe { igraph_write_graph_edgelist(self, f) })
1378 }
1379
1380 /// Writes the graph to an NCOL file (see
1381 /// [`read_graph_ncol`](Self::read_graph_ncol)): one `from to [weight]`
1382 /// line per edge.
1383 ///
1384 /// `names` is the name of a string vertex attribute to write instead of
1385 /// the vertex ids, `weights` the name of a numeric edge attribute to
1386 /// write as weights; `None` skips them. A requested attribute that does
1387 /// not exist (always the case without the
1388 /// [attribute handler](crate::attributes::enable)) is skipped with an
1389 /// igraph warning ([module docs](self#attributes)). NaN and infinite
1390 /// weights are written as `NaN`, `Inf` and `-Inf`, and read back as such.
1391 /// Names must be non-empty and contain no spaces or non-printable
1392 /// characters. The format can't represent
1393 /// isolated vertices; multi-edges and loops are written (though they
1394 /// break the LGL software). Time complexity: O(|E|).
1395 ///
1396 /// Binds [`igraph_write_graph_ncol`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_ncol).
1397 ///
1398 /// # Errors
1399 /// [`ErrorKind::File`] on I/O errors, [`ErrorKind::InvalidValue`] for
1400 /// attribute names with NUL bytes or invalid vertex names.
1401 pub fn write_graph_ncol(
1402 &self,
1403 path: impl AsRef<Path>,
1404 names: Option<&str>,
1405 weights: Option<&str>,
1406 ) -> Result<()> {
1407 let (n, w) = (
1408 opt_cstring(names, "names")?,
1409 opt_cstring(weights, "weights")?,
1410 );
1411 write_to_path(path.as_ref(), |f| unsafe {
1412 igraph_write_graph_ncol(self, f, opt_ptr(&n), opt_ptr(&w))
1413 })
1414 }
1415
1416 /// The NCOL serialization as a string, see
1417 /// [`write_graph_ncol`](Self::write_graph_ncol).
1418 ///
1419 /// # Examples
1420 /// ```
1421 /// use igraph::prelude::*;
1422 /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 4, false).unwrap();
1423 /// assert_eq!(g.write_graph_ncol_to_string(None, None).unwrap(), "0 1\n1 2\n");
1424 /// ```
1425 pub fn write_graph_ncol_to_string(
1426 &self,
1427 names: Option<&str>,
1428 weights: Option<&str>,
1429 ) -> Result<String> {
1430 let (n, w) = (
1431 opt_cstring(names, "names")?,
1432 opt_cstring(weights, "weights")?,
1433 );
1434 write_to_string(|f| unsafe { igraph_write_graph_ncol(self, f, opt_ptr(&n), opt_ptr(&w)) })
1435 }
1436
1437 /// Writes the graph to an LGL file (see [`read_graph_lgl`](Self::read_graph_lgl)).
1438 ///
1439 /// Edges are grouped by their first endpoint: `# from` followed by one
1440 /// `to [weight]` line per edge. `names`/`weights` name a string vertex
1441 /// attribute and a numeric edge attribute to write (skipped with a
1442 /// warning when missing, as always without the
1443 /// [attribute handler](crate::attributes::enable), see the
1444 /// [module docs](self#attributes)); names must be non-empty and contain
1445 /// no spaces, `#` or non-printable characters. With `isolates = true` isolated
1446 /// vertices are written as lone `# v` lines. Time complexity: O(|E|), or
1447 /// O(|V| + |E|) with isolates.
1448 ///
1449 /// Binds [`igraph_write_graph_lgl`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_lgl).
1450 ///
1451 /// # Errors
1452 /// [`ErrorKind::File`] on I/O errors, [`ErrorKind::InvalidValue`] for
1453 /// attribute names with NUL bytes or invalid vertex names.
1454 pub fn write_graph_lgl(
1455 &self,
1456 path: impl AsRef<Path>,
1457 names: Option<&str>,
1458 weights: Option<&str>,
1459 isolates: bool,
1460 ) -> Result<()> {
1461 let (n, w) = (
1462 opt_cstring(names, "names")?,
1463 opt_cstring(weights, "weights")?,
1464 );
1465 write_to_path(path.as_ref(), |f| unsafe {
1466 igraph_write_graph_lgl(self, f, opt_ptr(&n), opt_ptr(&w), isolates)
1467 })
1468 }
1469
1470 /// The LGL serialization as a string, see [`write_graph_lgl`](Self::write_graph_lgl).
1471 ///
1472 /// # Examples
1473 /// ```
1474 /// use igraph::prelude::*;
1475 /// // The graph of igraph's `igraph_write_graph_lgl.c` example.
1476 /// let g = Graph::from_edges(&[(0, 1), (1, 3), (1, 2), (2, 0), (4, 2), (3, 4)], 7, false).unwrap();
1477 /// let lgl = g.write_graph_lgl_to_string(None, None, true).unwrap();
1478 /// assert_eq!(lgl, "# 0\n1\n2\n# 1\n2\n3\n# 2\n4\n# 3\n4\n# 5\n# 6\n");
1479 /// ```
1480 pub fn write_graph_lgl_to_string(
1481 &self,
1482 names: Option<&str>,
1483 weights: Option<&str>,
1484 isolates: bool,
1485 ) -> Result<String> {
1486 let (n, w) = (
1487 opt_cstring(names, "names")?,
1488 opt_cstring(weights, "weights")?,
1489 );
1490 write_to_string(|f| unsafe {
1491 igraph_write_graph_lgl(self, f, opt_ptr(&n), opt_ptr(&w), isolates)
1492 })
1493 }
1494
1495 /// Writes the graph to a GraphML file (without attribute prefixes).
1496 ///
1497 /// GraphML is an XML format, see the
1498 /// [GraphML primer](http://graphml.graphdrawing.org/primer/graphml-primer.html).
1499 /// Vertices are written as `n0, n1, ...`, edges in id order, and the
1500 /// `edgedefault` reflects the directedness. With the
1501 /// [attribute handler](crate::attributes::enable) on, all graph, vertex
1502 /// and edge attributes are written as typed `<key>`s (`boolean`,
1503 /// `double` or `string`; NaN values are omitted), so a GraphML round trip
1504 /// preserves them ([module docs](self#attributes)). Attribute names and
1505 /// string values should be UTF-8 (igraph copies the bytes as they are)
1506 /// and must not contain control characters other than tab, CR and LF. Use
1507 /// [`write_graph_graphml_with`](Self::write_graph_graphml_with) to
1508 /// control the attribute name prefixes. Time complexity: O(|V| + |E|).
1509 ///
1510 /// This replaces the former, infallible `write_graph_graphml(&self, &str)`
1511 /// that leaked its file handle: the file is now always closed and errors
1512 /// are reported.
1513 ///
1514 /// Binds [`igraph_write_graph_graphml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_graphml).
1515 ///
1516 /// # Errors
1517 /// [`ErrorKind::File`] if the file can't be created or written,
1518 /// [`ErrorKind::InvalidValue`] if an attribute name or string value
1519 /// contains a control character other than tab, CR and LF.
1520 ///
1521 /// # Examples
1522 /// ```
1523 /// use igraph::prelude::*;
1524 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
1525 /// let path = std::env::temp_dir().join(format!("igraph-doc-{}.graphml", std::process::id()));
1526 /// g.write_graph_graphml(&path).unwrap();
1527 /// let xml = std::fs::read_to_string(&path).unwrap();
1528 /// std::fs::remove_file(&path).unwrap();
1529 /// assert!(xml.contains(r#"edgedefault="directed""#));
1530 /// assert!(xml.contains(r#"<edge source="n2" target="n0">"#));
1531 /// ```
1532 pub fn write_graph_graphml(&self, path: impl AsRef<Path>) -> Result<()> {
1533 self.write_graph_graphml_with(path, false)
1534 }
1535
1536 /// Writes the graph to a GraphML file; with `prefixattr = true`
1537 /// attribute names get a `g_`, `v_` or `e_` prefix to keep graph, vertex
1538 /// and edge attributes with the same name distinct. See
1539 /// [`write_graph_graphml`](Self::write_graph_graphml).
1540 ///
1541 /// Binds [`igraph_write_graph_graphml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_graphml).
1542 ///
1543 /// # Errors
1544 /// As [`write_graph_graphml`](Self::write_graph_graphml).
1545 pub fn write_graph_graphml_with(&self, path: impl AsRef<Path>, prefixattr: bool) -> Result<()> {
1546 write_to_path(path.as_ref(), |f| unsafe {
1547 igraph_write_graph_graphml(self, f, prefixattr)
1548 })
1549 }
1550
1551 /// The GraphML serialization as a string, see
1552 /// [`write_graph_graphml_with`](Self::write_graph_graphml_with).
1553 ///
1554 /// # Examples
1555 /// ```
1556 /// use igraph::prelude::*;
1557 /// let mut g = Graph::from_edges(&[(0, 1)], 2, false).unwrap();
1558 /// // Setting an attribute turns on the attribute handler.
1559 /// g.set_vertex_attr_str_values("name", &["ann", "bob"]).unwrap();
1560 /// g.set_edge_attr_numeric("weight", 0, 2.5).unwrap();
1561 /// let xml = g.write_graph_graphml_to_string(true).unwrap();
1562 /// assert!(xml.contains(r#"<key id="v_name" for="node" attr.name="name" attr.type="string"/>"#));
1563 /// assert!(xml.contains(r#"<data key="v_name">bob</data>"#));
1564 /// assert!(xml.contains(r#"<data key="e_weight">2.5</data>"#));
1565 /// // Reading it back restores the attributes (under their original names).
1566 /// let h = Graph::read_graph_graphml_from_str(&xml, 0).unwrap();
1567 /// assert_eq!(h.vertex_attr_str_values("name", ..).unwrap(), ["ann", "bob"]);
1568 /// assert_eq!(h.edge_attr_numeric("weight", 0).unwrap(), 2.5);
1569 /// ```
1570 pub fn write_graph_graphml_to_string(&self, prefixattr: bool) -> Result<String> {
1571 write_to_string(|f| unsafe { igraph_write_graph_graphml(self, f, prefixattr) })
1572 }
1573
1574 /// Writes the graph to a Pajek `.net` file.
1575 ///
1576 /// The format is meant for interoperability with the Pajek software, not
1577 /// for data exchange. Vertex ids are written 1-based; directed graphs use
1578 /// an `*Arcs` section, undirected ones `*Edges`. Vertex and edge
1579 /// parameters come from the attributes named as in
1580 /// [`read_graph_pajek`](Self::read_graph_pajek) (`name` as the label,
1581 /// `x`/`y`/`z`, `color`, edge `weight`, ...; other attributes are
1582 /// discarded), hence are only written with the
1583 /// [attribute handler](crate::attributes::enable) on
1584 /// ([module docs](self#attributes)). A boolean `type` vertex attribute
1585 /// makes a two-mode (bipartite) file: since Pajek needs the vertices of
1586 /// the first mode first, the vertices are then reordered, and their ids
1587 /// change on a round trip (without a `name` attribute the labels are then
1588 /// the original 1-based ids). igraph never writes a UTF-8 byte-order mark.
1589 /// Time complexity: O(|V| + |E|).
1590 ///
1591 /// Binds [`igraph_write_graph_pajek`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_pajek).
1592 ///
1593 /// # Errors
1594 /// [`ErrorKind::File`] if the file can't be created or written.
1595 pub fn write_graph_pajek(&self, path: impl AsRef<Path>) -> Result<()> {
1596 write_to_path(path.as_ref(), |f| unsafe {
1597 igraph_write_graph_pajek(self, f)
1598 })
1599 }
1600
1601 /// The Pajek serialization as a string, see
1602 /// [`write_graph_pajek`](Self::write_graph_pajek).
1603 ///
1604 /// # Examples
1605 /// ```
1606 /// use igraph::prelude::*;
1607 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
1608 /// assert_eq!(g.write_graph_pajek_to_string().unwrap(), "*Vertices 3\n*Arcs\n1 2\n2 3\n3 1\n");
1609 /// ```
1610 pub fn write_graph_pajek_to_string(&self) -> Result<String> {
1611 write_to_string(|f| unsafe { igraph_write_graph_pajek(self, f) })
1612 }
1613
1614 /// Writes a maximum flow problem in DIMACS format (see
1615 /// [`read_graph_dimacs_flow`](Self::read_graph_dimacs_flow)): a comment,
1616 /// the `p max` line, the source and target `n` lines and one
1617 /// `a from to capacity` line per edge (1-based ids). `capacity` must have
1618 /// one entry per edge. Time complexity: O(|E|).
1619 ///
1620 /// See [`Graph::maxflow`] to solve the instance directly.
1621 ///
1622 /// Binds [`igraph_write_graph_dimacs_flow`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_dimacs_flow).
1623 ///
1624 /// # Errors
1625 /// [`ErrorKind::InvalidVertexId`] if `source` or `target` is not a vertex
1626 /// of the graph (igraph itself would write them unchecked),
1627 /// [`ErrorKind::InvalidValue`] if `capacity.len() != ecount`,
1628 /// [`ErrorKind::File`] on I/O errors.
1629 pub fn write_graph_dimacs_flow(
1630 &self,
1631 path: impl AsRef<Path>,
1632 source: VertexId,
1633 target: VertexId,
1634 capacity: &[f64],
1635 ) -> Result<()> {
1636 self.check_dimacs_args(source, target, capacity)?;
1637 let cap = Vector::view(capacity);
1638 write_to_path(path.as_ref(), |f| unsafe {
1639 igraph_write_graph_dimacs_flow(self, f, source, target, cap.as_ptr())
1640 })
1641 }
1642
1643 /// The DIMACS serialization as a string, see
1644 /// [`write_graph_dimacs_flow`](Self::write_graph_dimacs_flow).
1645 ///
1646 /// # Examples
1647 /// ```
1648 /// use igraph::prelude::*;
1649 /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, true).unwrap();
1650 /// let text = g.write_graph_dimacs_flow_to_string(0, 2, &[4.0, 7.5]).unwrap();
1651 /// assert_eq!(text, "c created by igraph\np max 3 2\nn 1 s\nn 3 t\na 1 2 4\na 2 3 7.5\n");
1652 /// ```
1653 pub fn write_graph_dimacs_flow_to_string(
1654 &self,
1655 source: VertexId,
1656 target: VertexId,
1657 capacity: &[f64],
1658 ) -> Result<String> {
1659 self.check_dimacs_args(source, target, capacity)?;
1660 let cap = Vector::view(capacity);
1661 write_to_string(|f| unsafe {
1662 igraph_write_graph_dimacs_flow(self, f, source, target, cap.as_ptr())
1663 })
1664 }
1665
1666 fn check_dimacs_args(
1667 &self,
1668 source: VertexId,
1669 target: VertexId,
1670 capacity: &[f64],
1671 ) -> Result<()> {
1672 // igraph writes any id verbatim, producing files that can't be read back.
1673 for (what, v) in [("source", source), ("target", target)] {
1674 if !(0..self.vcount() as VertexId).contains(&v) {
1675 return Err(Error::new(
1676 ErrorKind::InvalidVertexId,
1677 format!(
1678 "invalid {what} vertex {v} for a graph with {} vertices",
1679 self.vcount()
1680 ),
1681 ));
1682 }
1683 }
1684 if capacity.len() != self.ecount() {
1685 return Err(Error::invalid(format!(
1686 "the capacity vector has length {}, but the graph has {} edges",
1687 capacity.len(),
1688 self.ecount()
1689 )));
1690 }
1691 Ok(())
1692 }
1693
1694 /// Writes the graph to a GML file.
1695 ///
1696 /// The output lists the `directed` flag, one `node [ id ... ]` record per
1697 /// vertex and one `edge [ source ... target ... ]` record per edge. See
1698 /// [`GmlWriteOptions`] for the ids, the creator line and the entity
1699 /// encoding. Time complexity: proportional to the output size.
1700 ///
1701 /// With the [attribute handler](crate::attributes::enable) on, numeric
1702 /// and string attributes are written too (booleans as 0/1, NaN values
1703 /// skipped, infinite values kept with a warning since they are not
1704 /// standard GML). Attribute names are reduced to their alphanumeric
1705 /// characters (prefixed with `igraph` if they don't start with a
1706 /// letter); attributes whose name would clash with the GML structure
1707 /// (`source`/`target` on edges, `directed`, `node` and `edge` on the
1708 /// graph, `id` on vertices) are skipped with a warning. The `id` vertex
1709 /// attribute itself is never written as an attribute: a numeric one
1710 /// supplies the node ids (see [`GmlWriteOptions::ids`]), any other is
1711 /// dropped ([module docs](self#attributes)).
1712 ///
1713 /// Binds [`igraph_write_graph_gml`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_gml).
1714 ///
1715 /// # Errors
1716 /// [`ErrorKind::InvalidValue`] if `options.ids` doesn't have one entry per
1717 /// vertex, or the creator contains a NUL byte; [`ErrorKind::File`] on I/O
1718 /// errors.
1719 pub fn write_graph_gml(
1720 &self,
1721 path: impl AsRef<Path>,
1722 options: &GmlWriteOptions<'_>,
1723 ) -> Result<()> {
1724 let (opts, ids, creator) = self.gml_args(options)?;
1725 let ids_ptr = ids.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
1726 // `ctime` (used for the default creator line) is not reentrant.
1727 let _guard = options.creator.is_none().then(gml_lock);
1728 write_to_path(path.as_ref(), |f| unsafe {
1729 igraph_write_graph_gml(self, f, opts, ids_ptr, opt_ptr(&creator))
1730 })
1731 }
1732
1733 /// The GML serialization as a string, see
1734 /// [`write_graph_gml`](Self::write_graph_gml).
1735 ///
1736 /// # Examples
1737 /// ```
1738 /// use igraph::{foreign::GmlWriteOptions, prelude::*};
1739 /// let g = Graph::from_edges(&[(0, 1)], 2, false).unwrap();
1740 /// let opts = GmlWriteOptions::default().with_creator("").with_ids(&[10.0, 20.0]);
1741 /// let gml = g.write_graph_gml_to_string(&opts).unwrap();
1742 /// // igraph stores undirected edges with the larger endpoint first.
1743 /// assert!(gml.contains("id 10") && gml.contains("source 20") && gml.contains("target 10"));
1744 /// assert!(!gml.contains("Creator"));
1745 /// ```
1746 pub fn write_graph_gml_to_string(&self, options: &GmlWriteOptions<'_>) -> Result<String> {
1747 let (opts, ids, creator) = self.gml_args(options)?;
1748 let ids_ptr = ids.as_ref().map_or(std::ptr::null(), |v| v.as_ptr());
1749 // `ctime` (used for the default creator line) is not reentrant.
1750 let _guard = options.creator.is_none().then(gml_lock);
1751 write_to_string(|f| unsafe {
1752 igraph_write_graph_gml(self, f, opts, ids_ptr, opt_ptr(&creator))
1753 })
1754 }
1755
1756 /// Validates and converts the GML writer options.
1757 #[allow(clippy::type_complexity)]
1758 fn gml_args<'a>(
1759 &self,
1760 options: &GmlWriteOptions<'a>,
1761 ) -> Result<(
1762 igraph_write_gml_sw_t,
1763 Option<crate::vector::View<'a, Vector>>,
1764 Option<CString>,
1765 )> {
1766 if let Some(ids) = options.ids
1767 && ids.len() != self.vcount()
1768 {
1769 return Err(Error::invalid(format!(
1770 "the id vector has length {}, but the graph has {} vertices",
1771 ids.len(),
1772 self.vcount()
1773 )));
1774 }
1775 let flags = if options.encode_only_quot {
1776 IGRAPH_WRITE_GML_ENCODE_ONLY_QUOT_SW
1777 } else {
1778 IGRAPH_WRITE_GML_DEFAULT_SW
1779 } as igraph_write_gml_sw_t;
1780 // igraph 1.0.0 and 1.0.1 compute the entity-encoded creator but then
1781 // print the raw string, so a `"` would end the GML string early:
1782 // encode it here.
1783 let creator = options
1784 .creator
1785 .map(|c| gml_entity_encode(c, options.encode_only_quot));
1786 let creator = opt_cstring(creator.as_deref(), "the GML creator")?;
1787 Ok((flags, options.ids.map(Vector::view), creator))
1788 }
1789
1790 /// Writes the graph to a Graphviz DOT file.
1791 ///
1792 /// The structure is written as `a -- b;` (`a -> b;` for directed
1793 /// graphs) lines after the list of vertices; igraph adds no layout or
1794 /// visualization information of its own. With the
1795 /// [attribute handler](crate::attributes::enable) on, the graph
1796 /// attributes are written in a `graph [ ... ]` block and every vertex and
1797 /// edge attribute in the `[ ... ]` block of its element (numbers as
1798 /// written by igraph, `NaN` included, booleans as 0/1 with a warning,
1799 /// strings and names quoted and escaped when needed). The format is meant
1800 /// for interoperability with Graphviz, not for data exchange. Time
1801 /// complexity: proportional to the output size.
1802 ///
1803 /// See [`Graph::layout_circle`] and the [`layout`](crate::layout) module
1804 /// to compute coordinates to add as attributes (e.g. `pos`).
1805 ///
1806 /// Binds [`igraph_write_graph_dot`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_dot).
1807 ///
1808 /// # Errors
1809 /// [`ErrorKind::File`] if the file can't be created or written.
1810 pub fn write_graph_dot(&self, path: impl AsRef<Path>) -> Result<()> {
1811 write_to_path(path.as_ref(), |f| unsafe {
1812 igraph_write_graph_dot(self, f)
1813 })
1814 }
1815
1816 /// The DOT serialization as a string, see [`write_graph_dot`](Self::write_graph_dot).
1817 ///
1818 /// # Examples
1819 /// ```
1820 /// use igraph::prelude::*;
1821 /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, true).unwrap();
1822 /// let dot = g.write_graph_dot_to_string().unwrap();
1823 /// assert!(dot.starts_with("/* Created by igraph"));
1824 /// assert!(dot.contains("digraph {") && dot.contains(" 0 -> 1;\n") && dot.contains(" 1 -> 2;\n"));
1825 /// ```
1826 pub fn write_graph_dot_to_string(&self) -> Result<String> {
1827 write_to_string(|f| unsafe { igraph_write_graph_dot(self, f) })
1828 }
1829
1830 /// Writes the graph in the LEDA native graph format.
1831 ///
1832 /// Only the graph section is written: the vertices, then the edges with
1833 /// 1-based endpoints (and, for undirected graphs, a `-2` direction flag).
1834 /// `vertex_attr`/`edge_attr` name one vertex and one edge attribute whose
1835 /// values are stored, with their LEDA type (`double`, `string` or `bool`)
1836 /// in the header; `void` is written for `None` and for attributes that
1837 /// don't exist (with a warning; always the case without the
1838 /// [attribute handler](crate::attributes::enable), see the
1839 /// [module docs](self#attributes)). Time complexity: O(|V| + |E|).
1840 ///
1841 /// Binds [`igraph_write_graph_leda`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_write_graph_leda).
1842 ///
1843 /// # Errors
1844 /// [`ErrorKind::File`] on I/O errors, [`ErrorKind::InvalidValue`] for
1845 /// attribute names with NUL bytes or string values containing a newline.
1846 pub fn write_graph_leda(
1847 &self,
1848 path: impl AsRef<Path>,
1849 vertex_attr: Option<&str>,
1850 edge_attr: Option<&str>,
1851 ) -> Result<()> {
1852 let (v, e) = (
1853 opt_cstring(vertex_attr, "vertex_attr")?,
1854 opt_cstring(edge_attr, "edge_attr")?,
1855 );
1856 write_to_path(path.as_ref(), |f| unsafe {
1857 igraph_write_graph_leda(self, f, opt_ptr(&v), opt_ptr(&e))
1858 })
1859 }
1860
1861 /// The LEDA serialization as a string, see [`write_graph_leda`](Self::write_graph_leda).
1862 ///
1863 /// # Examples
1864 /// ```
1865 /// use igraph::prelude::*;
1866 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], 3, true).unwrap();
1867 /// let leda = g.write_graph_leda_to_string(None, None).unwrap();
1868 /// assert_eq!(
1869 /// leda,
1870 /// "LEDA.GRAPH\nvoid\nvoid\n-1\n# Vertices\n3\n|{}|\n|{}|\n|{}|\n# Edges\n3\n\
1871 /// 1 2 0 |{}|\n2 3 0 |{}|\n3 1 0 |{}|\n"
1872 /// );
1873 /// ```
1874 pub fn write_graph_leda_to_string(
1875 &self,
1876 vertex_attr: Option<&str>,
1877 edge_attr: Option<&str>,
1878 ) -> Result<String> {
1879 let (v, e) = (
1880 opt_cstring(vertex_attr, "vertex_attr")?,
1881 opt_cstring(edge_attr, "edge_attr")?,
1882 );
1883 write_to_string(|f| unsafe { igraph_write_graph_leda(self, f, opt_ptr(&v), opt_ptr(&e)) })
1884 }
1885}
1886
1887// ---------------------------------------------------------------------------
1888// Locale
1889// ---------------------------------------------------------------------------
1890
1891/// RAII guard temporarily switching the numeric locale to `"C"`
1892/// (`igraph_enter_safelocale` / `igraph_exit_safelocale`).
1893///
1894/// igraph's readers and writers need a locale with a decimal *point*. While
1895/// the guard is alive the `"C"` numeric locale is in effect for the current
1896/// thread (on platforms without per-thread locales igraph falls back to the
1897/// process-wide `setlocale`, which is not thread safe); dropping it restores
1898/// the previous locale. Rust programs start in the `"C"` locale, so this is
1899/// only needed when some other code changed it.
1900///
1901/// The guard is neither [`Send`] nor [`Sync`]: it must be dropped on the
1902/// thread that created it. Nested guards restore their locales correctly
1903/// when dropped in reverse order of creation (as scopes do).
1904///
1905/// Binds [`igraph_enter_safelocale`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_enter_safelocale)
1906/// and [`igraph_exit_safelocale`](https://igraph.org/c/html/latest/igraph-Foreign.html#igraph_exit_safelocale).
1907///
1908/// ```
1909/// use igraph::{foreign::SafeLocale, prelude::*};
1910/// let g = Graph::from_edges(&[(0, 1)], 2, true).unwrap();
1911/// let text = {
1912/// let _locale = SafeLocale::enter().unwrap();
1913/// g.write_graph_dimacs_flow_to_string(0, 1, &[0.5]).unwrap()
1914/// }; // previous locale restored here
1915/// assert!(text.ends_with("a 1 2 0.5\n"));
1916/// ```
1917#[derive(Debug)]
1918pub struct SafeLocale {
1919 loc: igraph_safelocale_t,
1920}
1921
1922impl SafeLocale {
1923 /// Switches the current thread to the `"C"` numeric locale until the
1924 /// returned guard is dropped.
1925 ///
1926 /// # Errors
1927 /// [`ErrorKind::Failure`] if the `"C"` locale can't be created.
1928 pub fn enter() -> Result<Self> {
1929 let mut loc: igraph_safelocale_t = std::ptr::null_mut();
1930 if let Err(e) = igraph_call!(igraph_enter_safelocale(&mut loc)) {
1931 // igraph 1.0.0 and 1.0.1 return the error without freeing the
1932 // state they allocated (and never switched to): release it.
1933 if !loc.is_null() {
1934 unsafe { igraph_free(loc.cast()) };
1935 }
1936 return Err(e);
1937 }
1938 Ok(Self { loc })
1939 }
1940}
1941
1942impl Drop for SafeLocale {
1943 fn drop(&mut self) {
1944 if !self.loc.is_null() {
1945 unsafe { igraph_exit_safelocale(&mut self.loc) };
1946 }
1947 }
1948}
1949
1950/// Runs `f` with the `"C"` numeric locale in effect, see [`SafeLocale`].
1951///
1952/// # Errors
1953/// The errors of [`SafeLocale::enter`]; `f`'s own result is returned inside
1954/// the `Ok`.
1955///
1956/// ```
1957/// use igraph::{foreign::with_safe_locale, prelude::*};
1958/// let g = with_safe_locale(|| Graph::read_graph_edgelist_from_str("0 1", 0, false)).unwrap().unwrap();
1959/// assert_eq!(g.ecount(), 1);
1960/// ```
1961pub fn with_safe_locale<T>(f: impl FnOnce() -> T) -> Result<T> {
1962 let _guard = SafeLocale::enter()?;
1963 Ok(f())
1964}