use crate::catalog::{Catalog, Entry};
pub(super) fn normalize(name: &str) -> String {
name.chars()
.map(|c| {
if c == '-' {
'_'
} else {
c.to_ascii_lowercase()
}
})
.collect()
}
pub(super) enum ResolveError {
NotFound,
Ambiguous,
}
fn find<'a>(catalog: &'a Catalog, wanted: &str) -> Result<&'a Entry, ResolveError> {
let mut matched = catalog
.entries()
.iter()
.filter(|entry| normalize(entry.name()) == wanted);
match (matched.next(), matched.next()) {
(Some(entry), None) => Ok(entry),
(Some(_), Some(_)) => Err(ResolveError::Ambiguous),
(None, _) => Err(ResolveError::NotFound),
}
}
pub(super) fn resolve<'a>(
catalog: &'a Catalog,
requested: &str,
) -> Result<&'a Entry, ResolveError> {
find(catalog, &normalize(requested))
}
pub(super) fn nearest_first(catalog: &Catalog, wanted: &str) -> String {
if catalog.is_empty() {
return "No prompts are enabled.".to_owned();
}
let mut ranked: Vec<(usize, &str)> = catalog
.entries()
.iter()
.map(|entry| {
let name = entry.name();
(distance(&normalize(name), wanted), name)
})
.collect();
ranked.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)));
let names: Vec<&str> = ranked.into_iter().map(|(_, name)| name).collect();
format!("Enabled prompts, closest first: {}.", names.join(", "))
}
fn distance(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut row: Vec<usize> = (0..=b.len()).collect();
for (i, ca) in a.chars().enumerate() {
let mut diagonal = row[0];
row[0] = i + 1;
for (j, &cb) in b.iter().enumerate() {
let cost = usize::from(ca != cb);
let next_diagonal = row[j + 1];
row[j + 1] = (row[j + 1] + 1).min(row[j] + 1).min(diagonal + cost);
diagonal = next_diagonal;
}
}
row[b.len()]
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{distance, nearest_first, normalize};
use crate::catalog::{Catalog, Entry};
#[test]
fn equal_distance_names_come_back_in_lexical_order() {
let catalog = Catalog::new(vec![
Entry::broken("ccc".to_owned(), PathBuf::from("c.md"), "x"),
Entry::broken("bbb".to_owned(), PathBuf::from("b.md"), "x"),
]);
assert_eq!(
nearest_first(&catalog, "aaa"),
"Enabled prompts, closest first: bbb, ccc."
);
}
#[test]
fn normalization_folds_case_and_hyphens() {
assert_eq!(normalize("Research-Person"), "research_person");
assert_eq!(normalize("research_person"), "research_person");
}
#[test]
fn distance_counts_single_edits() {
assert_eq!(distance("", ""), 0);
assert_eq!(distance("echo", "echo"), 0);
assert_eq!(distance("echo", "ech"), 1);
assert_eq!(distance("echo", "echos"), 1);
assert_eq!(distance("echo", "ecmo"), 1);
assert_eq!(distance("echo", ""), 4);
}
}