pub const UNGROUPED: &str = "Ungrouped";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SectionGroup {
pub label: String,
pub items: Vec<usize>,
}
pub fn group(
order: &[String],
items: &[usize],
section_of: impl Fn(usize) -> Option<String>,
) -> Vec<SectionGroup> {
let mut named: Vec<SectionGroup> = order
.iter()
.map(|name| SectionGroup {
label: name.clone(),
items: Vec::new(),
})
.collect();
let mut ungrouped: Vec<usize> = Vec::new();
for &item in items {
match section_position(order, section_of(item).as_deref()) {
Some(pos) => named[pos].items.push(item),
None => ungrouped.push(item),
}
}
let mut groups: Vec<SectionGroup> =
named.into_iter().filter(|g| !g.items.is_empty()).collect();
if !ungrouped.is_empty() {
groups.push(SectionGroup {
label: UNGROUPED.to_string(),
items: ungrouped,
});
}
groups
}
fn section_position(order: &[String], section: Option<&str>) -> Option<usize> {
let name = section?;
order.iter().position(|o| o.eq_ignore_ascii_case(name))
}
pub fn flatten(groups: &[SectionGroup]) -> Vec<usize> {
groups
.iter()
.flat_map(|g| g.items.iter().copied())
.collect()
}
pub fn section_starts(groups: &[SectionGroup]) -> Vec<usize> {
let mut starts = Vec::with_capacity(groups.len());
let mut pos = 0;
for group in groups {
starts.push(pos);
pos += group.items.len();
}
starts
}
pub fn current_section(starts: &[usize], cursor: usize) -> Option<usize> {
starts.iter().rposition(|&start| start <= cursor)
}
#[cfg(test)]
mod tests {
use super::*;
fn order() -> Vec<String> {
vec!["Work".to_string(), "Personal".to_string()]
}
#[test]
fn groups_by_order_with_ungrouped_last() {
let section_of = |i: usize| match i {
0 | 1 => Some("Work".to_string()),
2 => Some("personal".to_string()), _ => None,
};
let groups = group(&order(), &[0, 1, 2, 3], section_of);
assert_eq!(groups.len(), 3);
assert_eq!(groups[0].label, "Work");
assert_eq!(groups[0].items, vec![0, 1]);
assert_eq!(groups[1].label, "Personal");
assert_eq!(groups[1].items, vec![2]);
assert_eq!(groups[2].label, UNGROUPED);
assert_eq!(groups[2].items, vec![3]);
}
#[test]
fn empty_named_sections_are_dropped() {
let groups = group(&order(), &[0], |_| Some("Personal".to_string()));
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].label, "Personal");
}
#[test]
fn unknown_section_falls_into_ungrouped() {
let groups = group(&order(), &[0], |_| Some("Misc".to_string()));
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].label, UNGROUPED);
}
#[test]
fn starts_and_jump_move_between_sections() {
let groups = vec![
SectionGroup {
label: "a".to_string(),
items: vec![0, 1],
},
SectionGroup {
label: "b".to_string(),
items: vec![2],
},
SectionGroup {
label: "c".to_string(),
items: vec![3, 4, 5],
},
];
let starts = section_starts(&groups);
assert_eq!(starts, vec![0, 2, 3]);
assert_eq!(current_section(&starts, 1), Some(0));
assert_eq!(current_section(&starts, 4), Some(2));
}
}