pub struct Needle {
chars: Vec<char>,
fold: bool,
}
impl Needle {
pub fn new(term: &str) -> Option<Self> {
if term.is_empty() {
return None;
}
let fold = !term.chars().any(char::is_uppercase);
let chars = term.chars().map(|c| if fold { fold_char(c) } else { c }).collect();
Some(Needle { chars, fold })
}
pub fn len(&self) -> usize {
self.chars.len()
}
pub fn find_all(&self, line: &str) -> Vec<(usize, usize)> {
let haystack: Vec<char> = line.chars().map(|c| if self.fold { fold_char(c) } else { c }).collect();
let length = self.chars.len();
let mut found = Vec::new();
let mut index = 0;
while index + length <= haystack.len() {
if haystack[index..index + length] == self.chars[..] {
found.push((index, index + length));
index += length;
} else {
index += 1;
}
}
found
}
}
pub fn fold_char(c: char) -> char {
let mut lower = c.to_lowercase();
match (lower.next(), lower.next()) {
(Some(single), None) => single,
_ => c,
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Found {
pub line: usize,
pub start: usize,
pub end: usize,
pub wrapped: bool,
}
pub fn search(needle: &Needle, count: usize, line_at: impl Fn(usize) -> String, from: (usize, usize), forward: bool) -> Option<Found> {
if count == 0 {
return None;
}
let (from_line, from_col) = (from.0.min(count - 1), from.1);
for step in 0..=count {
let (line, wrapped) = if forward {
((from_line + step) % count, from_line + step >= count)
} else {
((from_line + count - step) % count, step > from_line)
};
let matches = needle.find_all(&line_at(line));
let wanted = |&&(start, _): &&(usize, usize)| match (step, forward) {
(0, true) => start >= from_col,
(0, false) => start < from_col,
(last, true) if last == count => start < from_col,
(last, false) if last == count => start >= from_col,
_ => true,
};
let hit = if forward { matches.iter().find(wanted) } else { matches.iter().rev().find(wanted) };
if let Some(&(start, end)) = hit {
return Some(Found { line, start, end, wrapped });
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(text: &'static str) -> (usize, impl Fn(usize) -> String) {
let lines: Vec<&str> = text.lines().collect();
(lines.len(), move |index| lines[index].to_string())
}
#[test]
fn lower_case_finds_either_case_and_a_capital_finds_only_itself() {
let needle = Needle::new("readme").unwrap();
assert_eq!(needle.find_all("README and readme"), [(0, 6), (11, 17)]);
let needle = Needle::new("README").unwrap();
assert_eq!(needle.find_all("README and readme"), [(0, 6)]);
assert!(Needle::new("").is_none());
}
#[test]
fn columns_are_characters_not_bytes() {
let needle = Needle::new("fé").unwrap();
assert_eq!(needle.find_all("café fé"), [(2, 4), (5, 7)]);
let needle = Needle::new("x").unwrap();
assert_eq!(needle.find_all("İx"), [(1, 2)]);
}
#[test]
fn forward_goes_on_from_the_end_of_a_match_and_wraps() {
let (count, line_at) = lines("one two\ntwo\nthree two");
let needle = Needle::new("two").unwrap();
let first = search(&needle, count, &line_at, (0, 0), true).unwrap();
assert_eq!(first, Found { line: 0, start: 4, end: 7, wrapped: false });
let second = search(&needle, count, &line_at, (first.line, first.end), true).unwrap();
assert_eq!(second, Found { line: 1, start: 0, end: 3, wrapped: false });
let third = search(&needle, count, &line_at, (second.line, second.end), true).unwrap();
assert_eq!(third, Found { line: 2, start: 6, end: 9, wrapped: false });
let round = search(&needle, count, &line_at, (third.line, third.end), true).unwrap();
assert_eq!(round, Found { line: 0, start: 4, end: 7, wrapped: true });
}
#[test]
fn backward_goes_on_from_the_start_of_a_match_and_wraps() {
let (count, line_at) = lines("one two\ntwo\nthree two");
let needle = Needle::new("two").unwrap();
let before = search(&needle, count, &line_at, (1, 0), false).unwrap();
assert_eq!(before, Found { line: 0, start: 4, end: 7, wrapped: false });
let round = search(&needle, count, &line_at, (before.line, before.start), false).unwrap();
assert_eq!(round, Found { line: 2, start: 6, end: 9, wrapped: true });
}
#[test]
fn a_lone_match_is_found_again_and_none_is_none() {
let (count, line_at) = lines("a\nneedle here\nb");
let needle = Needle::new("needle").unwrap();
let only = search(&needle, count, &line_at, (1, 6), true).unwrap();
assert_eq!(only, Found { line: 1, start: 0, end: 6, wrapped: true });
let only = search(&needle, count, &line_at, (1, 0), false).unwrap();
assert_eq!(only, Found { line: 1, start: 0, end: 6, wrapped: true });
let missing = Needle::new("absent").unwrap();
assert_eq!(search(&missing, count, &line_at, (0, 0), true), None);
assert_eq!(search(&missing, 0, |_| String::new(), (0, 0), true), None);
}
#[test]
fn matches_do_not_overlap() {
let needle = Needle::new("aa").unwrap();
assert_eq!(needle.find_all("aaaaa"), [(0, 2), (2, 4)]);
assert_eq!(needle.len(), 2);
}
#[test]
fn a_letter_folds_to_one_lower_case_letter_or_stays() {
assert_eq!(fold_char('Ä'), 'ä');
assert_eq!(fold_char('Ж'), 'ж');
assert_eq!(fold_char('7'), '7');
assert_eq!(fold_char('İ'), 'İ');
}
#[test]
fn a_start_past_the_last_line_searches_from_the_last() {
let (count, line_at) = lines(
"two
one two",
);
let needle = Needle::new("two").unwrap();
let found = search(&needle, count, &line_at, (9, 0), true).unwrap();
assert_eq!(found, Found { line: 1, start: 4, end: 7, wrapped: false });
}
#[test]
fn backward_from_the_middle_of_a_line_finds_what_is_before_it_there() {
let (count, line_at) = lines(
"x
two and two",
);
let needle = Needle::new("two").unwrap();
let found = search(&needle, count, &line_at, (1, 8), false).unwrap();
assert_eq!(found, Found { line: 1, start: 0, end: 3, wrapped: false });
}
}