use crate::offsets::{java_strip, java_strip_leading, utf16_len};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Sentence {
pub text: String,
pub start_offset: usize,
pub end_offset: usize,
}
impl Sentence {
pub fn new(text: impl Into<String>, start_offset: usize, end_offset: usize) -> Sentence {
Sentence {
text: text.into(),
start_offset,
end_offset,
}
}
}
const ABBREVIATIONS: [&str; 5] = ["e.g.", "i.e.", "etc.", "cf.", "vs."];
pub fn split_sentences(text: &str) -> Vec<Sentence> {
let chars: Vec<char> = text.chars().collect();
let n = chars.len();
let mut cp_to_u16 = vec![0usize; n + 1];
for i in 0..n {
cp_to_u16[i + 1] = cp_to_u16[i] + chars[i].len_utf16();
}
let mut skip = vec![false; n];
let mut j = 0;
while j < n {
let c = chars[j];
if c == '`' || c == '"' {
match find_char(&chars, j + 1, c) {
Some(close) => {
for entry in skip.iter_mut().take(close + 1).skip(j) {
*entry = true;
}
j = close;
}
None => break,
}
}
j += 1;
}
let mut out = Vec::new();
let mut segment_start = 0usize;
let mut i = 0;
while i < n {
if skip[i] {
i += 1;
continue;
}
let ch = chars[i];
if ch == '\n' || ch == '.' || ch == '!' || ch == '?' {
if ch == '.' && is_inside_number_or_abbrev(&chars, i) {
i += 1;
continue;
}
let end = i + 1;
push_segment(&mut out, &chars, &cp_to_u16, segment_start, end);
i = end;
while i < n && (chars[i] == ' ' || chars[i] == '\n') {
i += 1;
}
segment_start = i;
continue;
}
i += 1;
}
push_segment(&mut out, &chars, &cp_to_u16, segment_start, n);
out
}
fn find_char(chars: &[char], from: usize, target: char) -> Option<usize> {
(from..chars.len()).find(|&k| chars[k] == target)
}
fn push_segment(
out: &mut Vec<Sentence>,
chars: &[char],
cp_to_u16: &[usize],
start: usize,
end: usize,
) {
if end <= start {
return;
}
let raw: String = chars[start..end].iter().collect();
let slice = java_strip(&raw);
if slice.is_empty() {
return;
}
let leading = utf16_len(&raw) - utf16_len(java_strip_leading(&raw));
let trimmed_start = cp_to_u16[start] + leading;
let trimmed_end = trimmed_start + utf16_len(slice);
out.push(Sentence {
text: slice.to_string(),
start_offset: trimmed_start,
end_offset: trimmed_end,
});
}
fn is_inside_number_or_abbrev(chars: &[char], dot_pos: usize) -> bool {
let prev = if dot_pos > 0 {
chars[dot_pos - 1]
} else {
'\0'
};
let next = if dot_pos + 1 < chars.len() {
chars[dot_pos + 1]
} else {
'\0'
};
if prev.is_ascii_digit() && next.is_ascii_digit() {
return true;
}
for abbrev in ABBREVIATIONS {
let len = abbrev.chars().count();
let from = (dot_pos + 1).saturating_sub(len);
let candidate: String = chars[from..dot_pos + 1].iter().collect();
if candidate == abbrev {
return true;
}
}
next.is_ascii_lowercase()
}