#[cfg(feature = "no_std")]
use alloc::{format, string::{String, ToString}, vec, vec::Vec};
pub const CORE_CALLABLE_BUILTINS: &[&str] = &[
"range",
"rand",
"print",
"try_call",
"panic",
];
pub const NUMERIC_METHODS: &[&str] = &[
"abs", "sqrt", "sin", "cos", "tan",
"floor", "ceil", "round", "exp", "log",
"pow", "min", "max",
"to_int", "to_float",
"type", "to_str", "inspect",
];
pub fn closest_match<I, S>(target: &str, candidates: I) -> Option<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let target_len = target.chars().count();
if target_len == 0 {
return None;
}
let max_dist = (target_len / 3).max(1).min(3);
let mut best: Option<(usize, String)> = None;
for cand in candidates {
let cand_str = cand.as_ref();
if cand_str.is_empty() || cand_str == target {
continue;
}
let cand_len = cand_str.chars().count();
let len_diff = if cand_len > target_len {
cand_len - target_len
} else {
target_len - cand_len
};
if len_diff > max_dist {
continue;
}
let d = levenshtein(target, cand_str);
if d > max_dist {
continue;
}
match &best {
Some((best_d, _)) if d >= *best_d => {}
_ => best = Some((d, cand_str.to_string())),
}
}
best.map(|(_, s)| s)
}
pub fn levenshtein(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let (m, n) = (a.len(), b.len());
if m == 0 {
return n;
}
if n == 0 {
return m;
}
let mut prev: Vec<usize> = (0..=n).collect();
let mut curr: Vec<usize> = vec![0usize; n + 1];
for i in 1..=m {
curr[0] = i;
for j in 1..=n {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
let mut x = prev[j] + 1;
let y = curr[j - 1] + 1;
if y < x {
x = y;
}
let z = prev[j - 1] + cost;
if z < x {
x = z;
}
curr[j] = x;
}
core::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
pub fn did_you_mean<I, S>(target: &str, candidates: I) -> Option<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
closest_match(target, candidates).map(|m| format!("Did you mean `{}`?", m))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levenshtein_matches_expected_distances() {
assert_eq!(levenshtein("", ""), 0);
assert_eq!(levenshtein("abc", "abc"), 0);
assert_eq!(levenshtein("kitten", "sitting"), 3);
assert_eq!(levenshtein("flaw", "lawn"), 2);
assert_eq!(levenshtein("", "abc"), 3);
assert_eq!(levenshtein("abc", ""), 3);
assert_eq!(levenshtein("print", "prnit"), 2);
}
#[test]
fn closest_finds_single_edit_typo() {
let cands = ["length", "height", "width"];
assert_eq!(
closest_match("lenght", cands),
Some("length".to_string())
);
}
#[test]
fn closest_returns_none_when_nothing_close() {
let cands = ["print", "range", "str"];
assert_eq!(closest_match("xylophone", cands), None);
}
#[test]
fn closest_skips_exact_matches() {
let cands = ["print", "range"];
assert_eq!(closest_match("print", cands), None);
}
#[test]
fn closest_skips_empty_candidates() {
let cands = ["", "range"];
assert_eq!(closest_match("rang", cands), Some("range".to_string()));
}
#[test]
fn closest_short_targets_have_tight_budget() {
let cands = ["xy", "range"];
assert_eq!(closest_match("x", cands), Some("xy".to_string()));
}
#[test]
fn closest_picks_shortest_distance_on_tie() {
let cands = ["hello", "ho"];
assert_eq!(closest_match("hi", cands), Some("ho".to_string()));
}
#[test]
fn closest_length_prune_skips_far_length_candidates() {
let cands = ["logarithm"];
assert_eq!(closest_match("ln", cands), None);
}
#[test]
fn did_you_mean_formats_or_returns_none() {
let cands = ["length"];
assert_eq!(
did_you_mean("lenght", cands),
Some("Did you mean `length`?".to_string())
);
assert_eq!(did_you_mean("xyz", cands), None);
}
}