1use std::collections::{BTreeMap, BTreeSet, HashSet};
15
16use consortium::range_set::RangeSet;
17
18pub fn parse_fold_axis(axis: &str) -> Result<Vec<i64>, String> {
37 if !axis.starts_with('-') {
38 let rs = RangeSet::parse(axis, None).map_err(|e| e.to_string())?;
40 Ok(rs.intiter().filter(|x| *x > 0).map(|x| x - 1).collect())
41 } else {
42 axis.parse::<i64>()
44 .map(|v| vec![v])
45 .map_err(|_| format!("invalid literal for int() with base 10: '{axis}'"))
46 }
47}
48
49fn scan_node(name: &str) -> (String, Vec<(i64, u32)>) {
52 let mut template = String::new();
53 let mut indices = Vec::new();
54
55 let chars: Vec<char> = name.chars().collect();
56 let len = chars.len();
57 let mut i = 0;
58
59 while i < len {
60 let prefix_start = i;
61 while i < len && !chars[i].is_ascii_digit() {
62 i += 1;
63 }
64 template.push_str(&name[prefix_start..i]);
65
66 let digit_start = i;
67 while i < len && chars[i].is_ascii_digit() {
68 i += 1;
69 }
70 let digits = &name[digit_start..i];
71
72 if !digits.is_empty() {
73 template.push_str("%s");
74 let value: i64 = digits.parse().unwrap_or(0);
75 let pad = if value != 0 {
76 let stripped = digits.trim_start_matches('0');
77 if digits.len() > stripped.len() {
78 digits.len() as u32
79 } else {
80 0
81 }
82 } else if digits.len() > 1 {
83 digits.len() as u32
84 } else {
85 0
86 };
87 indices.push((value, pad));
88 }
89 }
90
91 (template, indices)
92}
93
94pub fn fold_nodes<S: AsRef<str>>(names: &[S], fold_axis: Option<&[i64]>) -> String {
108 let mut groups: BTreeMap<String, BTreeSet<Vec<(i64, u32)>>> = BTreeMap::new();
110 for name in names {
111 let (template, indices) = scan_node(name.as_ref());
112 groups.entry(template).or_default().insert(indices);
113 }
114
115 let mut results: Vec<String> = Vec::new();
116 for (template, vectors) in &groups {
117 let dim = vectors.iter().next().map_or(0, Vec::len);
118 if dim == 0 {
119 results.push(template.clone());
121 } else if dim == 1 {
122 results.extend(fold_1d(template, vectors, fold_axis));
123 } else {
124 results.extend(fold_nd(template, vectors, fold_axis));
125 }
126 }
127 results.join(",")
128}
129
130fn rangeset_of<I: IntoIterator<Item = (i64, u32)>>(items: I) -> RangeSet {
132 let mut rs = RangeSet::new();
133 for (value, pad) in items {
134 rs.add_int(value, pad);
135 }
136 rs
137}
138
139fn fold_1d(
142 template: &str,
143 vectors: &BTreeSet<Vec<(i64, u32)>>,
144 fold_axis: Option<&[i64]>,
145) -> Vec<String> {
146 let rs = rangeset_of(vectors.iter().map(|v| v[0]));
147
148 let may_fold = fold_axis.is_none()
150 || fold_axis
151 .map(|axes| axes.iter().any(|x| (-1..1).contains(x)))
152 .unwrap_or(false);
153
154 if may_fold {
155 if rs.len() > 1 {
156 vec![template.replacen("%s", &format!("[{rs}]"), 1)]
157 } else {
158 vec![template.replacen("%s", &rs.to_string(), 1)]
159 }
160 } else {
161 rs.striter()
162 .map(|s| template.replacen("%s", &s, 1))
163 .collect()
164 }
165}
166
167fn fold_nd(
170 template: &str,
171 vectors: &BTreeSet<Vec<(i64, u32)>>,
172 fold_axis: Option<&[i64]>,
173) -> Vec<String> {
174 let dim = vectors.iter().next().map_or(0, Vec::len);
175
176 let mut veclist: Vec<Vec<RangeSet>> = Vec::new();
178 for ivec in vectors {
179 let singletons: Vec<RangeSet> = ivec.iter().map(|&(v, p)| rangeset_of([(v, p)])).collect();
180 if !veclist.contains(&singletons) {
181 veclist.push(singletons);
182 }
183 }
184
185 fold_veclist(&mut veclist);
186
187 let dimcnt = dim as i64;
190 let fold_set: HashSet<usize> = match fold_axis {
191 None => (0..dim).collect(),
192 Some(axes) => axes
193 .iter()
194 .filter(|x| **x >= -dimcnt && **x < dimcnt)
195 .map(|x| x.rem_euclid(dimcnt) as usize)
196 .collect(),
197 };
198
199 let mut results = Vec::new();
200 for rgvec in &veclist {
201 let mut combos: Vec<Vec<String>> = vec![Vec::new()];
204 for (axis, rs) in rgvec.iter().enumerate() {
205 let strings: Vec<String> = if rs.len() > 1 {
206 if fold_set.contains(&axis) {
207 vec![format!("[{rs}]")]
208 } else {
209 rs.striter().collect()
210 }
211 } else {
212 vec![rs.to_string()]
213 };
214 let mut next = Vec::new();
215 for s in &strings {
216 for combo in &combos {
217 let mut extended = combo.clone();
218 extended.push(s.clone());
219 next.push(extended);
220 }
221 }
222 combos = next;
223 }
224 for combo in combos {
225 let mut rendered = template.to_string();
226 for part in &combo {
227 rendered = rendered.replacen("%s", part, 1);
228 }
229 results.push(rendered);
230 }
231 }
232 results
233}
234
235fn fold_veclist(veclist: &mut Vec<Vec<RangeSet>>) {
238 if veclist.len() > 1 && !fold_univariate(veclist) {
239 fold_multivariate_merge(veclist);
240 } else {
241 sort_veclist(veclist);
242 }
243}
244
245fn fold_univariate(veclist: &mut Vec<Vec<RangeSet>>) -> bool {
248 let dim = veclist[0].len();
249 let mut vardim = 0usize;
250 let mut dimdiff = 0;
251 if dim > 1 {
252 for i in 0..dim {
253 if veclist.iter().any(|vec| vec[i] != veclist[0][i]) {
255 dimdiff += 1;
256 if dimdiff > 1 {
257 break;
258 }
259 vardim = i;
260 }
261 }
262 }
263 let univar = dim == 1 || dimdiff == 1;
264 if univar {
265 for k in 1..veclist.len() {
266 let rs = veclist[k][vardim].clone();
267 veclist[0][vardim].update(&rs);
268 }
269 veclist.truncate(1);
270 }
271 univar
272}
273
274fn fold_multivariate_merge(veclist: &mut Vec<Vec<RangeSet>>) {
278 let mut full = false; let mut chg = true; while chg {
281 chg = false;
282 sort_veclist(veclist); let mut index1 = 0;
284 while index1 + 1 < veclist.len() {
285 let mut index2 = index1 + 1;
288 while index2 < veclist.len() {
289 match try_merge_pair(&veclist[index1], &veclist[index2]) {
290 Some(new_item) => {
291 chg = true;
294 veclist[index1] = new_item;
295 veclist.remove(index2);
296 }
297 None => {
298 index2 += 1;
299 if !full {
300 break;
303 }
304 }
305 }
306 }
307 index1 += 1;
308 }
309 if !chg && !full {
310 chg = true;
315 full = true;
316 }
317 }
318}
319
320fn try_merge_pair(item1: &[RangeSet], item2: &[RangeSet]) -> Option<Vec<RangeSet>> {
326 let mut new_item: Vec<RangeSet> = Vec::with_capacity(item1.len());
327 let mut nb_diff = 0;
328 for (rg1, rg2) in item1.iter().zip(item2.iter()) {
329 if rg1 == rg2 {
330 new_item.push(rg1.clone());
331 } else if rg1.intersection(rg2).is_empty() {
332 nb_diff += 1;
334 if nb_diff > 1 {
335 return None;
336 }
337 new_item.push(rg1.union(rg2));
338 } else if strict_superset(rg1, rg2) {
339 nb_diff += 1;
341 if nb_diff > 1 {
342 return None;
343 }
344 new_item.push(rg1.clone());
345 } else if strict_superset(rg2, rg1) {
346 nb_diff += 1;
347 if nb_diff > 1 {
348 return None;
349 }
350 new_item.push(rg2.clone());
351 } else {
352 return None;
354 }
355 }
356 Some(new_item)
357}
358
359fn strict_superset(rg1: &RangeSet, rg2: &RangeSet) -> bool {
362 rg1.len() > rg2.len() && rg1.intersection(rg2).len() == rg2.len()
363}
364
365fn sort_veclist(veclist: &mut [Vec<RangeSet>]) {
371 veclist.sort_by_key(|rgvec| rgvec_key(rgvec));
372}
373
374type RgvecKey = (
375 std::cmp::Reverse<u128>,
376 Vec<(std::cmp::Reverse<usize>, i64, i64)>,
377);
378
379fn rgvec_key(rgvec: &[RangeSet]) -> RgvecKey {
380 let product: u128 = rgvec.iter().map(|rg| rg.len() as u128).product();
381 let dims = rgvec
382 .iter()
383 .map(|rg| {
384 let first = rg.intiter().next().unwrap_or(0);
385 let last = rg.intiter().last().unwrap_or(first);
386 (std::cmp::Reverse(rg.len()), first, last)
387 })
388 .collect();
389 (std::cmp::Reverse(product), dims)
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 fn fold(names: &[&str], fold_axis: Option<&[i64]>) -> String {
397 fold_nodes(names, fold_axis)
398 }
399
400 #[test]
403 fn parse_axis_single() {
404 assert_eq!(parse_fold_axis("1").unwrap(), vec![0]);
405 assert_eq!(parse_fold_axis("2").unwrap(), vec![1]);
406 }
407
408 #[test]
409 fn parse_axis_rangeset() {
410 assert_eq!(parse_fold_axis("1-2").unwrap(), vec![0, 1]);
411 assert_eq!(parse_fold_axis("1,3").unwrap(), vec![0, 2]);
412 }
413
414 #[test]
415 fn parse_axis_zero_ignored() {
416 assert_eq!(parse_fold_axis("0").unwrap(), Vec::<i64>::new());
418 assert_eq!(parse_fold_axis("0,2").unwrap(), vec![1]);
419 }
420
421 #[test]
422 fn parse_axis_negative() {
423 assert_eq!(parse_fold_axis("-1").unwrap(), vec![-1]);
424 assert_eq!(parse_fold_axis("-2").unwrap(), vec![-2]);
425 }
426
427 #[test]
428 fn parse_axis_errors() {
429 assert!(parse_fold_axis("abc").is_err());
430 assert!(parse_fold_axis("-x").is_err());
431 assert!(parse_fold_axis("-").is_err());
432 }
433
434 #[test]
437 fn fold_1d_default() {
438 assert_eq!(
439 fold(&["node1", "node2", "node3", "node4"], None),
440 "node[1-4]"
441 );
442 }
443
444 #[test]
445 fn fold_1d_axis1_folds() {
446 assert_eq!(
448 fold(&["node1", "node2", "node3", "node4"], Some(&[0])),
449 "node[1-4]"
450 );
451 }
452
453 #[test]
454 fn fold_1d_axis_minus1_folds() {
455 assert_eq!(
457 fold(&["node1", "node2", "node3", "node4"], Some(&[-1])),
458 "node[1-4]"
459 );
460 }
461
462 #[test]
463 fn fold_1d_axis2_expands() {
464 assert_eq!(
466 fold(&["node1", "node2", "node3", "node4"], Some(&[1])),
467 "node1,node2,node3,node4"
468 );
469 }
470
471 #[test]
472 fn fold_1d_single_element_no_brackets() {
473 assert_eq!(fold(&["node3"], None), "node3");
474 }
475
476 #[test]
477 fn fold_1d_padding_preserved() {
478 assert_eq!(fold(&["prune003", "prune004"], None), "prune[003-004]");
479 }
480
481 #[test]
482 fn fold_plain_nodes() {
483 assert_eq!(fold(&["switch"], None), "switch");
484 assert_eq!(fold(&["node1", "switch"], None), "node1,switch");
485 }
486
487 #[test]
490 fn fold_nd_default_folds_all_axes() {
491 let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
492 assert_eq!(fold(&names, None), "foo[1-2]-[1-2]");
493 }
494
495 #[test]
496 fn fold_nd_axis1() {
497 let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
498 assert_eq!(fold(&names, Some(&[0])), "foo[1-2]-1,foo[1-2]-2");
499 }
500
501 #[test]
502 fn fold_nd_axis2() {
503 let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
504 assert_eq!(fold(&names, Some(&[1])), "foo1-[1-2],foo2-[1-2]");
505 }
506
507 #[test]
508 fn fold_nd_axis_minus1() {
509 let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
511 assert_eq!(fold(&names, Some(&[-1])), "foo1-[1-2],foo2-[1-2]");
512 }
513
514 #[test]
515 fn fold_nd_axis_minus2() {
516 let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
518 assert_eq!(fold(&names, Some(&[-2])), "foo[1-2]-1,foo[1-2]-2");
519 }
520
521 #[test]
522 fn fold_nd_out_of_range_axis_expands_everything() {
523 let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
526 assert_eq!(fold(&names, Some(&[2])), "foo1-1,foo2-1,foo1-2,foo2-2");
527 }
528
529 #[test]
530 fn fold_nd_multivariate_keeps_larger_vector_first() {
531 let names = [
533 "da1p1", "da1p2", "da1p3", "da2p1", "da2p2", "da2p3", "da5p1", "da5p2", "da6p1",
534 "da6p2",
535 ];
536 assert_eq!(fold(&names, None), "da[1-2]p[1-3],da[5-6]p[1-2]");
537 }
538
539 #[test]
540 fn fold_nd_univariate_merges_along_varying_dim() {
541 let names = ["r1n1", "r1n2", "r1n3"];
543 assert_eq!(fold(&names, None), "r1n[1-3]");
544 }
545
546 #[test]
547 fn fold_nd_partial_overlap_does_not_merge() {
548 let names = ["a1b1", "a2b1", "a2b2", "a3b2"];
550 assert_eq!(fold(&names, None), "a[1-2]b1,a[2-3]b2");
551 }
552
553 #[test]
554 fn fold_nd_contained_range_keeps_largest() {
555 let names = ["a1b1", "a1b2", "a2b1"];
558 assert_eq!(fold(&names, None), "a1b[1-2],a2b1");
559 }
560
561 #[test]
562 fn fold_nd_3d_full_cartesian() {
563 let names = [
564 "a1b1c1", "a1b1c2", "a1b2c1", "a1b2c2", "a2b1c1", "a2b1c2", "a2b2c1", "a2b2c2",
565 ];
566 assert_eq!(fold(&names, None), "a[1-2]b[1-2]c[1-2]");
567 assert_eq!(
569 fold(&names, Some(&[2])),
570 "a1b1c[1-2],a2b1c[1-2],a1b2c[1-2],a2b2c[1-2]"
571 );
572 }
573
574 #[test]
575 fn fold_nd_padding_preserved() {
576 let names = ["n01-1", "n01-2", "n02-1", "n02-2"];
577 assert_eq!(fold(&names, None), "n[01-02]-[1-2]");
578 }
579
580 #[test]
581 fn fold_mixed_templates_sorted() {
582 let names = ["node1", "bmc10", "node2", "bmc11"];
583 assert_eq!(fold(&names, None), "bmc[10-11],node[1-2]");
584 }
585}