Skip to main content

weavatrix_graph/format/
graph6.rs

1use crate::{
2    BitMatrix, EdgeEndpoints, GraphError, IndexUndirectedGraphView, NodeIndex, Result,
3    UndirectedTopology,
4};
5use crate::{String, Vec};
6
7/// Encodes a compact simple undirected graph in Graph6 form.
8///
9/// # Errors
10///
11/// Returns an error for non-compact indexes, loops, parallel edges, or capacity
12/// overflow because Graph6 cannot represent those contracts.
13pub fn graph6_encode<G>(graph: &G) -> Result<String>
14where
15    G: IndexUndirectedGraphView,
16{
17    if graph.node_bound() != graph.node_count() {
18        return Err(unsupported("non-compact node indexes"));
19    }
20    let mut seen = BitMatrix::try_new(graph.node_count())?;
21    for node in graph.node_indices() {
22        if G::node_slot(node) >= graph.node_count() {
23            return Err(unsupported("non-compact node indexes"));
24        }
25    }
26    for (_, endpoints) in graph
27        .edge_indices()
28        .filter_map(|edge| graph.edge_endpoints(edge).map(|ends| (edge, ends)))
29    {
30        let mut left = G::node_slot(endpoints.source());
31        let mut right = G::node_slot(endpoints.target());
32        if left == right {
33            return Err(unsupported("self-loops"));
34        }
35        if left > right {
36            core::mem::swap(&mut left, &mut right);
37        }
38        if !seen.insert(node(left), node(right))? {
39            return Err(unsupported("parallel edges"));
40        }
41    }
42    let mut encoded = encode_node_count(graph.node_count())?;
43    let mut value = 0_u8;
44    let mut width = 0_u8;
45    for right in 1..graph.node_count() {
46        for left in 0..right {
47            value = (value << 1) | u8::from(seen.contains(node(left), node(right)));
48            width += 1;
49            if width == 6 {
50                encoded.push(char::from(value + 63));
51                value = 0;
52                width = 0;
53            }
54        }
55    }
56    if width != 0 {
57        encoded.push(char::from((value << (6 - width)) + 63));
58    }
59    Ok(encoded)
60}
61
62/// Decodes one Graph6 record into a compact undirected topology.
63///
64/// # Errors
65///
66/// Returns an error for malformed headers, lengths, padding, or capacity.
67pub fn graph6_decode(input: &str) -> Result<UndirectedTopology> {
68    let record = input.trim();
69    let record = record.strip_prefix(">>graph6<<").unwrap_or(record);
70    let bytes = record.as_bytes();
71    let (node_count, offset) = decode_node_count(bytes)?;
72    let bits = node_count
73        .checked_mul(node_count.saturating_sub(1))
74        .ok_or_else(|| invalid("node count overflows adjacency bits"))?
75        / 2;
76    let encoded_len = bits
77        .checked_add(5)
78        .ok_or_else(|| invalid("adjacency length overflow"))?
79        / 6;
80    if bytes.len() != offset + encoded_len {
81        return Err(invalid("adjacency payload has the wrong length"));
82    }
83    let values = bytes[offset..]
84        .iter()
85        .map(|byte| six(*byte))
86        .collect::<Result<Vec<_>>>()?;
87    if let Some(last) = values.last() {
88        let used = bits % 6;
89        if used != 0 && last & ((1_u8 << (6 - used)) - 1) != 0 {
90            return Err(invalid("nonzero Graph6 padding bits"));
91        }
92    }
93    let mut edges = Vec::new();
94    let mut position = 0_usize;
95    for right in 1..node_count {
96        for left in 0..right {
97            let value = values[position / 6];
98            let bit = 5 - position % 6;
99            if value & (1_u8 << bit) != 0 {
100                edges.push(EdgeEndpoints::new(node(left), node(right)));
101            }
102            position += 1;
103        }
104    }
105    UndirectedTopology::try_from_edges(node_count, edges)
106}
107
108fn encode_node_count(node_count: usize) -> Result<String> {
109    let count = u64::try_from(node_count).map_err(|_| GraphError::IndexCapacityExceeded {
110        category: "Graph6 nodes",
111        count: node_count,
112    })?;
113    let mut output = String::new();
114    if count <= 62 {
115        push_six(&mut output, count)?;
116    } else if count <= 258_047 {
117        output.push('~');
118        for shift in [12, 6, 0] {
119            push_six(&mut output, (count >> shift) & 0x3f)?;
120        }
121    } else if count <= 0xffff_fffff {
122        output.push_str("~~");
123        for shift in [30, 24, 18, 12, 6, 0] {
124            push_six(&mut output, (count >> shift) & 0x3f)?;
125        }
126    } else {
127        return Err(GraphError::IndexCapacityExceeded {
128            category: "Graph6 nodes",
129            count: node_count,
130        });
131    }
132    Ok(output)
133}
134
135fn decode_node_count(bytes: &[u8]) -> Result<(usize, usize)> {
136    let Some(&first) = bytes.first() else {
137        return Err(invalid("empty record"));
138    };
139    if first != b'~' {
140        return Ok((usize::from(six(first)?), 1));
141    }
142    if bytes.get(1) != Some(&b'~') {
143        return Ok((decode_groups(bytes, 1, 3)?, 4));
144    }
145    Ok((decode_groups(bytes, 2, 6)?, 8))
146}
147
148fn decode_groups(bytes: &[u8], offset: usize, count: usize) -> Result<usize> {
149    let slice = bytes
150        .get(offset..offset + count)
151        .ok_or_else(|| invalid("truncated node-count header"))?;
152    slice.iter().try_fold(0_usize, |value, byte| {
153        Ok((value << 6) | usize::from(six(*byte)?))
154    })
155}
156
157fn push_six(output: &mut String, value: u64) -> Result<()> {
158    let value = u8::try_from(value).map_err(|_| invalid("invalid six-bit value"))?;
159    output.push(char::from(value + 63));
160    Ok(())
161}
162
163fn six(byte: u8) -> Result<u8> {
164    byte.checked_sub(63)
165        .filter(|value| *value < 64)
166        .ok_or_else(|| invalid("character is outside the Graph6 alphabet"))
167}
168
169fn node(index: usize) -> NodeIndex {
170    NodeIndex::new(u32::try_from(index).unwrap_or(u32::MAX))
171}
172
173fn invalid(reason: &str) -> GraphError {
174    GraphError::InvalidFormat {
175        format: "Graph6",
176        reason: String::from(reason),
177    }
178}
179
180fn unsupported(feature: &'static str) -> GraphError {
181    GraphError::UnsupportedGraphFeature {
182        format: "Graph6",
183        feature,
184    }
185}