1use crate::ordinal::{Ordinal, to_usize};
9
10#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12pub enum CsrError {
13 #[error("ordinal {ordinal} is out of range for {nodes} nodes")]
15 OutOfRange {
16 ordinal: Ordinal,
18 nodes: u32,
20 },
21 #[error("offsets are inconsistent with {targets} targets")]
23 Offsets {
24 targets: usize,
26 },
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Csr {
32 offsets: Vec<u32>,
33 targets: Vec<u32>,
34}
35
36impl Csr {
37 pub fn build(
46 nodes: u32,
47 edges: impl IntoIterator<Item = (Ordinal, Ordinal)>,
48 ) -> Result<Self, CsrError> {
49 let mut pairs: Vec<(u32, u32)> = Vec::new();
50 for (from, to) in edges {
51 for ordinal in [from, to] {
52 if ordinal.index() >= nodes {
53 return Err(CsrError::OutOfRange { ordinal, nodes });
54 }
55 }
56 pairs.push((from.index(), to.index()));
57 }
58 pairs.sort_unstable();
59 pairs.dedup();
60 let count = |targets: &Vec<u32>| {
63 let Ok(count) = u32::try_from(targets.len()) else {
64 return Err(CsrError::Offsets {
65 targets: targets.len(),
66 });
67 };
68 Ok(count)
69 };
70 let mut offsets = Vec::with_capacity(to_usize(nodes).saturating_add(1));
71 let mut targets = Vec::with_capacity(pairs.len());
72 let mut cursor = 0_usize;
73 for node in 0..nodes {
74 offsets.push(count(&targets)?);
75 while let Some((from, to)) = pairs.get(cursor).copied() {
76 if from != node {
77 break;
78 }
79 targets.push(to);
80 cursor = cursor.saturating_add(1);
81 }
82 }
83 offsets.push(count(&targets)?);
84 Ok(Self { offsets, targets })
85 }
86
87 pub fn from_parts(offsets: Vec<u32>, targets: Vec<u32>) -> Result<Self, CsrError> {
94 let consistent = offsets.first() == Some(&0)
95 && offsets.windows(2).all(|w| w.first() <= w.get(1))
96 && offsets.last().copied().map(to_usize) == Some(targets.len());
97 if !consistent {
98 return Err(CsrError::Offsets {
99 targets: targets.len(),
100 });
101 }
102 Ok(Self { offsets, targets })
103 }
104
105 #[must_use]
107 pub fn nodes(&self) -> u32 {
108 u32::try_from(self.offsets.len().saturating_sub(1)).unwrap_or(u32::MAX)
109 }
110
111 #[must_use]
113 pub fn edges(&self) -> usize {
114 self.targets.len()
115 }
116
117 #[must_use]
119 pub fn neighbours(&self, node: Ordinal) -> &[u32] {
120 let start = self.offsets.get(node.as_usize()).copied();
121 let end = self.offsets.get(node.as_usize().saturating_add(1)).copied();
122 match (start, end) {
123 (Some(start), Some(end)) => self
124 .targets
125 .get(to_usize(start)..to_usize(end))
126 .unwrap_or(&[]),
127 _ => &[],
128 }
129 }
130
131 #[must_use]
133 pub fn offsets(&self) -> &[u32] {
134 &self.offsets
135 }
136
137 #[must_use]
139 pub fn targets(&self) -> &[u32] {
140 &self.targets
141 }
142
143 pub fn transpose(&self) -> Result<Self, CsrError> {
149 let nodes = self.nodes();
150 let reversed = (0..nodes).flat_map(|from| {
151 self.neighbours(Ordinal::new(from))
152 .iter()
153 .map(move |to| (Ordinal::new(*to), Ordinal::new(from)))
154 });
155 Self::build(nodes, reversed)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{Csr, CsrError};
162 use crate::ordinal::Ordinal;
163
164 fn o(i: u32) -> Ordinal {
165 Ordinal::new(i)
166 }
167
168 #[test]
169 fn rows_are_sorted_and_deduplicated() {
170 let csr = Csr::build(4, [(o(2), o(1)), (o(0), o(3)), (o(0), o(1)), (o(0), o(1))])
171 .expect("builds");
172 assert_eq!(csr.nodes(), 4);
173 assert_eq!(csr.edges(), 3);
174 assert_eq!(csr.neighbours(o(0)), &[1, 3]);
175 assert!(csr.neighbours(o(1)).is_empty());
176 assert_eq!(csr.neighbours(o(2)), &[1]);
177 assert!(csr.neighbours(o(9)).is_empty());
178 assert_eq!(csr.offsets(), &[0, 2, 2, 3, 3]);
179 }
180
181 #[test]
182 fn transpose_reverses_every_edge() {
183 let csr = Csr::build(3, [(o(0), o(1)), (o(0), o(2)), (o(1), o(2))]).expect("builds");
184 let back = csr.transpose().expect("transposes");
185 assert_eq!(back.neighbours(o(2)), &[0, 1]);
186 assert_eq!(back.neighbours(o(1)), &[0]);
187 assert!(back.neighbours(o(0)).is_empty());
188 assert_eq!(back.transpose().expect("transposes"), csr);
189 }
190
191 #[test]
192 fn out_of_range_and_inconsistent_parts_are_refused() {
193 assert_eq!(
194 Csr::build(2, [(o(0), o(2))]),
195 Err(CsrError::OutOfRange {
196 ordinal: o(2),
197 nodes: 2
198 })
199 );
200 assert!(Csr::from_parts(vec![0, 2, 1], vec![1]).is_err());
201 assert!(Csr::from_parts(vec![1, 1], Vec::new()).is_err());
202 assert!(Csr::from_parts(vec![0, 1], vec![0]).is_ok());
203 }
204}