use std::collections::{BTreeMap, BTreeSet, HashSet};
use consortium::range_set::RangeSet;
pub fn parse_fold_axis(axis: &str) -> Result<Vec<i64>, String> {
if !axis.starts_with('-') {
let rs = RangeSet::parse(axis, None).map_err(|e| e.to_string())?;
Ok(rs.intiter().filter(|x| *x > 0).map(|x| x - 1).collect())
} else {
axis.parse::<i64>()
.map(|v| vec![v])
.map_err(|_| format!("invalid literal for int() with base 10: '{axis}'"))
}
}
fn scan_node(name: &str) -> (String, Vec<(i64, u32)>) {
let mut template = String::new();
let mut indices = Vec::new();
let chars: Vec<char> = name.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
let prefix_start = i;
while i < len && !chars[i].is_ascii_digit() {
i += 1;
}
template.push_str(&name[prefix_start..i]);
let digit_start = i;
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
let digits = &name[digit_start..i];
if !digits.is_empty() {
template.push_str("%s");
let value: i64 = digits.parse().unwrap_or(0);
let pad = if value != 0 {
let stripped = digits.trim_start_matches('0');
if digits.len() > stripped.len() {
digits.len() as u32
} else {
0
}
} else if digits.len() > 1 {
digits.len() as u32
} else {
0
};
indices.push((value, pad));
}
}
(template, indices)
}
pub fn fold_nodes<S: AsRef<str>>(names: &[S], fold_axis: Option<&[i64]>) -> String {
let mut groups: BTreeMap<String, BTreeSet<Vec<(i64, u32)>>> = BTreeMap::new();
for name in names {
let (template, indices) = scan_node(name.as_ref());
groups.entry(template).or_default().insert(indices);
}
let mut results: Vec<String> = Vec::new();
for (template, vectors) in &groups {
let dim = vectors.iter().next().map_or(0, Vec::len);
if dim == 0 {
results.push(template.clone());
} else if dim == 1 {
results.extend(fold_1d(template, vectors, fold_axis));
} else {
results.extend(fold_nd(template, vectors, fold_axis));
}
}
results.join(",")
}
fn rangeset_of<I: IntoIterator<Item = (i64, u32)>>(items: I) -> RangeSet {
let mut rs = RangeSet::new();
for (value, pad) in items {
rs.add_int(value, pad);
}
rs
}
fn fold_1d(
template: &str,
vectors: &BTreeSet<Vec<(i64, u32)>>,
fold_axis: Option<&[i64]>,
) -> Vec<String> {
let rs = rangeset_of(vectors.iter().map(|v| v[0]));
let may_fold = fold_axis.is_none()
|| fold_axis
.map(|axes| axes.iter().any(|x| (-1..1).contains(x)))
.unwrap_or(false);
if may_fold {
if rs.len() > 1 {
vec![template.replacen("%s", &format!("[{rs}]"), 1)]
} else {
vec![template.replacen("%s", &rs.to_string(), 1)]
}
} else {
rs.striter()
.map(|s| template.replacen("%s", &s, 1))
.collect()
}
}
fn fold_nd(
template: &str,
vectors: &BTreeSet<Vec<(i64, u32)>>,
fold_axis: Option<&[i64]>,
) -> Vec<String> {
let dim = vectors.iter().next().map_or(0, Vec::len);
let mut veclist: Vec<Vec<RangeSet>> = Vec::new();
for ivec in vectors {
let singletons: Vec<RangeSet> = ivec.iter().map(|&(v, p)| rangeset_of([(v, p)])).collect();
if !veclist.contains(&singletons) {
veclist.push(singletons);
}
}
fold_veclist(&mut veclist);
let dimcnt = dim as i64;
let fold_set: HashSet<usize> = match fold_axis {
None => (0..dim).collect(),
Some(axes) => axes
.iter()
.filter(|x| **x >= -dimcnt && **x < dimcnt)
.map(|x| x.rem_euclid(dimcnt) as usize)
.collect(),
};
let mut results = Vec::new();
for rgvec in &veclist {
let mut combos: Vec<Vec<String>> = vec![Vec::new()];
for (axis, rs) in rgvec.iter().enumerate() {
let strings: Vec<String> = if rs.len() > 1 {
if fold_set.contains(&axis) {
vec![format!("[{rs}]")]
} else {
rs.striter().collect()
}
} else {
vec![rs.to_string()]
};
let mut next = Vec::new();
for s in &strings {
for combo in &combos {
let mut extended = combo.clone();
extended.push(s.clone());
next.push(extended);
}
}
combos = next;
}
for combo in combos {
let mut rendered = template.to_string();
for part in &combo {
rendered = rendered.replacen("%s", part, 1);
}
results.push(rendered);
}
}
results
}
fn fold_veclist(veclist: &mut Vec<Vec<RangeSet>>) {
if veclist.len() > 1 && !fold_univariate(veclist) {
fold_multivariate_merge(veclist);
} else {
sort_veclist(veclist);
}
}
fn fold_univariate(veclist: &mut Vec<Vec<RangeSet>>) -> bool {
let dim = veclist[0].len();
let mut vardim = 0usize;
let mut dimdiff = 0;
if dim > 1 {
for i in 0..dim {
if veclist.iter().any(|vec| vec[i] != veclist[0][i]) {
dimdiff += 1;
if dimdiff > 1 {
break;
}
vardim = i;
}
}
}
let univar = dim == 1 || dimdiff == 1;
if univar {
for k in 1..veclist.len() {
let rs = veclist[k][vardim].clone();
veclist[0][vardim].update(&rs);
}
veclist.truncate(1);
}
univar
}
fn fold_multivariate_merge(veclist: &mut Vec<Vec<RangeSet>>) {
let mut full = false; let mut chg = true; while chg {
chg = false;
sort_veclist(veclist); let mut index1 = 0;
while index1 + 1 < veclist.len() {
let mut index2 = index1 + 1;
while index2 < veclist.len() {
match try_merge_pair(&veclist[index1], &veclist[index2]) {
Some(new_item) => {
chg = true;
veclist[index1] = new_item;
veclist.remove(index2);
}
None => {
index2 += 1;
if !full {
break;
}
}
}
}
index1 += 1;
}
if !chg && !full {
chg = true;
full = true;
}
}
}
fn try_merge_pair(item1: &[RangeSet], item2: &[RangeSet]) -> Option<Vec<RangeSet>> {
let mut new_item: Vec<RangeSet> = Vec::with_capacity(item1.len());
let mut nb_diff = 0;
for (rg1, rg2) in item1.iter().zip(item2.iter()) {
if rg1 == rg2 {
new_item.push(rg1.clone());
} else if rg1.intersection(rg2).is_empty() {
nb_diff += 1;
if nb_diff > 1 {
return None;
}
new_item.push(rg1.union(rg2));
} else if strict_superset(rg1, rg2) {
nb_diff += 1;
if nb_diff > 1 {
return None;
}
new_item.push(rg1.clone());
} else if strict_superset(rg2, rg1) {
nb_diff += 1;
if nb_diff > 1 {
return None;
}
new_item.push(rg2.clone());
} else {
return None;
}
}
Some(new_item)
}
fn strict_superset(rg1: &RangeSet, rg2: &RangeSet) -> bool {
rg1.len() > rg2.len() && rg1.intersection(rg2).len() == rg2.len()
}
fn sort_veclist(veclist: &mut [Vec<RangeSet>]) {
veclist.sort_by_key(|rgvec| rgvec_key(rgvec));
}
type RgvecKey = (
std::cmp::Reverse<u128>,
Vec<(std::cmp::Reverse<usize>, i64, i64)>,
);
fn rgvec_key(rgvec: &[RangeSet]) -> RgvecKey {
let product: u128 = rgvec.iter().map(|rg| rg.len() as u128).product();
let dims = rgvec
.iter()
.map(|rg| {
let first = rg.intiter().next().unwrap_or(0);
let last = rg.intiter().last().unwrap_or(first);
(std::cmp::Reverse(rg.len()), first, last)
})
.collect();
(std::cmp::Reverse(product), dims)
}
#[cfg(test)]
mod tests {
use super::*;
fn fold(names: &[&str], fold_axis: Option<&[i64]>) -> String {
fold_nodes(names, fold_axis)
}
#[test]
fn parse_axis_single() {
assert_eq!(parse_fold_axis("1").unwrap(), vec![0]);
assert_eq!(parse_fold_axis("2").unwrap(), vec![1]);
}
#[test]
fn parse_axis_rangeset() {
assert_eq!(parse_fold_axis("1-2").unwrap(), vec![0, 1]);
assert_eq!(parse_fold_axis("1,3").unwrap(), vec![0, 2]);
}
#[test]
fn parse_axis_zero_ignored() {
assert_eq!(parse_fold_axis("0").unwrap(), Vec::<i64>::new());
assert_eq!(parse_fold_axis("0,2").unwrap(), vec![1]);
}
#[test]
fn parse_axis_negative() {
assert_eq!(parse_fold_axis("-1").unwrap(), vec![-1]);
assert_eq!(parse_fold_axis("-2").unwrap(), vec![-2]);
}
#[test]
fn parse_axis_errors() {
assert!(parse_fold_axis("abc").is_err());
assert!(parse_fold_axis("-x").is_err());
assert!(parse_fold_axis("-").is_err());
}
#[test]
fn fold_1d_default() {
assert_eq!(
fold(&["node1", "node2", "node3", "node4"], None),
"node[1-4]"
);
}
#[test]
fn fold_1d_axis1_folds() {
assert_eq!(
fold(&["node1", "node2", "node3", "node4"], Some(&[0])),
"node[1-4]"
);
}
#[test]
fn fold_1d_axis_minus1_folds() {
assert_eq!(
fold(&["node1", "node2", "node3", "node4"], Some(&[-1])),
"node[1-4]"
);
}
#[test]
fn fold_1d_axis2_expands() {
assert_eq!(
fold(&["node1", "node2", "node3", "node4"], Some(&[1])),
"node1,node2,node3,node4"
);
}
#[test]
fn fold_1d_single_element_no_brackets() {
assert_eq!(fold(&["node3"], None), "node3");
}
#[test]
fn fold_1d_padding_preserved() {
assert_eq!(fold(&["prune003", "prune004"], None), "prune[003-004]");
}
#[test]
fn fold_plain_nodes() {
assert_eq!(fold(&["switch"], None), "switch");
assert_eq!(fold(&["node1", "switch"], None), "node1,switch");
}
#[test]
fn fold_nd_default_folds_all_axes() {
let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
assert_eq!(fold(&names, None), "foo[1-2]-[1-2]");
}
#[test]
fn fold_nd_axis1() {
let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
assert_eq!(fold(&names, Some(&[0])), "foo[1-2]-1,foo[1-2]-2");
}
#[test]
fn fold_nd_axis2() {
let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
assert_eq!(fold(&names, Some(&[1])), "foo1-[1-2],foo2-[1-2]");
}
#[test]
fn fold_nd_axis_minus1() {
let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
assert_eq!(fold(&names, Some(&[-1])), "foo1-[1-2],foo2-[1-2]");
}
#[test]
fn fold_nd_axis_minus2() {
let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
assert_eq!(fold(&names, Some(&[-2])), "foo[1-2]-1,foo[1-2]-2");
}
#[test]
fn fold_nd_out_of_range_axis_expands_everything() {
let names = ["foo1-1", "foo1-2", "foo2-1", "foo2-2"];
assert_eq!(fold(&names, Some(&[2])), "foo1-1,foo2-1,foo1-2,foo2-2");
}
#[test]
fn fold_nd_multivariate_keeps_larger_vector_first() {
let names = [
"da1p1", "da1p2", "da1p3", "da2p1", "da2p2", "da2p3", "da5p1", "da5p2", "da6p1",
"da6p2",
];
assert_eq!(fold(&names, None), "da[1-2]p[1-3],da[5-6]p[1-2]");
}
#[test]
fn fold_nd_univariate_merges_along_varying_dim() {
let names = ["r1n1", "r1n2", "r1n3"];
assert_eq!(fold(&names, None), "r1n[1-3]");
}
#[test]
fn fold_nd_partial_overlap_does_not_merge() {
let names = ["a1b1", "a2b1", "a2b2", "a3b2"];
assert_eq!(fold(&names, None), "a[1-2]b1,a[2-3]b2");
}
#[test]
fn fold_nd_contained_range_keeps_largest() {
let names = ["a1b1", "a1b2", "a2b1"];
assert_eq!(fold(&names, None), "a1b[1-2],a2b1");
}
#[test]
fn fold_nd_3d_full_cartesian() {
let names = [
"a1b1c1", "a1b1c2", "a1b2c1", "a1b2c2", "a2b1c1", "a2b1c2", "a2b2c1", "a2b2c2",
];
assert_eq!(fold(&names, None), "a[1-2]b[1-2]c[1-2]");
assert_eq!(
fold(&names, Some(&[2])),
"a1b1c[1-2],a2b1c[1-2],a1b2c[1-2],a2b2c[1-2]"
);
}
#[test]
fn fold_nd_padding_preserved() {
let names = ["n01-1", "n01-2", "n02-1", "n02-2"];
assert_eq!(fold(&names, None), "n[01-02]-[1-2]");
}
#[test]
fn fold_mixed_templates_sorted() {
let names = ["node1", "bmc10", "node2", "bmc11"];
assert_eq!(fold(&names, None), "bmc[10-11],node[1-2]");
}
}