use crate::domain::language::traits::{BoundaryContext, BoundaryDecision};
use crate::domain::BoundaryFlags;
use regex::Regex;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub enum ContextCondition {
FollowedByCapital,
FollowedByLowercase,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct ContextRule {
pub condition: ContextCondition,
pub is_boundary: bool,
}
#[derive(Debug)]
pub struct ExceptionPattern {
pub regex: Regex,
pub is_boundary: bool,
}
#[derive(Debug)]
pub struct EllipsisRules {
treat_as_boundary: bool,
patterns: HashSet<String>,
context_rules: Vec<ContextRule>,
exception_patterns: Vec<ExceptionPattern>,
}
impl EllipsisRules {
pub fn new(
treat_as_boundary: bool,
patterns: Vec<String>,
context_rules: Vec<(String, bool)>,
exceptions: Vec<(String, bool)>,
) -> Result<Self, String> {
let parsed_context_rules: Vec<ContextRule> = context_rules
.into_iter()
.map(|(condition_str, is_boundary)| {
let condition = match condition_str.as_str() {
"followed_by_capital" => ContextCondition::FollowedByCapital,
"followed_by_lowercase" => ContextCondition::FollowedByLowercase,
other => ContextCondition::Custom(other.to_string()),
};
ContextRule {
condition,
is_boundary,
}
})
.collect();
let mut parsed_exceptions = Vec::new();
for (pattern_str, is_boundary) in exceptions {
let regex = Regex::new(&pattern_str)
.map_err(|e| format!("Invalid exception regex '{pattern_str}': {e}"))?;
parsed_exceptions.push(ExceptionPattern { regex, is_boundary });
}
Ok(Self {
treat_as_boundary,
patterns: patterns.into_iter().collect(),
context_rules: parsed_context_rules,
exception_patterns: parsed_exceptions,
})
}
pub fn is_ellipsis_pattern(&self, context: &BoundaryContext) -> bool {
for pattern in &self.patterns {
if self.matches_pattern_at_position(&context.text, context.position, pattern) {
return true;
}
}
false
}
fn matches_pattern_at_position(&self, text: &str, position: usize, pattern: &str) -> bool {
let pattern_len = pattern.len();
for offset in 0..pattern_len {
if position >= offset {
let start = position - offset;
let end = start + pattern_len;
if end <= text.len()
&& text.is_char_boundary(start)
&& text.is_char_boundary(end)
&& &text[start..end] == pattern
{
return offset == pattern_len - 1;
}
}
}
false
}
pub fn evaluate_boundary(&self, context: &BoundaryContext) -> BoundaryDecision {
if let Some(decision) = self.check_exceptions(context) {
return decision;
}
for rule in &self.context_rules {
if let Some(decision) = self.apply_context_rule(context, rule) {
return decision;
}
}
if self.treat_as_boundary {
BoundaryDecision::Boundary(BoundaryFlags::WEAK)
} else {
BoundaryDecision::NotBoundary
}
}
fn check_exceptions(&self, context: &BoundaryContext) -> Option<BoundaryDecision> {
let mut start = context.position.saturating_sub(20);
while start > 0 && !context.text.is_char_boundary(start) {
start -= 1;
}
let mut end = (context.position + 20).min(context.text.len());
while end < context.text.len() && !context.text.is_char_boundary(end) {
end += 1;
}
let window = &context.text[start..end];
for exception in &self.exception_patterns {
if exception.regex.is_match(window) {
return Some(if exception.is_boundary {
BoundaryDecision::Boundary(BoundaryFlags::WEAK)
} else {
BoundaryDecision::NotBoundary
});
}
}
None
}
fn apply_context_rule(
&self,
context: &BoundaryContext,
rule: &ContextRule,
) -> Option<BoundaryDecision> {
let matches = match &rule.condition {
ContextCondition::FollowedByCapital => self.is_followed_by_capital(context),
ContextCondition::FollowedByLowercase => self.is_followed_by_lowercase(context),
ContextCondition::Custom(_) => {
false
}
};
if matches {
Some(if rule.is_boundary {
BoundaryDecision::Boundary(BoundaryFlags::WEAK)
} else {
BoundaryDecision::NotBoundary
})
} else {
None
}
}
fn is_followed_by_capital(&self, context: &BoundaryContext) -> bool {
context
.following_context
.chars()
.find(|&ch| ch.is_alphabetic())
.map(|ch| ch.is_uppercase())
.unwrap_or(false)
}
fn is_followed_by_lowercase(&self, context: &BoundaryContext) -> bool {
context
.following_context
.chars()
.find(|&ch| ch.is_alphabetic())
.map(|ch| ch.is_lowercase())
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ellipsis_pattern_detection() {
let rules = EllipsisRules::new(
true,
vec!["...".to_string(), "…".to_string()],
vec![],
vec![],
)
.unwrap();
let context = BoundaryContext {
text: "Hello... World".to_string(),
position: 7, boundary_char: '.',
preceding_context: "Hello..".to_string(),
following_context: " World".to_string(),
};
assert!(rules.is_ellipsis_pattern(&context));
}
#[test]
fn test_context_rules() {
let rules = EllipsisRules::new(
true,
vec!["...".to_string()],
vec![
("followed_by_capital".to_string(), true),
("followed_by_lowercase".to_string(), false),
],
vec![],
)
.unwrap();
let context = BoundaryContext {
text: "Wait... Then he left.".to_string(),
position: 7,
boundary_char: '.',
preceding_context: "Wait..".to_string(),
following_context: " Then he left.".to_string(),
};
match rules.evaluate_boundary(&context) {
BoundaryDecision::Boundary(_) => {}
_ => panic!("Expected boundary when followed by capital"),
}
let context = BoundaryContext {
text: "Wait... then he left.".to_string(),
position: 7,
boundary_char: '.',
preceding_context: "Wait..".to_string(),
following_context: " then he left.".to_string(),
};
assert_eq!(
rules.evaluate_boundary(&context),
BoundaryDecision::NotBoundary
);
}
#[test]
fn test_exception_patterns() {
let rules = EllipsisRules::new(
true,
vec!["...".to_string()],
vec![],
vec![(r"\b(um|uh|er)\.\.\.".to_string(), false)],
)
.unwrap();
let context = BoundaryContext {
text: "He said um... I think so.".to_string(),
position: 12, boundary_char: '.',
preceding_context: "He said um..".to_string(),
following_context: " I think so.".to_string(),
};
assert_eq!(
rules.evaluate_boundary(&context),
BoundaryDecision::NotBoundary
);
}
#[test]
fn test_default_behavior() {
let rules = EllipsisRules::new(true, vec!["...".to_string()], vec![], vec![]).unwrap();
let context = BoundaryContext {
text: "Hello...".to_string(),
position: 7,
boundary_char: '.',
preceding_context: "Hello..".to_string(),
following_context: "".to_string(),
};
match rules.evaluate_boundary(&context) {
BoundaryDecision::Boundary(flags) => assert_eq!(flags, BoundaryFlags::WEAK),
_ => panic!("Expected weak boundary"),
}
let rules = EllipsisRules::new(false, vec!["...".to_string()], vec![], vec![]).unwrap();
assert_eq!(
rules.evaluate_boundary(&context),
BoundaryDecision::NotBoundary
);
}
}