#[must_use]
pub fn safe_slice(s: &str, start: usize, end: usize) -> &str {
let lo = s.floor_char_boundary(start.min(s.len()));
let hi = s.ceil_char_boundary(end.min(s.len()));
&s[lo..hi]
}
#[must_use]
pub fn safe_prefix(s: &str, end: usize) -> &str {
&s[..s.floor_char_boundary(end.min(s.len()))]
}
#[must_use]
pub fn safe_suffix(s: &str, start: usize) -> &str {
&s[s.ceil_char_boundary(start.min(s.len()))..]
}
#[must_use]
pub fn snap_range(s: &str, start: usize, end: usize) -> (usize, usize) {
(
s.floor_char_boundary(start.min(s.len())),
s.ceil_char_boundary(end.min(s.len())),
)
}
#[must_use]
pub fn min_suggestion_distance(token: &str, suggestions: &[String]) -> Option<usize> {
let lowered = token.to_lowercase();
suggestions
.iter()
.filter(|s| !s.chars().any(char::is_whitespace))
.map(|s| strsim::damerau_levenshtein(&lowered, &s.to_lowercase()))
.min()
}
#[cfg(test)]
mod tests {
use super::{min_suggestion_distance, safe_prefix, safe_slice, safe_suffix, snap_range};
#[test]
fn ascii_slice_is_exact() {
assert_eq!(safe_slice("hello world", 0, 5), "hello");
assert_eq!(safe_slice("hello world", 6, 11), "world");
}
#[test]
fn snaps_offsets_inside_multibyte_chars() {
let s = "Ölförderung";
let slice = safe_slice(s, 1, 4);
assert!(s.starts_with(slice) || s.contains(slice));
assert!(slice.is_char_boundary(0));
}
#[test]
fn clamps_out_of_range_offsets() {
assert_eq!(safe_slice("abc", 0, 999), "abc");
assert_eq!(safe_slice("abc", 999, 999), "");
}
#[test]
fn prefix_and_suffix_snap_away_from_a_split_char() {
let s = "Föö";
assert_eq!(safe_prefix(s, 2), "F"); assert_eq!(safe_suffix(s, 2), "ö"); assert_eq!(safe_prefix(s, 0), "");
assert_eq!(safe_suffix(s, 999), "");
}
#[test]
fn snap_range_reports_whether_it_moved() {
let s = "Föö";
assert_eq!(snap_range(s, 0, 3), (0, 3));
assert_eq!(snap_range(s, 2, 2), (1, 3));
assert_eq!(snap_range(s, 0, 999), (0, s.len()));
}
#[test]
fn prefix_and_suffix_partition_on_a_real_boundary() {
let s = "Föö";
assert_eq!(format!("{}{}", safe_prefix(s, 3), safe_suffix(s, 3)), s);
}
#[test]
fn word_split_suggestions_are_not_evidence_of_a_typo() {
let sugg = vec!["Abram sky".to_string()];
assert_eq!(min_suggestion_distance("Abramsky", &sugg), None);
}
#[test]
fn suggestion_distance_is_case_insensitive() {
let sugg = vec!["Hoar".to_string()];
assert_eq!(min_suggestion_distance("Hoare", &sugg), Some(1));
assert_eq!(min_suggestion_distance("recieve", &[]), None);
}
}