pub fn find_common_prefix(names: &[&str]) -> Option<String> {
if names.is_empty() || names.iter().any(|n| n.is_empty()) {
return None;
}
let first = names[0];
let mut prefix_len = 0;
for (i, ch) in first.chars().enumerate() {
if names.iter().all(|n| n.chars().nth(i) == Some(ch)) {
prefix_len = i + 1;
} else {
break;
}
}
if prefix_len == 0 {
return None;
}
let raw_prefix = &first[..prefix_len];
if raw_prefix.ends_with('_') {
return Some(raw_prefix.to_string());
}
if names.contains(&raw_prefix) {
return Some(format!("{}_", raw_prefix));
}
if let Some(last_underscore) = raw_prefix.rfind('_') {
let clean_prefix = &first[..=last_underscore];
if names.iter().all(|n| n.starts_with(clean_prefix)) {
return Some(clean_prefix.to_string());
}
}
None
}
pub fn find_common_suffix(names: &[&str]) -> Option<String> {
if names.is_empty() || names.iter().any(|n| n.is_empty()) {
return None;
}
let first = names[0];
let first_chars: Vec<char> = first.chars().collect();
let mut suffix_len = 0;
for i in 0..first_chars.len() {
let idx_from_end = first_chars.len() - 1 - i;
let ch = first_chars[idx_from_end];
let all_match = names.iter().all(|n| {
let n_chars: Vec<char> = n.chars().collect();
if i >= n_chars.len() {
return false;
}
n_chars[n_chars.len() - 1 - i] == ch
});
if all_match {
suffix_len = i + 1;
} else {
break;
}
}
if suffix_len == 0 {
return None;
}
let raw_suffix = &first[first.len() - suffix_len..];
if raw_suffix.starts_with('_') {
return Some(raw_suffix.to_string());
}
let at_word_boundary = names.iter().all(|n| {
if *n == raw_suffix {
true } else if let Some(prefix) = n.strip_suffix(raw_suffix) {
prefix.ends_with('_')
} else {
false
}
});
if at_word_boundary {
return Some(format!("_{}", raw_suffix));
}
if let Some(first_underscore) = raw_suffix.find('_') {
let clean_suffix = &raw_suffix[first_underscore..];
if names.iter().all(|n| n.ends_with(clean_suffix)) {
return Some(clean_suffix.to_string());
}
}
None
}
pub fn normalize_prefix(s: &str) -> String {
if s.is_empty() {
String::new()
} else if s.ends_with('_') {
s.to_string()
} else {
format!("{}_", s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_common_prefix_basic() {
let names = vec!["addrs_0sats", "addrs_1sats", "addrs_2sats"];
assert_eq!(find_common_prefix(&names), Some("addrs_".to_string()));
}
#[test]
fn test_common_prefix_none() {
let names = vec!["foo", "bar", "baz"];
assert_eq!(find_common_prefix(&names), None);
}
#[test]
fn test_common_prefix_lth() {
let names = vec!["lth_cost_basis_max", "lth_cost_basis_min", "lth_cost_basis"];
assert_eq!(
find_common_prefix(&names),
Some("lth_cost_basis_".to_string())
);
}
#[test]
fn test_common_suffix_basic() {
let names = vec!["cumulative_supply", "net_supply", "total_supply"];
assert_eq!(find_common_suffix(&names), Some("_supply".to_string()));
}
#[test]
fn test_common_prefix_cost_basis() {
let names = vec!["cost_basis_max", "cost_basis_min", "cost_basis"];
assert_eq!(find_common_prefix(&names), Some("cost_basis_".to_string()));
}
#[test]
fn test_common_suffix_none() {
let names = vec!["foo", "bar", "baz"];
assert_eq!(find_common_suffix(&names), None);
}
#[test]
fn test_common_prefix_one_is_prefix_of_other() {
let names = vec!["block_count_cumulative", "block_count"];
assert_eq!(find_common_prefix(&names), Some("block_count_".to_string()));
}
#[test]
fn test_common_suffix_realized_loss() {
let names = vec![
"cumulative_realized_loss",
"net_realized_loss",
"realized_loss",
];
assert_eq!(
find_common_suffix(&names),
Some("_realized_loss".to_string())
);
}
}