use dashmap::DashMap;
use log::debug;
use crate::analysis::Punctuation;
use crate::config::RuntimeConfig;
pub trait SentenceAnalyzer {
fn extract_sentences(text: &str) -> Vec<String>;
fn analyze_sentence_structure(sentence: &str) -> SentenceStats;
fn extract_sentence_patterns(sentences: &[String], config: &RuntimeConfig) -> Vec<String>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SentenceEmphasis {
None,
Question,
Exclamation,
}
#[derive(Debug, Clone)]
pub struct SentenceStats {
pub word_count: usize,
pub char_count: usize,
pub has_quotes: bool,
pub emphasis: SentenceEmphasis,
pub ends_with_period: bool,
}
fn sentence_emphasis(sentence: &str) -> SentenceEmphasis {
let last_q = sentence.rfind(Punctuation::QUESTION);
let last_e = sentence.rfind(Punctuation::EXCLAMATION);
match (last_q, last_e) {
(Some(q), Some(e)) => {
if q > e {
SentenceEmphasis::Question
} else {
SentenceEmphasis::Exclamation
}
}
(Some(_), None) => SentenceEmphasis::Question,
(None, Some(_)) => SentenceEmphasis::Exclamation,
(None, None) => SentenceEmphasis::None,
}
}
impl SentenceStats {
pub fn new(sentence: &str) -> Self {
let words: Vec<&str> = sentence.split_whitespace().collect();
Self {
word_count: words.len(),
char_count: sentence.chars().count(),
has_quotes: sentence.contains(Punctuation::is_quote),
emphasis: sentence_emphasis(sentence),
ends_with_period: sentence.trim_end().ends_with(Punctuation::PERIOD),
}
}
}
pub struct DefaultSentenceAnalyzer;
impl SentenceAnalyzer for DefaultSentenceAnalyzer {
fn extract_sentences(text: &str) -> Vec<String> {
let normalized = text
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && line.chars().any(char::is_alphanumeric))
.collect::<Vec<_>>()
.join(" ");
if normalized.is_empty() {
return Vec::new();
}
let mut sentences = Vec::new();
let mut current_sentence = String::new();
let chars: Vec<char> = normalized.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
current_sentence.push(ch);
if Punctuation::is_end_punct(ch) {
if ch == Punctuation::PERIOD {
let next_is_period = chars.get(i + 1).copied() == Some(Punctuation::PERIOD);
let prev_is_period =
i > 0 && chars.get(i - 1).copied() == Some(Punctuation::PERIOD);
if next_is_period || prev_is_period {
i += 1;
continue;
}
}
let prev_char = if i > 0 {
chars.get(i - 1).copied()
} else {
None
};
if let Some(p) = prev_char
&& p.is_ascii_digit()
{
i += 1;
continue;
}
if let Some(p) = prev_char
&& p.is_alphabetic()
&& (i == 1 || !chars.get(i - 2).is_some_and(|c| c.is_alphabetic()))
{
i += 1;
continue;
}
let next_char_idx = i + 1;
let next_char = chars.get(next_char_idx).copied();
if next_char == Some(Punctuation::CLOSE_PAREN) {
i += 1;
continue;
}
let should_split = match next_char {
Some(c) if c.is_lowercase() => false,
Some(Punctuation::SPACE | Punctuation::NEWLINE | Punctuation::TAB) => {
let mut j = next_char_idx + 1;
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
if j >= chars.len() {
true } else {
chars[j].is_uppercase()
}
}
_ => true,
};
if should_split {
let sentence = current_sentence.trim().to_string();
if !sentence.is_empty() {
sentences.push(sentence);
}
current_sentence.clear();
}
}
i += 1;
}
let remaining = current_sentence.trim().to_string();
if !remaining.is_empty() {
sentences.push(remaining);
}
debug!("Extracted {} sentences from text", sentences.len());
sentences
}
fn analyze_sentence_structure(sentence: &str) -> SentenceStats {
SentenceStats::new(sentence)
}
fn extract_sentence_patterns(sentences: &[String], config: &RuntimeConfig) -> Vec<String> {
let pattern_freq: DashMap<String, usize> = DashMap::new();
for sentence in sentences {
let stats = Self::analyze_sentence_structure(sentence);
let pattern = format!(
"[SENTENCE:words={},quotes={},emphasis={:?}]",
stats.word_count, stats.has_quotes, stats.emphasis
);
pattern_freq
.entry(pattern)
.and_modify(|c| *c += 1)
.or_insert(1);
}
let threshold = (sentences.len() as f64 * config.text_threshold) as usize;
pattern_freq
.iter()
.filter(|entry| *entry.value() >= threshold)
.map(|entry| entry.key().clone())
.collect()
}
}