use crate::application::parser::{
ParseError, ParseStrategy, ParsingInput, ParsingOutput, SequentialParser, StreamingParser,
TextParser,
};
use crate::domain::enclosure::{EnclosureChar, EnclosureType};
use crate::domain::language::{
AbbreviationResult, BoundaryContext, BoundaryDecision, LanguageRules, QuotationContext,
QuotationDecision,
};
use crate::domain::types::BoundaryFlags;
struct RealisticMockRules {
detect_abbreviations: bool,
abbreviations: Vec<&'static str>,
}
impl RealisticMockRules {
fn new() -> Self {
Self {
detect_abbreviations: true,
abbreviations: vec!["Dr", "Mr", "Mrs", "Ms", "Prof", "Inc", "Ltd"],
}
}
}
impl LanguageRules for RealisticMockRules {
fn detect_sentence_boundary(&self, context: &BoundaryContext) -> BoundaryDecision {
if context.boundary_char == '.' {
if self.detect_abbreviations {
let before_period = context.preceding_context.trim_end();
if let Some(last_word_start) = before_period.rfind(|c: char| c.is_whitespace()) {
let word = &before_period[last_word_start + 1..];
if self.abbreviations.contains(&word) {
return BoundaryDecision::NotBoundary;
}
} else {
if self.abbreviations.contains(&before_period) {
return BoundaryDecision::NotBoundary;
}
}
}
let trimmed = context.following_context.trim_start();
if let Some(first_char) = trimmed.chars().next() {
if first_char.is_uppercase() {
return BoundaryDecision::Boundary(BoundaryFlags::STRONG);
}
}
BoundaryDecision::NeedsMoreContext
} else if matches!(context.boundary_char, '!' | '?') {
BoundaryDecision::Boundary(BoundaryFlags::STRONG)
} else {
BoundaryDecision::NotBoundary
}
}
fn process_abbreviation(&self, text: &str, position: usize) -> AbbreviationResult {
if !self.detect_abbreviations {
return AbbreviationResult {
is_abbreviation: false,
confidence: 0.0,
length: 0,
};
}
if position == 0 || text.chars().nth(position.saturating_sub(1)) != Some('.') {
return AbbreviationResult {
is_abbreviation: false,
confidence: 0.0,
length: 0,
};
}
let before_period = &text[..position.saturating_sub(1)];
let start = before_period
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
let word = &before_period[start..];
let is_abbr = self.abbreviations.contains(&word);
AbbreviationResult {
is_abbreviation: is_abbr,
confidence: if is_abbr { 0.9 } else { 0.1 },
length: word.len() + 1, }
}
fn handle_quotation(&self, _context: &QuotationContext) -> QuotationDecision {
QuotationDecision::Regular
}
fn get_enclosure_char(&self, ch: char) -> Option<EnclosureChar> {
match ch {
'(' => Some(EnclosureChar {
enclosure_type: EnclosureType::Parenthesis,
is_opening: true,
is_symmetric: false,
}),
')' => Some(EnclosureChar {
enclosure_type: EnclosureType::Parenthesis,
is_opening: false,
is_symmetric: false,
}),
'[' => Some(EnclosureChar {
enclosure_type: EnclosureType::SquareBracket,
is_opening: true,
is_symmetric: false,
}),
']' => Some(EnclosureChar {
enclosure_type: EnclosureType::SquareBracket,
is_opening: false,
is_symmetric: false,
}),
'{' => Some(EnclosureChar {
enclosure_type: EnclosureType::CurlyBrace,
is_opening: true,
is_symmetric: false,
}),
'}' => Some(EnclosureChar {
enclosure_type: EnclosureType::CurlyBrace,
is_opening: false,
is_symmetric: false,
}),
_ => None,
}
}
fn get_enclosure_type_id(&self, ch: char) -> Option<usize> {
match ch {
'(' | ')' => Some(0),
'[' | ']' => Some(1),
'{' | '}' => Some(2),
_ => None,
}
}
fn enclosure_type_count(&self) -> usize {
3 }
fn language_code(&self) -> &str {
"mock"
}
fn language_name(&self) -> &str {
"Mock Language"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parser_with_strategies() {
let parser = TextParser::new();
let rules = RealisticMockRules::new();
let text = "Hello world. This is a test! Another sentence?";
let state = parser.scan_chunk(text, &rules);
assert_eq!(state.boundary_candidates.len(), 3);
let positions: Vec<usize> = state
.boundary_candidates
.iter()
.map(|c| c.local_offset)
.collect();
assert!(positions.contains(&12)); assert!(positions.contains(&28)); assert!(positions.contains(&46)); }
#[test]
fn test_parser_with_abbreviations() {
let parser = TextParser::new();
let rules = RealisticMockRules::new();
let text = "Dr. Smith arrived. Mr. Jones left.";
let state = parser.scan_chunk(text, &rules);
let positions: Vec<usize> = state
.boundary_candidates
.iter()
.map(|c| c.local_offset)
.collect();
assert!(positions.contains(&18)); assert!(positions.contains(&34));
assert!(!positions.contains(&3)); assert!(!positions.contains(&22)); }
#[test]
fn test_sequential_vs_streaming_consistency() {
let sequential = SequentialParser::new();
let streaming = StreamingParser::new();
let rules = RealisticMockRules::new();
let text = "First sentence. Second sentence. Third sentence.";
let seq_result = sequential.parse(ParsingInput::Text(text), &rules).unwrap();
let seq_state = match seq_result {
ParsingOutput::State(state) => state,
_ => panic!("Expected single state"),
};
let stream_result = streaming.parse(ParsingInput::Text(text), &rules).unwrap();
let stream_state = match stream_result {
ParsingOutput::State(state) => state,
_ => panic!("Expected single state"),
};
assert_eq!(
seq_state.boundary_candidates.len(),
stream_state.boundary_candidates.len()
);
assert_eq!(seq_state.chunk_length, stream_state.chunk_length);
}
#[test]
fn test_parser_with_nested_enclosures() {
let parser = TextParser::new();
let rules = RealisticMockRules::new();
let text = "He said (and I quote [from the book]). Done.";
let state = parser.scan_chunk(text, &rules);
assert_eq!(state.deltas.len(), 3);
for delta in &state.deltas {
assert_eq!(delta.net, 0);
}
assert!(!state.boundary_candidates.is_empty());
}
#[test]
fn test_strategy_selection() {
let rules = RealisticMockRules::new();
let text = "Test text. Another sentence.";
let strategies: Vec<Box<dyn ParseStrategy>> = vec![
Box::new(SequentialParser::new()),
Box::new(StreamingParser::new()),
Box::new(SequentialParser::with_chunk_size(16)), Box::new(StreamingParser::with_buffer_size(20, 5)), ];
for (i, strategy) in strategies.iter().enumerate() {
let result = strategy.parse(ParsingInput::Text(text), &rules);
assert!(result.is_ok(), "Strategy {} failed", i);
match result.unwrap() {
ParsingOutput::State(state) => {
assert_eq!(
state.boundary_candidates.len(),
2,
"Strategy {} found wrong number of boundaries",
i
);
}
_ => panic!("Expected single state for strategy {}", i),
}
}
}
#[test]
fn test_chunked_parsing_consistency() {
let parser = StreamingParser::with_buffer_size(1024, 10);
let rules = RealisticMockRules::new();
let full_text = "First part. Second part. Third part.";
let chunks = vec!["First part.", " Second part.", " Third part."];
let single_result = parser.parse(ParsingInput::Text(full_text), &rules).unwrap();
let single_boundaries = match single_result {
ParsingOutput::State(state) => state.boundary_candidates.len(),
_ => panic!("Expected single state"),
};
let chunks_result = parser
.parse(ParsingInput::Chunks(Box::new(chunks.into_iter())), &rules)
.unwrap();
let total_boundaries: usize = match chunks_result {
ParsingOutput::States(states) => {
states.iter().map(|s| s.boundary_candidates.len()).sum()
}
_ => panic!("Expected multiple states"),
};
assert!(single_boundaries > 0);
assert!(total_boundaries > 0);
assert!(total_boundaries <= single_boundaries * 2);
}
#[test]
fn test_error_propagation() {
let sequential = SequentialParser::new();
let streaming = StreamingParser::new();
let rules = RealisticMockRules::new();
let empty_text = ParsingInput::Text("");
assert!(matches!(
sequential.parse(empty_text, &rules),
Err(ParseError::EmptyInput)
));
let empty_chunks = ParsingInput::Chunks(Box::new(std::iter::empty()));
assert!(matches!(
streaming.parse(empty_chunks, &rules),
Err(ParseError::EmptyInput)
));
}
#[test]
fn test_utf8_boundary_handling() {
let parser = TextParser::new();
let rules = RealisticMockRules::new();
let texts = vec![
"Hello 世界. Another sentence.",
"Здравствуй мир! Другое предложение.",
"مرحبا بالعالم. جملة أخرى.",
];
for text in texts {
let state = parser.scan_chunk(text, &rules);
assert_eq!(state.chunk_length, text.len());
assert!(!state.boundary_candidates.is_empty());
for candidate in &state.boundary_candidates {
assert!(text.is_char_boundary(candidate.local_offset));
}
}
}
}