///|
/// Closest string search functionality
///
/// Find the most similar string from a string array to a given string
/// Uses Levenshtein distance algorithm by default
///|
/// Closest string search options
pub struct ClosestStringOptions {
/// Whether to be case sensitive
/// Default value: false
case_sensitive : Bool
/// Custom comparison function
/// Parameters: (a, b) -> distance between two strings
/// Default value: levenshtein_distance
compare_fn : (String, String) -> Int
}
///|
/// Create default closest string search options
pub fn default_closest_string_options() -> ClosestStringOptions {
ClosestStringOptions::{
case_sensitive: false,
compare_fn: levenshtein_distance,
}
}
///|
/// Create case-sensitive closest string search options
pub fn case_sensitive_closest_string_options() -> ClosestStringOptions {
ClosestStringOptions::{
case_sensitive: true,
compare_fn: levenshtein_distance,
}
}
///|
pub suberror StringMatchingError String
///|
/// Find the most similar string from a string array
///
/// Uses Levenshtein distance algorithm by default to calculate distance between words
///
/// * `given_word` - The string for measuring distance
/// * `possible_words` - Array of candidate strings
/// * `options` - Comparison options
/// + Returns the closest string
/// + Throws if the possible_words array is empty
///
/// # Examples
/// ```mbt
/// let possible_words = ["length", "size", "blah", "help"]
/// let suggestion = closest_string("hep", possible_words, None)
/// // suggestion will be "help"
/// ```
pub fn closest_string(
given_word : String,
possible_words : Array[String],
options : ClosestStringOptions?,
) -> String raise StringMatchingError {
if possible_words.length() == 0 {
raise StringMatchingError(
"When using closest_string(), the possible_words array must contain at least one word",
)
}
let opts = match options {
Some(opts) => opts
None => default_closest_string_options()
}
let compare_fn = opts.compare_fn
let case_sensitive = opts.case_sensitive
let mut nearest_word = possible_words[0]
let mut closest_distance = 2147483647 // Int max value
for word in possible_words {
let distance = if case_sensitive {
compare_fn(given_word, word)
} else {
let given_lower = to_lowercase_string(given_word)
let word_lower = to_lowercase_string(word)
compare_fn(given_lower, word_lower)
}
if distance < closest_distance {
nearest_word = word
closest_distance = distance
}
}
nearest_word
}
///|
/// Simplified version using default options
pub fn closest_string_simple(
given_word : String,
possible_words : Array[String],
) -> String raise StringMatchingError {
closest_string(given_word, possible_words, None)
}
///|
/// Find multiple closest strings (return top n)
///
/// * `given_word` - The string for measuring distance
/// * `possible_words` - Array of candidate strings
/// * `count` - Number of closest strings to return
/// * `options` - Comparison options
/// + Returns an array of top count strings sorted by similarity
pub fn closest_strings(
given_word : String,
possible_words : Array[String],
count : Int,
options : ClosestStringOptions?,
) -> Array[String] {
if possible_words.length() == 0 {
return []
}
// Use similarity sorting functionality
let compare_opts = match options {
Some(opts) =>
Some(CompareSimilarityOptions::{
case_sensitive: opts.case_sensitive,
compare_fn: opts.compare_fn,
})
None => None
}
let sorted = sort_by_similarity(possible_words, given_word, compare_opts)
let result = Array::new()
let max_count = if count > sorted.length() { sorted.length() } else { count }
for i = 0; i < max_count; i = i + 1 {
result.push(sorted[i])
}
result
}
///|
/// Tests
test "closest_string_basic" {
let possible_words = ["length", "size", "blah", "help"]
let suggestion = closest_string_simple("hep", possible_words)
assert_eq(suggestion, "help")
}
///|
test "closest_string_empty_array" {
let possible_words : Array[String] = []
let err_msg = closest_string_simple("test", possible_words) catch {
StringMatchingError(msg) => msg
}
inspect(
err_msg,
content="When using closest_string(), the possible_words array must contain at least one word",
)
}
///|
test "closest_string_case_sensitive" {
let possible_words = ["Hello", "hello", "HELLO"]
let opts = case_sensitive_closest_string_options()
let suggestion = closest_string("Hello", possible_words, Some(opts))
assert_eq(suggestion, "Hello")
}
///|
test "closest_string_case_insensitive" {
let possible_words = ["Hello", "world", "test"]
let suggestion = closest_string_simple("hello", possible_words)
assert_eq(suggestion, "Hello")
}
///|
test "closest_strings_multiple" {
let possible_words = ["help", "hello", "world", "test", "hi"]
let suggestions = closest_strings("hep", possible_words, 3, None)
// The top three closest should include "help"
assert_eq(suggestions[0], "help")
assert_eq(suggestions.length(), 3)
}