use r3bl_rs_utils_core::{ch, ChUnit, UnicodeString};
#[derive(Debug)]
pub enum CharacterMatchResult {
Reset,
ResetAndKeep,
Keep,
Finished,
Skip,
}
pub struct PatternMatcherStateMachine<'a> {
pattern: &'a str,
current_index: usize,
is_finished: bool,
maybe_scroll_offset_col_index: Option<ChUnit>,
}
impl<'a> PatternMatcherStateMachine<'a> {
pub fn new(pattern: &'a str, scroll_offset_col_index: Option<ChUnit>) -> Self {
Self {
pattern,
current_index: 0,
is_finished: false,
maybe_scroll_offset_col_index: scroll_offset_col_index,
}
}
pub fn get_current_index(&self) -> usize { self.current_index }
pub fn match_next(&mut self, character_to_test: char) -> CharacterMatchResult {
let character_to_test_width = UnicodeString::get_char_width(character_to_test);
if let Some(scroll_offset_col_index) = self.maybe_scroll_offset_col_index {
if scroll_offset_col_index != ch!(0) {
self.maybe_scroll_offset_col_index =
(scroll_offset_col_index - character_to_test_width).into();
return CharacterMatchResult::Skip;
}
}
if self.is_finished {
return CharacterMatchResult::Finished;
}
let Some(current_pattern_char) = self.pattern.chars().nth(self.current_index)
else {
self.is_finished = true;
return CharacterMatchResult::Finished;
};
match current_pattern_char == character_to_test {
true => {
self.current_index += 1;
CharacterMatchResult::Keep
}
false => {
if let Some(first_pattern_char) = self.pattern.chars().next() {
if character_to_test == first_pattern_char {
self.current_index = 1;
return CharacterMatchResult::ResetAndKeep;
}
}
self.current_index = 0;
CharacterMatchResult::Reset
}
}
}
}
#[cfg(test)]
mod tests {
use r3bl_rs_utils_core::{assert_eq2, ch, UnicodeString};
use super::*;
#[test]
fn test_with_emoji() {
let my_pattern = "ππ½foo";
let my_line = "πmonkeyππ½fooπbar";
let mut final_index = 0;
for (index, character) in my_line.chars().enumerate() {
println!(
"index[{}]: '{}' -> width: {}",
index,
character,
UnicodeString::get_char_width(character),
);
}
let scroll_offset_col_index = UnicodeString::get_char_width('π')
+ UnicodeString::get_char_width('m')
+ UnicodeString::get_char_width('o')
+ UnicodeString::get_char_width('n')
+ UnicodeString::get_char_width('k')
+ UnicodeString::get_char_width('e')
+ UnicodeString::get_char_width('y');
assert_eq2!(scroll_offset_col_index, ch!(8));
let mut pattern_matcher = PatternMatcherStateMachine::new(
my_pattern,
ch!(scroll_offset_col_index).into(),
);
let mut result = String::new();
for (index, character) in my_line.chars().enumerate() {
match pattern_matcher.match_next(character) {
CharacterMatchResult::Skip => {
continue;
}
CharacterMatchResult::Keep => {
result.push(character);
continue;
}
CharacterMatchResult::Reset => {
result.clear();
}
CharacterMatchResult::ResetAndKeep => {
result.clear();
result.push(character);
continue;
}
CharacterMatchResult::Finished => {
final_index = index;
break;
}
}
}
assert_eq2!(result, my_pattern);
assert_eq2!(final_index, 12);
}
#[test]
fn matches_occurrence_after_scroll_offset() {
let my_line = "abcabcdabcd";
let my_pattern = "abcd";
let mut pattern_matcher =
PatternMatcherStateMachine::new(my_pattern, ch!(4).into());
let mut result = String::new();
let mut final_index = 0;
for (index, character) in my_line.chars().enumerate() {
final_index = index;
match pattern_matcher.match_next(character) {
CharacterMatchResult::Skip => {
continue;
}
CharacterMatchResult::Keep => {
result.push(character);
continue;
}
CharacterMatchResult::Reset => {
result.clear();
}
CharacterMatchResult::ResetAndKeep => {
result.clear();
result.push(character);
continue;
}
CharacterMatchResult::Finished => {
break;
}
}
}
assert_eq2!(result, my_pattern);
assert_eq2!(final_index, 10);
}
#[test]
fn matches_first_occurrence() {
let my_line = "abcabcdabcd";
let my_pattern = "abcd";
let mut pattern_matcher = PatternMatcherStateMachine::new(my_pattern, None);
let mut result = String::new();
let mut final_index = 0;
for (index, character) in my_line.chars().enumerate() {
final_index = index;
match pattern_matcher.match_next(character) {
CharacterMatchResult::Skip => {
continue;
}
CharacterMatchResult::Keep => {
result.push(character);
continue;
}
CharacterMatchResult::Reset => {
result.clear();
}
CharacterMatchResult::ResetAndKeep => {
result.clear();
result.push(character);
continue;
}
CharacterMatchResult::Finished => {
break;
}
}
}
assert_eq2!(result, my_pattern);
assert_eq2!(final_index, 7);
}
#[test]
fn matches_start() {
let my_line = "abc_abcdabcd";
let my_pattern = "abc_";
let mut pattern_matcher = PatternMatcherStateMachine::new(my_pattern, None);
let mut result = String::new();
let mut final_index = 0;
for (index, character) in my_line.chars().enumerate() {
match pattern_matcher.match_next(character) {
CharacterMatchResult::Skip => {
continue;
}
CharacterMatchResult::Reset => {
result.clear();
}
CharacterMatchResult::ResetAndKeep => {
result.clear();
result.push(character);
continue;
}
CharacterMatchResult::Keep => {
result.push(character);
continue;
}
CharacterMatchResult::Finished => {
final_index = index;
break;
}
}
}
assert_eq2!(result, my_pattern);
assert_eq2!(final_index, 4);
}
#[test]
fn matches_end() {
let my_line = "abcabcdabcdx";
let my_pattern = "cdx";
let mut pattern_matcher = PatternMatcherStateMachine::new(my_pattern, None);
let mut result = String::new();
let mut final_index = 0;
for (index, character) in my_line.chars().enumerate() {
final_index = index;
match pattern_matcher.match_next(character) {
CharacterMatchResult::Skip => {
continue;
}
CharacterMatchResult::Reset => {
result.clear();
}
CharacterMatchResult::ResetAndKeep => {
result.clear();
result.push(character);
continue;
}
CharacterMatchResult::Keep => {
result.push(character);
continue;
}
CharacterMatchResult::Finished => {
break;
}
}
}
assert_eq2!(result, my_pattern);
assert_eq2!(final_index, 11);
}
}