1use std::collections::BTreeMap;
21
22use crate::select::{Row, Selection};
23use crate::spec::Grouping;
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct Group<'a> {
28 pub key: String,
30 pub rows: Vec<&'a Row>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct RowSet<'a> {
37 pub view: String,
39 pub groups: Vec<Group<'a>>,
41 pub ungrouped: Vec<&'a Row>,
50}
51
52impl RowSet<'_> {
53 pub fn len(&self) -> usize {
60 let mut paths: Vec<_> = self
61 .groups
62 .iter()
63 .flat_map(|g| &g.rows)
64 .chain(&self.ungrouped)
65 .map(|r| &r.path)
66 .collect();
67 paths.sort();
68 paths.dedup();
69 paths.len()
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.groups.is_empty() && self.ungrouped.is_empty()
75 }
76
77 pub fn placements(&self) -> usize {
80 self.groups.iter().map(|g| g.rows.len()).sum::<usize>() + self.ungrouped.len()
81 }
82}
83
84pub fn group<'a>(selection: &'a Selection, grouping: &Grouping) -> RowSet<'a> {
90 let mut grouped: BTreeMap<String, Vec<&'a Row>> = BTreeMap::new();
91 let mut ungrouped: Vec<&'a Row> = Vec::new();
92
93 for row in &selection.rows {
94 let keys = grouping.keys_of(&row.meta);
95 if keys.is_empty() {
96 ungrouped.push(row);
97 continue;
98 }
99 for key in keys {
100 let bucket = grouped.entry(key).or_default();
101 if !bucket.iter().any(|r| r.path == row.path) {
104 bucket.push(row);
105 }
106 }
107 }
108
109 let groups = grouped
112 .into_iter()
113 .map(|(key, rows)| Group { key, rows })
114 .collect();
115
116 RowSet {
117 view: selection.view.clone(),
118 groups,
119 ungrouped,
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::spec::Grain;
127 use prov_graph::meta::{Mapping, Value};
128 use std::path::PathBuf;
129
130 fn selection(rows: &[(&str, &[(&str, Value)])]) -> Selection {
133 Selection {
134 view: "v".into(),
135 rows: rows
136 .iter()
137 .map(|(path, fields)| {
138 let mut meta = Mapping::new();
139 for (k, v) in *fields {
140 meta.insert((*k).into(), v.clone());
141 }
142 Row {
143 path: PathBuf::from(path),
144 meta: Value::Mapping(meta),
145 }
146 })
147 .collect(),
148 }
149 }
150
151 fn text(s: &str) -> Value {
152 Value::String(s.to_string())
153 }
154
155 fn seq(items: &[&str]) -> Value {
156 Value::Sequence(items.iter().map(|s| text(s)).collect())
157 }
158
159 #[test]
160 fn groups_are_ascending_and_rows_stay_in_path_order() {
161 let sel = selection(&[
162 ("b.md", &[("created", text("2026-08-01"))]),
163 ("a.md", &[("created", text("2026-07-24"))]),
164 ("c.md", &[("created", text("2026-07-30"))]),
165 ]);
166 let rows = group(
167 &sel,
168 &Grouping {
169 keys: vec!["created".into()],
170 by: Some(Grain::Month),
171 },
172 );
173 assert_eq!(
174 rows.groups
175 .iter()
176 .map(|g| g.key.as_str())
177 .collect::<Vec<_>>(),
178 ["2026-07", "2026-08"]
179 );
180 assert_eq!(
181 rows.groups[0]
182 .rows
183 .iter()
184 .map(|r| r.path.to_str().unwrap())
185 .collect::<Vec<_>>(),
186 ["a.md", "c.md"]
187 );
188 }
189
190 #[test]
193 fn len_counts_documents_and_placements_counts_rows() {
194 let sel = selection(&[
195 ("letter.md", &[("people", seq(&["Ada", "Grace"]))]),
196 ("note.md", &[("people", seq(&["Ada"]))]),
197 ("bare.md", &[]),
198 ]);
199 let rows = group(&sel, &Grouping::field("people"));
200
201 assert_eq!(rows.len(), 3, "three documents");
202 assert_eq!(
203 rows.placements(),
204 4,
205 "Ada twice, Grace once, ungrouped once"
206 );
207 assert_eq!(rows.len(), sel.len(), "nothing selected went missing");
208 }
209
210 #[test]
212 fn a_repeated_value_does_not_double_a_row_within_its_group() {
213 let sel = selection(&[("letter.md", &[("people", seq(&["Ada", "Ada"]))])]);
214 let rows = group(&sel, &Grouping::field("people"));
215 assert_eq!(rows.groups.len(), 1);
216 assert_eq!(rows.groups[0].rows.len(), 1);
217 }
218
219 #[test]
221 fn every_selected_row_lands_somewhere() {
222 let sel = selection(&[
223 ("a.md", &[("created", text("2026-07-24"))]),
224 ("b.md", &[("created", text("banana"))]),
225 ("c.md", &[]),
226 ]);
227 let rows = group(
228 &sel,
229 &Grouping {
230 keys: vec!["created".into()],
231 by: Some(Grain::Year),
232 },
233 );
234 assert_eq!(rows.len(), 3);
235 assert_eq!(rows.ungrouped.len(), 2, "the unparseable and the absent");
236 }
237
238 #[test]
241 fn one_selection_groups_several_ways_at_once() {
242 let sel = selection(&[(
243 "letter.md",
244 &[("people", seq(&["Ada"])), ("created", text("2026-07-24"))],
245 )]);
246 let by_people = group(&sel, &Grouping::field("people"));
247 let by_year = group(
248 &sel,
249 &Grouping {
250 keys: vec!["created".into()],
251 by: Some(Grain::Year),
252 },
253 );
254 assert_eq!(by_people.groups[0].key, "Ada");
255 assert_eq!(by_year.groups[0].key, "2026");
256 assert_eq!(
257 by_people.groups[0].rows[0].path,
258 by_year.groups[0].rows[0].path
259 );
260 }
261}