const MAX_DEDUPE_ORDINAL: u32 = 9_999;
fn split_trailing_ordinal(name: &str) -> Option<(&str, u32, usize)> {
let dash = name.rfind('-')?;
let base = &name[..dash];
let digits = &name[dash + 1..];
if digits.is_empty() || digits.len() > 6 || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let n: u32 = digits.parse().ok()?;
Some((base, n, digits.len()))
}
pub fn dedupe_by_ordinal(candidate: &str, is_taken: impl Fn(&str) -> bool) -> String {
if !is_taken(candidate) {
return candidate.to_string();
}
let (base, mut n, width) = match split_trailing_ordinal(candidate) {
Some((base, n, width)) => (base.to_string(), n.saturating_add(1), width),
None => (candidate.to_string(), 2, 0),
};
loop {
let attempt = if width > 1 {
format!("{base}-{n:0width$}")
} else {
format!("{base}-{n}")
};
if !is_taken(&attempt) {
return attempt;
}
if n >= MAX_DEDUPE_ORDINAL {
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or_default();
return format!("{base}-{millis}");
}
n += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dedupe_returns_candidate_when_free() {
let name = dedupe_by_ordinal("tm-tagents", |_| false);
assert_eq!(name, "tm-tagents");
}
#[test]
fn dedupe_appends_2_on_first_collision() {
let taken = ["tm-tagents"];
let name = dedupe_by_ordinal("tm-tagents", |c| taken.contains(&c));
assert_eq!(name, "tm-tagents-2");
}
#[test]
fn dedupe_increments_past_multiple_collisions() {
let taken = ["tm-tagents", "tm-tagents-2"];
let name = dedupe_by_ordinal("tm-tagents", |c| taken.contains(&c));
assert_eq!(name, "tm-tagents-3");
}
#[test]
fn dedupe_skips_gaps_left_by_freed_names() {
let taken = ["tm-tagents", "tm-tagents-3"];
let name = dedupe_by_ordinal("tm-tagents", |c| taken.contains(&c));
assert_eq!(name, "tm-tagents-2");
}
#[test]
fn dedupe_increments_existing_ordinal_suffix() {
let taken = ["tm-tagents-2"];
let name = dedupe_by_ordinal("tm-tagents-2", |c| taken.contains(&c));
assert_eq!(name, "tm-tagents-3");
}
#[test]
fn dedupe_preserves_zero_padded_width() {
let taken = ["tm-trusty-tools-01"];
let name = dedupe_by_ordinal("tm-trusty-tools-01", |c| taken.contains(&c));
assert_eq!(name, "tm-trusty-tools-02");
}
#[test]
fn split_trailing_ordinal_rejects_non_numeric_suffix() {
assert_eq!(split_trailing_ordinal("tm-my-project"), None);
assert_eq!(split_trailing_ordinal("no-dash-at-all-x"), None);
}
#[test]
fn split_trailing_ordinal_parses_numeric_suffix() {
assert_eq!(
split_trailing_ordinal("tm-trusty-tools-01"),
Some(("tm-trusty-tools", 1, 2))
);
assert_eq!(
split_trailing_ordinal("tm-tagents-2"),
Some(("tm-tagents", 2, 1))
);
}
}