use std::collections::HashMap;
use crate::storage::ACRONYM;
fn split_full_id(full: &str) -> (&str, Option<&str>) {
let without_prefix = full
.strip_prefix(&format!("{ACRONYM}-"))
.or_else(|| full.strip_prefix(&format!("{}-", ACRONYM.to_lowercase())))
.unwrap_or(full);
match without_prefix.split_once('-') {
Some((counter, suffix)) => (counter, Some(suffix)),
None => (without_prefix, None),
}
}
fn strip_leading_zeros(hex: &str) -> String {
let trimmed = hex.trim_start_matches('0');
if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.to_string()
}
}
pub fn short_id(full: &str) -> String {
let (counter, _) = split_full_id(full);
format!("#{}", strip_leading_zeros(counter))
}
pub fn format_ids(full_ids: &[&str]) -> Vec<String> {
let mut counter_frequency: HashMap<String, usize> = HashMap::new();
let parsed: Vec<(String, Option<String>)> = full_ids
.iter()
.map(|id| {
let (c, s) = split_full_id(id);
(c.to_uppercase(), s.map(|s| s.to_uppercase()))
})
.collect();
for (counter, _) in &parsed {
*counter_frequency.entry(counter.clone()).or_insert(0) += 1;
}
parsed
.into_iter()
.map(|(counter, suffix)| {
let short_counter = strip_leading_zeros(&counter);
let ambiguous = counter_frequency.get(&counter).copied().unwrap_or(1) > 1;
match (ambiguous, suffix) {
(true, Some(sfx)) => format!("#{short_counter}-{sfx}"),
_ => format!("#{short_counter}"),
}
})
.collect()
}
pub fn normalize_id_input(raw: &str) -> String {
let trimmed = raw.trim().trim_start_matches('#');
if trimmed.to_uppercase().starts_with(&format!("{ACRONYM}-")) {
return trimmed.to_uppercase();
}
let (counter, suffix) = match trimmed.split_once('-') {
Some((c, s)) => (c, Some(s)),
None => (trimmed, None),
};
let counter_valid =
!counter.is_empty() && counter.len() <= 4 && counter.chars().all(|c| c.is_ascii_hexdigit());
if !counter_valid {
return raw.to_string();
}
let padded = format!("{:0>4}", counter.to_uppercase());
match suffix {
Some(sfx) if sfx.len() == 2 && sfx.chars().all(|c| c.is_ascii_hexdigit()) => {
format!("{ACRONYM}-{padded}-{}", sfx.to_uppercase())
}
_ => format!("{ACRONYM}-{padded}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_id_strips_prefix_suffix_and_leading_zeros() {
assert_eq!(short_id("TODO-00A1-EA"), "#A1");
assert_eq!(short_id("TODO-0001-7F"), "#1");
assert_eq!(short_id("TODO-0110-B3"), "#110");
assert_eq!(short_id("TODO-FFFF-00"), "#FFFF");
}
#[test]
fn short_id_without_suffix_still_works() {
assert_eq!(short_id("TODO-0042"), "#42");
}
#[test]
fn format_ids_unique_counters_stay_short() {
let ids = vec!["TODO-0001-7F", "TODO-00A1-EA", "TODO-0110-B3"];
let out = format_ids(&ids);
assert_eq!(out, vec!["#1", "#A1", "#110"]);
}
#[test]
fn format_ids_expands_only_colliding_rows() {
let ids = vec![
"TODO-0001-7F",
"TODO-00A1-EA",
"TODO-00A1-7F",
"TODO-0110-B3",
];
let out = format_ids(&ids);
assert_eq!(out, vec!["#1", "#A1-EA", "#A1-7F", "#110"]);
}
#[test]
fn normalize_accepts_short_form() {
assert_eq!(normalize_id_input("#A1"), "TODO-00A1");
assert_eq!(normalize_id_input("A1"), "TODO-00A1");
assert_eq!(normalize_id_input("a1"), "TODO-00A1");
assert_eq!(normalize_id_input("1"), "TODO-0001");
assert_eq!(normalize_id_input("110"), "TODO-0110");
assert_eq!(normalize_id_input("FFFF"), "TODO-FFFF");
}
#[test]
fn normalize_accepts_short_form_with_suffix() {
assert_eq!(normalize_id_input("#A1-EA"), "TODO-00A1-EA");
assert_eq!(normalize_id_input("a1-ea"), "TODO-00A1-EA");
}
#[test]
fn normalize_passes_through_full_form() {
assert_eq!(normalize_id_input("TODO-00A1"), "TODO-00A1");
assert_eq!(normalize_id_input("todo-00a1-ea"), "TODO-00A1-EA");
}
#[test]
fn normalize_passes_through_nonsense() {
assert_eq!(normalize_id_input("GGGGG"), "GGGGG");
assert_eq!(normalize_id_input("12345"), "12345");
}
}