use crate::phonetic::grep::{GrepError, WordBoundaryIterator};
use crate::phonetic::nfa::{compile, NFAChar, ProductAutomatonChar, ThompsonBuilderChar};
use crate::phonetic::types::RewriteRuleChar;
use crate::phonetic::{apply_rules_seq_char, PhoneChar};
fn parse_error(msg: &str) -> GrepError {
GrepError::Compile(format!("parse error: {}", msg))
}
#[derive(Debug, Clone)]
pub struct TokenQuery {
pub tokens: Vec<TokenSpec>,
pub separators: Vec<Separator>,
pub default_distance: u8,
}
#[derive(Debug, Clone)]
pub struct TokenSpec {
pub pattern: TokenPattern,
pub max_distance: u8,
}
#[derive(Debug, Clone)]
pub enum TokenPattern {
Literal(String),
Alternation(Vec<TokenPattern>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Separator {
Whitespace,
Wildcard,
}
pub struct CompiledTokenQuery {
token_automata: Vec<ProductAutomatonChar>,
separators: Vec<Separator>,
rules: Option<Vec<RewriteRuleChar>>,
}
#[derive(Debug, Clone)]
pub struct TokenMatch {
pub byte_range: (usize, usize),
pub token_matches: Vec<TokenMatchDetail>,
pub total_distance: u8,
pub matched_text: String,
}
#[derive(Debug, Clone)]
pub struct TokenMatchDetail {
pub token_index: usize,
pub byte_range: (usize, usize),
pub original_text: String,
pub normalized_text: String,
pub distance: u8,
}
pub fn parse_query(input: &str, default_distance: u8) -> Result<TokenQuery, GrepError> {
let mut parser = QueryParser::new(input, default_distance);
parser.parse()
}
struct QueryParser<'a> {
input: &'a str,
chars: std::iter::Peekable<std::str::CharIndices<'a>>,
default_distance: u8,
}
impl<'a> QueryParser<'a> {
fn new(input: &'a str, default_distance: u8) -> Self {
Self {
input,
chars: input.char_indices().peekable(),
default_distance,
}
}
fn parse(&mut self) -> Result<TokenQuery, GrepError> {
let mut tokens = Vec::new();
let mut separators = Vec::new();
self.skip_whitespace();
if self.peek().is_some() {
tokens.push(self.parse_token_spec()?);
}
while self.peek().is_some() {
let sep = self.parse_separator()?;
if self.peek().is_none() {
break;
}
separators.push(sep);
tokens.push(self.parse_token_spec()?);
}
if tokens.is_empty() {
return Err(parse_error("empty query"));
}
Ok(TokenQuery {
tokens,
separators,
default_distance: self.default_distance,
})
}
fn parse_token_spec(&mut self) -> Result<TokenSpec, GrepError> {
let pattern = self.parse_pattern()?;
let max_distance = if self.peek_char() == Some(':') {
self.advance(); self.parse_distance()?
} else {
self.default_distance
};
Ok(TokenSpec {
pattern,
max_distance,
})
}
fn parse_pattern(&mut self) -> Result<TokenPattern, GrepError> {
match self.peek_char() {
Some('(') => self.parse_alternation(),
Some('"') => self.parse_quoted(),
_ => self.parse_literal(),
}
}
fn parse_alternation(&mut self) -> Result<TokenPattern, GrepError> {
self.expect('(')?;
let mut alternatives = Vec::new();
loop {
let alt = self.parse_alternative_pattern()?;
alternatives.push(alt);
match self.peek_char() {
Some('|') => {
self.advance(); }
Some(')') => {
self.advance(); break;
}
Some(c) => {
return Err(parse_error(&format!(
"expected '|' or ')' in alternation, found '{}'",
c
)));
}
None => {
return Err(parse_error("unclosed alternation"));
}
}
}
if alternatives.is_empty() {
return Err(parse_error("empty alternation"));
}
Ok(TokenPattern::Alternation(alternatives))
}
fn parse_alternative_pattern(&mut self) -> Result<TokenPattern, GrepError> {
let mut chars = String::new();
while let Some(c) = self.peek_char() {
match c {
'|' | ')' => break,
'\\' => {
self.advance(); if let Some(escaped) = self.peek_char() {
chars.push(self.unescape(escaped)?);
self.advance();
} else {
return Err(parse_error("trailing backslash"));
}
}
_ => {
chars.push(c);
self.advance();
}
}
}
if chars.is_empty() {
return Err(parse_error("empty pattern in alternation"));
}
Ok(TokenPattern::Literal(chars))
}
fn parse_quoted(&mut self) -> Result<TokenPattern, GrepError> {
self.expect('"')?;
let mut chars = String::new();
loop {
match self.peek_char() {
Some('"') => {
self.advance(); break;
}
Some('\\') => {
self.advance(); if let Some(escaped) = self.peek_char() {
chars.push(self.unescape(escaped)?);
self.advance();
} else {
return Err(parse_error("trailing backslash in quoted string"));
}
}
Some(c) => {
chars.push(c);
self.advance();
}
None => {
return Err(parse_error("unclosed quoted string"));
}
}
}
Ok(TokenPattern::Literal(chars))
}
fn parse_literal(&mut self) -> Result<TokenPattern, GrepError> {
let mut chars = String::new();
while let Some(c) = self.peek_char() {
match c {
' ' | '\t' | '\n' | '\r' => break,
':' => {
break;
}
'(' | ')' | '|' | '"' => break,
'.' => {
if self.is_wildcard_ahead() {
break;
}
chars.push(c);
self.advance();
}
'\\' => {
self.advance(); if let Some(escaped) = self.peek_char() {
chars.push(self.unescape(escaped)?);
self.advance();
} else {
return Err(parse_error("trailing backslash"));
}
}
_ => {
chars.push(c);
self.advance();
}
}
}
if chars.is_empty() {
return Err(parse_error("empty token pattern"));
}
Ok(TokenPattern::Literal(chars))
}
fn parse_separator(&mut self) -> Result<Separator, GrepError> {
let start_whitespace = self.skip_whitespace();
if self.peek_char() == Some('.') && self.is_wildcard_ahead() {
self.advance(); self.advance(); self.skip_whitespace();
return Ok(Separator::Wildcard);
}
if start_whitespace {
return Ok(Separator::Whitespace);
}
Err(parse_error("expected separator between tokens"))
}
fn parse_distance(&mut self) -> Result<u8, GrepError> {
let mut digits = String::new();
while let Some(c) = self.peek_char() {
if c.is_ascii_digit() {
digits.push(c);
self.advance();
} else {
break;
}
}
if digits.is_empty() {
return Err(parse_error("expected distance after ':'"));
}
digits
.parse::<u8>()
.map_err(|_| parse_error(&format!("invalid distance: {} (must be 0-255)", digits)))
}
fn unescape(&self, c: char) -> Result<char, GrepError> {
match c {
':' | '\\' | '.' | '(' | ')' | '|' | '"' => Ok(c),
_ => Err(parse_error(&format!("invalid escape sequence: \\{}", c))),
}
}
fn is_wildcard_ahead(&mut self) -> bool {
let chars_copy: Vec<_> = self.input[self.current_pos()..].chars().take(2).collect();
chars_copy.len() >= 2 && chars_copy[0] == '.' && chars_copy[1] == '*'
}
fn current_pos(&mut self) -> usize {
self.chars
.peek()
.map(|(pos, _)| *pos)
.unwrap_or(self.input.len())
}
fn peek(&mut self) -> Option<(usize, char)> {
self.chars.peek().copied()
}
fn peek_char(&mut self) -> Option<char> {
self.chars.peek().map(|(_, c)| *c)
}
fn advance(&mut self) -> Option<(usize, char)> {
self.chars.next()
}
fn expect(&mut self, expected: char) -> Result<(), GrepError> {
match self.advance() {
Some((_, c)) if c == expected => Ok(()),
Some((_, c)) => Err(parse_error(&format!(
"expected '{}', found '{}'",
expected, c
))),
None => Err(parse_error(&format!(
"expected '{}', found end of input",
expected
))),
}
}
fn skip_whitespace(&mut self) -> bool {
let mut skipped = false;
while let Some(c) = self.peek_char() {
if c.is_whitespace() {
self.advance();
skipped = true;
} else {
break;
}
}
skipped
}
}
impl CompiledTokenQuery {
pub fn compile(query: &TokenQuery) -> Result<Self, GrepError> {
Self::compile_with_rules(query, None)
}
pub fn compile_with_rules(
query: &TokenQuery,
rules: Option<Vec<RewriteRuleChar>>,
) -> Result<Self, GrepError> {
let mut token_automata = Vec::with_capacity(query.tokens.len());
for token in &query.tokens {
let nfa = Self::pattern_to_nfa(&token.pattern)?;
let product = ProductAutomatonChar::new(nfa, token.max_distance);
token_automata.push(product);
}
Ok(Self {
token_automata,
separators: query.separators.clone(),
rules,
})
}
fn pattern_to_nfa(pattern: &TokenPattern) -> Result<NFAChar, GrepError> {
match pattern {
TokenPattern::Literal(s) => {
let escaped = Self::escape_for_regex(s);
let regex = crate::phonetic::regex::parse(&escaped)?;
compile(®ex).map_err(|e| GrepError::Compile(e.to_string()))
}
TokenPattern::Alternation(alts) => {
if alts.is_empty() {
return Err(GrepError::Compile("empty alternation".to_string()));
}
let builder = ThompsonBuilderChar::new();
let mut nfas = Vec::new();
for alt in alts {
nfas.push(Self::pattern_to_nfa(alt)?);
}
let mut result = nfas.remove(0);
for nfa in nfas {
result = builder.alternation(result, nfa);
}
Ok(result)
}
}
}
fn escape_for_regex(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 2);
for c in s.chars() {
match c {
'.' | '*' | '+' | '?' | '[' | ']' | '(' | ')' | '{' | '}' | '|' | '^' | '$'
| '\\' => {
result.push('\\');
result.push(c);
}
_ => result.push(c),
}
}
result
}
}
pub struct TokenGrep {
compiled: CompiledTokenQuery,
}
impl TokenGrep {
pub fn new(query: &str, default_distance: u8) -> Result<Self, GrepError> {
let parsed = parse_query(query, default_distance)?;
let compiled = CompiledTokenQuery::compile(&parsed)?;
Ok(Self { compiled })
}
pub fn with_rules(
query: &str,
rules: Vec<RewriteRuleChar>,
default_distance: u8,
) -> Result<Self, GrepError> {
let parsed = parse_query(query, default_distance)?;
let compiled = CompiledTokenQuery::compile_with_rules(&parsed, Some(rules))?;
Ok(Self { compiled })
}
pub fn scan(&self, document: &str) -> Vec<TokenMatch> {
let mut matches = Vec::new();
let words: Vec<(usize, &str, usize)> = WordBoundaryIterator::new(document).collect();
if words.is_empty() || self.compiled.token_automata.is_empty() {
return matches;
}
for start_idx in 0..words.len() {
if let Some(m) = self.try_match_from(&words, start_idx, document) {
let overlaps = matches.last().map_or(false, |last: &TokenMatch| {
m.byte_range.0 < last.byte_range.1
});
if !overlaps {
matches.push(m);
}
}
}
matches
}
fn try_match_from(
&self,
words: &[(usize, &str, usize)],
start_idx: usize,
document: &str,
) -> Option<TokenMatch> {
let mut token_matches = Vec::new();
let mut word_idx = start_idx;
let mut total_distance: u8 = 0;
for (token_idx, product) in self.compiled.token_automata.iter().enumerate() {
if word_idx >= words.len() {
return None;
}
if token_idx > 0 {
let sep = &self.compiled.separators[token_idx - 1];
match sep {
Separator::Whitespace => {
}
Separator::Wildcard => {
let mut found = false;
for try_idx in word_idx..words.len() {
if let Some(detail) =
self.try_match_token(product, words, try_idx, token_idx)
{
word_idx = try_idx;
found = true;
total_distance = total_distance.saturating_add(detail.distance);
token_matches.push(detail);
break;
}
}
if !found {
return None;
}
word_idx += 1;
continue; }
}
}
if let Some(detail) = self.try_match_token(product, words, word_idx, token_idx) {
total_distance = total_distance.saturating_add(detail.distance);
token_matches.push(detail);
word_idx += 1;
} else {
return None;
}
}
if token_matches.is_empty() {
return None;
}
let first = &token_matches[0];
let last = &token_matches[token_matches.len() - 1];
let byte_range = (first.byte_range.0, last.byte_range.1);
let matched_text = document[byte_range.0..byte_range.1].to_string();
Some(TokenMatch {
byte_range,
token_matches,
total_distance,
matched_text,
})
}
fn try_match_token(
&self,
product: &ProductAutomatonChar,
words: &[(usize, &str, usize)],
word_idx: usize,
token_idx: usize,
) -> Option<TokenMatchDetail> {
let (start, word, end) = words[word_idx];
let normalized = self.normalize(word);
product
.min_distance(&normalized)
.map(|distance| TokenMatchDetail {
token_index: token_idx,
byte_range: (start, end),
original_text: word.to_string(),
normalized_text: normalized,
distance,
})
}
fn normalize(&self, text: &str) -> String {
match &self.compiled.rules {
Some(rules) if !rules.is_empty() => {
let vowels = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
let input_phones: Vec<PhoneChar> = text
.chars()
.map(|c| {
if vowels.contains(&c) {
PhoneChar::Vowel(c)
} else {
PhoneChar::Consonant(c)
}
})
.collect();
match apply_rules_seq_char(rules, &input_phones, 100) {
Some(phones) => {
let mut s = String::new();
for p in phones.iter() {
match p {
PhoneChar::Vowel(c) | PhoneChar::Consonant(c) => s.push(*c),
PhoneChar::Digraph(c1, c2) => {
s.push(*c1);
s.push(*c2);
}
PhoneChar::Trigraph(c1, c2, c3) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
}
PhoneChar::Tetragraph(c1, c2, c3, c4) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
}
PhoneChar::Pentagraph(c1, c2, c3, c4, c5) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
s.push(*c5);
}
PhoneChar::Hexagraph(c1, c2, c3, c4, c5, c6) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
s.push(*c5);
s.push(*c6);
}
PhoneChar::Heptagraph(c1, c2, c3, c4, c5, c6, c7) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
s.push(*c5);
s.push(*c6);
s.push(*c7);
}
PhoneChar::Sequence(chars) => {
for c in chars {
s.push(*c);
}
}
PhoneChar::Silent => {}
}
}
s
}
None => text.to_string(),
}
}
_ => text.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct DocumentMatch<I> {
pub doc_id: I,
pub matches: Vec<TokenMatch>,
}
#[cfg(feature = "parallel-grep")]
impl TokenGrep {
pub fn scan_documents_parallel<I, S>(
&self,
documents: impl IntoIterator<Item = (I, S)>,
) -> Vec<DocumentMatch<I>>
where
I: Send + Clone,
S: AsRef<str> + Send,
{
use rayon::prelude::*;
let docs: Vec<_> = documents.into_iter().collect();
docs.into_par_iter()
.filter_map(|(id, doc)| {
let matches = self.scan(doc.as_ref());
if matches.is_empty() {
None
} else {
Some(DocumentMatch {
doc_id: id,
matches,
})
}
})
.collect()
}
pub fn filter_documents_parallel<I, S>(
&self,
documents: impl IntoIterator<Item = (I, S)>,
) -> Vec<I>
where
I: Send + Clone,
S: AsRef<str> + Send,
{
use rayon::prelude::*;
let docs: Vec<_> = documents.into_iter().collect();
docs.into_par_iter()
.filter_map(|(id, doc)| {
let matches = self.scan(doc.as_ref());
if matches.is_empty() {
None
} else {
Some(id)
}
})
.collect()
}
pub fn count_documents_parallel<I, S>(
&self,
documents: impl IntoIterator<Item = (I, S)>,
) -> usize
where
I: Send,
S: AsRef<str> + Send,
{
use rayon::prelude::*;
let docs: Vec<_> = documents.into_iter().collect();
docs.into_par_iter()
.map(|(_, doc)| self.scan(doc.as_ref()).len())
.sum()
}
}
use std::collections::VecDeque;
const MAX_BUFFERED_WORDS: usize = 1000;
const MAX_ACTIVE_ATTEMPTS: usize = 100;
#[derive(Debug, Clone)]
struct BufferedWord {
byte_start: usize,
byte_end: usize,
original: String,
normalized: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AttemptState {
InProgress,
Complete,
Failed,
NoMatch,
}
#[derive(Debug, Clone)]
struct MatchAttempt {
start_word_idx: usize,
current_token_idx: usize,
last_matched_word_idx: usize,
token_details: Vec<TokenMatchDetail>,
total_distance: u8,
}
pub struct StreamingTokenMatcher {
compiled: CompiledTokenQuery,
word_buffer: VecDeque<BufferedWord>,
match_attempts: Vec<MatchAttempt>,
completed_matches: Vec<TokenMatch>,
global_word_idx: usize,
word_idx_offset: usize,
}
impl StreamingTokenMatcher {
pub fn new(compiled: CompiledTokenQuery) -> Self {
Self {
compiled,
word_buffer: VecDeque::with_capacity(64),
match_attempts: Vec::with_capacity(16),
completed_matches: Vec::new(),
global_word_idx: 0,
word_idx_offset: 0,
}
}
pub fn feed_word(&mut self, word: &str, byte_start: usize, byte_end: usize) -> Vec<TokenMatch> {
let normalized = self.normalize(word);
let buffered = BufferedWord {
byte_start,
byte_end,
original: word.to_string(),
normalized,
};
self.word_buffer.push_back(buffered);
let current_word_idx = self.global_word_idx;
self.global_word_idx += 1;
if !self.compiled.token_automata.is_empty() {
self.match_attempts.push(MatchAttempt {
start_word_idx: current_word_idx,
current_token_idx: 0,
last_matched_word_idx: current_word_idx.wrapping_sub(1), token_details: Vec::new(),
total_distance: 0,
});
}
self.process_attempts(current_word_idx);
self.prune_if_needed();
std::mem::take(&mut self.completed_matches)
}
pub fn finish(&mut self) -> Vec<TokenMatch> {
self.match_attempts.clear();
self.word_buffer.clear();
std::mem::take(&mut self.completed_matches)
}
pub fn reset(&mut self) {
self.word_buffer.clear();
self.match_attempts.clear();
self.completed_matches.clear();
self.global_word_idx = 0;
self.word_idx_offset = 0;
}
fn process_attempts(&mut self, current_word_idx: usize) {
let mut i = 0;
while i < self.match_attempts.len() {
let state = self.try_advance_attempt(i, current_word_idx);
match state {
AttemptState::Complete => {
let attempt = self.match_attempts.remove(i);
if let Some(token_match) = self.finalize_attempt(&attempt) {
self.completed_matches.push(token_match);
}
}
AttemptState::Failed => {
self.match_attempts.remove(i);
}
AttemptState::InProgress | AttemptState::NoMatch => {
i += 1;
}
}
}
}
fn try_advance_attempt(&mut self, attempt_idx: usize, current_word_idx: usize) -> AttemptState {
let attempt = &self.match_attempts[attempt_idx];
let token_idx = attempt.current_token_idx;
if token_idx >= self.compiled.token_automata.len() {
return AttemptState::Complete;
}
let buffer_idx = current_word_idx - self.word_idx_offset;
if buffer_idx >= self.word_buffer.len() {
return AttemptState::InProgress; }
if token_idx > 0 {
let sep_idx = token_idx - 1;
let sep = &self.compiled.separators[sep_idx];
match sep {
Separator::Whitespace => {
let last_matched = self.match_attempts[attempt_idx].last_matched_word_idx;
if current_word_idx != last_matched + 1 {
if current_word_idx > last_matched + 1 {
return AttemptState::Failed;
}
return AttemptState::NoMatch;
}
}
Separator::Wildcard => {
}
}
} else {
let start_idx = self.match_attempts[attempt_idx].start_word_idx;
if current_word_idx != start_idx {
return AttemptState::NoMatch;
}
}
let word = &self.word_buffer[buffer_idx];
let product = &self.compiled.token_automata[token_idx];
match product.min_distance(&word.normalized) {
Some(distance) => {
let attempt = &mut self.match_attempts[attempt_idx];
attempt.token_details.push(TokenMatchDetail {
token_index: token_idx,
byte_range: (word.byte_start, word.byte_end),
original_text: word.original.clone(),
normalized_text: word.normalized.clone(),
distance,
});
attempt.total_distance = attempt.total_distance.saturating_add(distance);
attempt.current_token_idx += 1;
attempt.last_matched_word_idx = current_word_idx;
if attempt.current_token_idx >= self.compiled.token_automata.len() {
AttemptState::Complete
} else {
AttemptState::InProgress
}
}
None => {
if token_idx > 0 {
let sep_idx = token_idx - 1;
if self.compiled.separators[sep_idx] == Separator::Wildcard {
AttemptState::NoMatch
} else {
AttemptState::Failed
}
} else {
AttemptState::Failed
}
}
}
}
fn finalize_attempt(&self, attempt: &MatchAttempt) -> Option<TokenMatch> {
if attempt.token_details.is_empty() {
return None;
}
let first = &attempt.token_details[0];
let last = &attempt.token_details[attempt.token_details.len() - 1];
let byte_range = (first.byte_range.0, last.byte_range.1);
let matched_text = attempt
.token_details
.iter()
.map(|d| d.original_text.as_str())
.collect::<Vec<_>>()
.join(" ");
Some(TokenMatch {
byte_range,
token_matches: attempt.token_details.clone(),
total_distance: attempt.total_distance,
matched_text,
})
}
fn prune_if_needed(&mut self) {
let min_start = self
.match_attempts
.iter()
.map(|a| a.start_word_idx)
.min()
.unwrap_or(self.global_word_idx);
let words_to_remove = min_start.saturating_sub(self.word_idx_offset);
if words_to_remove > 0 && words_to_remove <= self.word_buffer.len() {
for _ in 0..words_to_remove {
self.word_buffer.pop_front();
}
self.word_idx_offset += words_to_remove;
}
while self.word_buffer.len() > MAX_BUFFERED_WORDS {
self.word_buffer.pop_front();
self.word_idx_offset += 1;
self.match_attempts
.retain(|a| a.start_word_idx >= self.word_idx_offset);
}
if self.match_attempts.len() > MAX_ACTIVE_ATTEMPTS {
self.match_attempts
.sort_by_key(|a| std::cmp::Reverse(a.current_token_idx));
self.match_attempts.truncate(MAX_ACTIVE_ATTEMPTS);
}
}
fn normalize(&self, text: &str) -> String {
match &self.compiled.rules {
Some(rules) if !rules.is_empty() => {
let vowels = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
let input_phones: Vec<PhoneChar> = text
.chars()
.map(|c| {
if vowels.contains(&c) {
PhoneChar::Vowel(c)
} else {
PhoneChar::Consonant(c)
}
})
.collect();
match apply_rules_seq_char(rules, &input_phones, 100) {
Some(phones) => {
let mut s = String::new();
for p in phones.iter() {
match p {
PhoneChar::Vowel(c) | PhoneChar::Consonant(c) => s.push(*c),
PhoneChar::Digraph(c1, c2) => {
s.push(*c1);
s.push(*c2);
}
PhoneChar::Trigraph(c1, c2, c3) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
}
PhoneChar::Tetragraph(c1, c2, c3, c4) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
}
PhoneChar::Pentagraph(c1, c2, c3, c4, c5) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
s.push(*c5);
}
PhoneChar::Hexagraph(c1, c2, c3, c4, c5, c6) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
s.push(*c5);
s.push(*c6);
}
PhoneChar::Heptagraph(c1, c2, c3, c4, c5, c6, c7) => {
s.push(*c1);
s.push(*c2);
s.push(*c3);
s.push(*c4);
s.push(*c5);
s.push(*c6);
s.push(*c7);
}
PhoneChar::Sequence(chars) => {
for c in chars {
s.push(*c);
}
}
PhoneChar::Silent => {}
}
}
s
}
None => text.to_string(),
}
}
_ => text.to_string(),
}
}
}
impl TokenGrep {
pub fn streaming(&self) -> StreamingTokenMatcher {
StreamingTokenMatcher::new(self.compiled.clone())
}
}
impl Clone for CompiledTokenQuery {
fn clone(&self) -> Self {
Self {
token_automata: self.token_automata.clone(),
separators: self.separators.clone(),
rules: self.rules.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_query() {
let query = parse_query("hello world", 1).expect("should parse");
assert_eq!(query.tokens.len(), 2);
assert_eq!(query.separators.len(), 1);
assert_eq!(query.separators[0], Separator::Whitespace);
match &query.tokens[0].pattern {
TokenPattern::Literal(s) => assert_eq!(s, "hello"),
_ => panic!("expected literal"),
}
assert_eq!(query.tokens[0].max_distance, 1);
match &query.tokens[1].pattern {
TokenPattern::Literal(s) => assert_eq!(s, "world"),
_ => panic!("expected literal"),
}
assert_eq!(query.tokens[1].max_distance, 1);
}
#[test]
fn test_parse_explicit_distance() {
let query = parse_query("error:0 failed:1", 2).expect("should parse");
assert_eq!(query.tokens.len(), 2);
match &query.tokens[0].pattern {
TokenPattern::Literal(s) => assert_eq!(s, "error"),
_ => panic!("expected literal"),
}
assert_eq!(query.tokens[0].max_distance, 0);
match &query.tokens[1].pattern {
TokenPattern::Literal(s) => assert_eq!(s, "failed"),
_ => panic!("expected literal"),
}
assert_eq!(query.tokens[1].max_distance, 1);
}
#[test]
fn test_parse_wildcard() {
let query = parse_query("error .* failed", 1).expect("should parse");
assert_eq!(query.tokens.len(), 2);
assert_eq!(query.separators.len(), 1);
assert_eq!(query.separators[0], Separator::Wildcard);
}
#[test]
fn test_parse_escaped_colon() {
let query = parse_query(r"error\:404:1", 0).expect("should parse");
assert_eq!(query.tokens.len(), 1);
match &query.tokens[0].pattern {
TokenPattern::Literal(s) => assert_eq!(s, "error:404"),
_ => panic!("expected literal"),
}
assert_eq!(query.tokens[0].max_distance, 1);
}
#[test]
fn test_parse_alternation() {
let query = parse_query("(phone|fone):1 call", 0).expect("should parse");
assert_eq!(query.tokens.len(), 2);
match &query.tokens[0].pattern {
TokenPattern::Alternation(alts) => {
assert_eq!(alts.len(), 2);
match &alts[0] {
TokenPattern::Literal(s) => assert_eq!(s, "phone"),
_ => panic!("expected literal in alternation"),
}
match &alts[1] {
TokenPattern::Literal(s) => assert_eq!(s, "fone"),
_ => panic!("expected literal in alternation"),
}
}
_ => panic!("expected alternation"),
}
assert_eq!(query.tokens[0].max_distance, 1);
}
#[test]
fn test_parse_quoted() {
let query = parse_query(r#""stack overflow""#, 1).expect("should parse");
assert_eq!(query.tokens.len(), 1);
match &query.tokens[0].pattern {
TokenPattern::Literal(s) => assert_eq!(s, "stack overflow"),
_ => panic!("expected literal"),
}
}
#[test]
fn test_scan_simple() {
let grep = TokenGrep::new("hello world", 1).expect("should compile");
let matches = grep.scan("hello world");
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].total_distance, 0);
assert_eq!(matches[0].matched_text, "hello world");
}
#[test]
fn test_scan_fuzzy() {
let grep = TokenGrep::new("hello world", 1).expect("should compile");
let matches = grep.scan("helo wrld");
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].total_distance, 2); }
#[test]
fn test_scan_no_separator() {
let grep = TokenGrep::new("hello world", 1).expect("should compile");
let matches = grep.scan("helloworld"); assert_eq!(matches.len(), 0); }
#[test]
fn test_scan_wildcard() {
let grep = TokenGrep::new("error .* failed", 0).expect("should compile");
let matches = grep.scan("error in module failed");
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].token_matches.len(), 2);
assert_eq!(matches[0].token_matches[0].original_text, "error");
assert_eq!(matches[0].token_matches[1].original_text, "failed");
}
#[test]
fn test_scan_explicit_distance() {
let grep = TokenGrep::new("error:0 failed:1", 0).expect("should compile");
let matches = grep.scan("error faild");
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].token_matches[0].distance, 0);
assert_eq!(matches[0].token_matches[1].distance, 1);
let matches2 = grep.scan("erro failed");
assert_eq!(matches2.len(), 0);
}
#[test]
fn test_scan_alternation() {
let grep = TokenGrep::new("(phone|fone) call", 0).expect("should compile");
let matches1 = grep.scan("phone call");
assert_eq!(matches1.len(), 1);
let matches2 = grep.scan("fone call");
assert_eq!(matches2.len(), 1);
}
#[test]
fn test_escaped_characters() {
let query = parse_query(r"C\\Users", 0).expect("should parse");
match &query.tokens[0].pattern {
TokenPattern::Literal(s) => assert_eq!(s, r"C\Users"),
_ => panic!("expected literal"),
}
let query2 = parse_query(r"1\.0", 0).expect("should parse");
match &query2.tokens[0].pattern {
TokenPattern::Literal(s) => assert_eq!(s, "1.0"),
_ => panic!("expected literal"),
}
}
#[test]
fn test_streaming_basic() {
let grep = TokenGrep::new("hello world", 1).expect("should compile");
let mut stream = grep.streaming();
let m1 = stream.feed_word("hello", 0, 5);
assert!(m1.is_empty(), "Should not match with only first word");
let m2 = stream.feed_word("world", 6, 11);
assert_eq!(m2.len(), 1, "Should match after second word");
assert_eq!(m2[0].total_distance, 0);
assert_eq!(m2[0].token_matches.len(), 2);
assert_eq!(m2[0].token_matches[0].original_text, "hello");
assert_eq!(m2[0].token_matches[1].original_text, "world");
let final_matches = stream.finish();
assert!(final_matches.is_empty());
}
#[test]
fn test_streaming_fuzzy() {
let grep = TokenGrep::new("hello world", 1).expect("should compile");
let mut stream = grep.streaming();
stream.feed_word("helo", 0, 4);
let matches = stream.feed_word("wrld", 5, 9);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].total_distance, 2); }
#[test]
fn test_streaming_wildcard() {
let grep = TokenGrep::new("error .* failed", 0).expect("should compile");
let mut stream = grep.streaming();
let m1 = stream.feed_word("error", 0, 5);
assert!(m1.is_empty());
let m2 = stream.feed_word("in", 6, 8);
assert!(m2.is_empty());
let m3 = stream.feed_word("module", 9, 15);
assert!(m3.is_empty());
let m4 = stream.feed_word("failed", 16, 22);
assert_eq!(m4.len(), 1, "Should match with wildcard between tokens");
assert_eq!(m4[0].token_matches.len(), 2);
assert_eq!(m4[0].token_matches[0].original_text, "error");
assert_eq!(m4[0].token_matches[1].original_text, "failed");
}
#[test]
fn test_streaming_no_match() {
let grep = TokenGrep::new("hello world", 0).expect("should compile");
let mut stream = grep.streaming();
stream.feed_word("goodbye", 0, 7);
let matches = stream.feed_word("world", 8, 13);
assert!(matches.is_empty(), "Should not match 'goodbye world'");
}
#[test]
fn test_streaming_multiple_matches() {
let grep = TokenGrep::new("hello world", 1).expect("should compile");
let mut stream = grep.streaming();
stream.feed_word("hello", 0, 5);
let m1 = stream.feed_word("world", 6, 11);
assert_eq!(m1.len(), 1);
stream.feed_word("hello", 12, 17);
let m2 = stream.feed_word("world", 18, 23);
assert_eq!(m2.len(), 1);
}
#[test]
fn test_streaming_reset() {
let grep = TokenGrep::new("hello world", 1).expect("should compile");
let mut stream = grep.streaming();
stream.feed_word("hello", 0, 5);
stream.reset();
stream.feed_word("hello", 0, 5);
let matches = stream.feed_word("world", 6, 11);
assert_eq!(matches.len(), 1);
}
#[test]
fn test_streaming_explicit_distance() {
let grep = TokenGrep::new("error:0 failed:1", 0).expect("should compile");
let mut stream = grep.streaming();
stream.feed_word("error", 0, 5);
let matches = stream.feed_word("faild", 6, 11);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].token_matches[0].distance, 0);
assert_eq!(matches[0].token_matches[1].distance, 1);
stream.reset();
stream.feed_word("erro", 0, 4);
let matches2 = stream.feed_word("failed", 5, 11);
assert!(matches2.is_empty());
}
#[test]
fn test_streaming_single_token() {
let grep = TokenGrep::new("hello", 1).expect("should compile");
let mut stream = grep.streaming();
let matches = stream.feed_word("hello", 0, 5);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].total_distance, 0);
stream.reset();
let matches2 = stream.feed_word("helo", 0, 4);
assert_eq!(matches2.len(), 1);
assert_eq!(matches2[0].total_distance, 1);
}
}