use super::types::{
BoundaryAdjustment, OverlapResult, PartialPattern, PatternType, SuppressionMarker,
SuppressionReason,
};
use crate::{
application::{chunking::base::TextChunk, config::ProcessingResult},
domain::enclosure_suppressor::{EnclosureContext, EnclosureSuppressor},
};
use smallvec::SmallVec;
use std::sync::Arc;
pub const CONTEXT_CHAR_COUNT: usize = 3;
pub const MAX_LIST_ITEM_LINE_OFFSET: usize = 10;
pub mod confidence {
pub const CONTRACTION_HIGH: f32 = 0.95;
pub const CONTRACTION_LOW: f32 = 0.8;
pub const POSSESSIVE: f32 = 0.9;
pub const MEASUREMENT: f32 = 0.95;
pub const LIST_ITEM: f32 = 0.85;
pub const GENERIC_PATTERN: f32 = 0.7;
}
pub struct OverlapProcessor {
overlap_size: usize,
pub(crate) enclosure_suppressor: Arc<dyn EnclosureSuppressor>,
}
impl OverlapProcessor {
pub fn new(overlap_size: usize, enclosure_suppressor: Arc<dyn EnclosureSuppressor>) -> Self {
Self {
overlap_size,
enclosure_suppressor,
}
}
pub fn process_overlap(
&self,
left_chunk: &TextChunk,
right_chunk: &TextChunk,
) -> ProcessingResult<OverlapResult> {
let (overlap_text, left_context, right_context) =
self.extract_overlap_region(left_chunk, right_chunk)?;
let suppressions =
self.detect_suppressions(&overlap_text, left_chunk.end_offset, left_context.len())?;
let partial_patterns = self.detect_partial_patterns(&left_context, &right_context);
let boundary_adjustments = self.calculate_boundary_adjustments(&suppressions);
Ok(OverlapResult {
suppressions,
boundary_adjustments,
extended_context: overlap_text,
partial_patterns,
})
}
fn extract_overlap_region(
&self,
left_chunk: &TextChunk,
right_chunk: &TextChunk,
) -> ProcessingResult<(String, String, String)> {
let left_content = &left_chunk.content;
let right_content = &right_chunk.content;
let left_end = left_content.len();
let mut left_start = left_end.saturating_sub(self.overlap_size);
while left_start < left_end && !left_content.is_char_boundary(left_start) {
left_start += 1;
}
let left_context = &left_content[left_start..];
let mut right_end = right_content.len().min(self.overlap_size);
while right_end > 0 && !right_content.is_char_boundary(right_end) {
right_end -= 1;
}
let right_context = &right_content[..right_end];
let overlap_text = format!("{left_context}{right_context}");
Ok((
overlap_text,
left_context.to_string(),
right_context.to_string(),
))
}
fn detect_suppressions(
&self,
extended_context: &str,
base_offset: usize,
left_context_len: usize,
) -> ProcessingResult<Vec<SuppressionMarker>> {
let mut suppressions = Vec::new();
for (idx, ch) in extended_context.char_indices() {
if !self.is_potential_enclosure(ch) {
continue;
}
let context = self.create_enclosure_context(extended_context, idx, ch);
if self
.enclosure_suppressor
.should_suppress_enclosure(ch, &context)
{
let reason = self.determine_suppression_reason(ch, &context);
let confidence = self.calculate_confidence(&context, &reason);
let position = if idx < left_context_len {
base_offset.saturating_sub(left_context_len - idx)
} else {
base_offset + (idx - left_context_len)
};
suppressions.push(SuppressionMarker {
position,
character: ch,
reason,
confidence,
from_overlap: true,
});
}
}
Ok(suppressions)
}
fn detect_partial_patterns(
&self,
left_context: &str,
right_context: &str,
) -> Vec<PartialPattern> {
let mut patterns = Vec::new();
if left_context.ends_with("isn")
|| left_context.ends_with("don")
|| left_context.ends_with("won")
|| left_context.ends_with("can")
{
patterns.push(PartialPattern {
text: left_context
.chars()
.rev()
.take(3)
.collect::<String>()
.chars()
.rev()
.collect(),
expected_continuations: vec!["'t".to_string()],
pattern_type: PatternType::Contraction,
});
}
if left_context
.chars()
.last()
.map(|c| c.is_alphabetic())
.unwrap_or(false)
&& right_context.starts_with('\'')
{
patterns.push(PartialPattern {
text: left_context.chars().last().unwrap().to_string(),
expected_continuations: vec!["'".to_string(), "'s".to_string()],
pattern_type: PatternType::Possessive,
});
}
patterns
}
fn calculate_boundary_adjustments(
&self,
suppressions: &[SuppressionMarker],
) -> Vec<BoundaryAdjustment> {
let mut adjustments = Vec::new();
for suppression in suppressions {
if matches!(suppression.character, '.' | '!' | '?') {
adjustments.push(BoundaryAdjustment {
original_position: suppression.position,
adjusted_position: None, reason: format!("Suppressed due to {:?}", suppression.reason),
});
}
}
adjustments
}
pub(crate) fn determine_suppression_reason(
&self,
ch: char,
context: &EnclosureContext,
) -> SuppressionReason {
if matches!(ch, '\'' | '\u{2019}') {
let prev_alpha = context
.preceding_chars
.last()
.map(|c| c.is_alphabetic())
.unwrap_or(false);
let next_alpha = context
.following_chars
.first()
.map(|c| c.is_alphabetic())
.unwrap_or(false);
if prev_alpha && next_alpha {
return SuppressionReason::Contraction;
}
if prev_alpha && !next_alpha {
return SuppressionReason::Possessive;
}
if context
.preceding_chars
.last()
.map(|c| c.is_numeric())
.unwrap_or(false)
{
return SuppressionReason::Measurement;
}
}
if ch == ')' && context.line_offset < MAX_LIST_ITEM_LINE_OFFSET {
return SuppressionReason::ListItem;
}
SuppressionReason::CrossChunkPattern {
pattern: format!("{:?}", context.preceding_chars),
}
}
pub(crate) fn calculate_confidence(
&self,
context: &EnclosureContext,
reason: &SuppressionReason,
) -> f32 {
match reason {
SuppressionReason::Contraction => {
if context.preceding_chars.len() >= 2 && !context.following_chars.is_empty() {
confidence::CONTRACTION_HIGH
} else {
confidence::CONTRACTION_LOW
}
}
SuppressionReason::Possessive => confidence::POSSESSIVE,
SuppressionReason::Measurement => confidence::MEASUREMENT,
SuppressionReason::ListItem => confidence::LIST_ITEM,
SuppressionReason::CrossChunkPattern { .. } => confidence::GENERIC_PATTERN,
}
}
pub(crate) fn create_enclosure_context<'a>(
&self,
text: &'a str,
position: usize,
ch: char,
) -> EnclosureContext<'a> {
let preceding_chars: SmallVec<[char; CONTEXT_CHAR_COUNT]> = text[..position]
.chars()
.rev()
.take(CONTEXT_CHAR_COUNT)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
let skip_position = position + ch.len_utf8();
let following_chars: SmallVec<[char; CONTEXT_CHAR_COUNT]> = if skip_position <= text.len() {
text[skip_position..]
.chars()
.take(CONTEXT_CHAR_COUNT)
.collect()
} else {
SmallVec::new()
};
let line_offset = text[..position]
.rfind('\n')
.map(|pos| position - pos - 1)
.unwrap_or(position);
EnclosureContext {
position,
preceding_chars,
following_chars,
line_offset,
chunk_text: text,
}
}
pub(crate) fn is_potential_enclosure(&self, ch: char) -> bool {
matches!(
ch,
'\'' | '"'
| '('
| ')'
| '['
| ']'
| '{'
| '}'
| '\u{2018}'
| '\u{2019}'
| '\u{201C}'
| '\u{201D}'
)
}
}