use regex::Regex;
use std::sync::LazyLock;
static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+")
.expect("pre-tokenization pattern is a fixed, tested constant")
});
pub fn split(text: &str) -> Vec<(usize, usize)> {
let raw: Vec<(usize, usize)> = PATTERN
.find_iter(text)
.map(|m| (m.start(), m.end()))
.collect();
let mut out = Vec::with_capacity(raw.len());
let mut pending_start: Option<usize> = None;
for (i, &(start, end)) in raw.iter().enumerate() {
let effective_start = pending_start.take().unwrap_or(start);
let is_last = i + 1 == raw.len();
let chunk = &text[start..end];
if is_last || !is_all_whitespace(chunk) {
out.push((effective_start, end));
continue;
}
let (last_char_offset, last_char) = chunk
.char_indices()
.last()
.expect("is_all_whitespace guarantees `chunk` is non-empty");
let split_at = start + last_char_offset;
if split_at > effective_start {
out.push((effective_start, split_at));
}
if last_char == ' ' {
pending_start = Some(split_at);
} else {
out.push((split_at, end));
}
}
out
}
fn is_all_whitespace(s: &str) -> bool {
!s.is_empty() && s.chars().all(char::is_whitespace)
}
#[cfg(test)]
mod tests {
use super::*;
fn chunks(text: &str) -> Vec<&str> {
split(text).into_iter().map(|(s, e)| &text[s..e]).collect()
}
#[test]
fn splits_words_and_punctuation() {
assert_eq!(chunks("Hello, world!"), vec!["Hello", ",", " world", "!"]);
}
#[test]
fn single_space_attaches_to_the_following_word() {
assert_eq!(chunks("a b"), vec!["a", " b"]);
}
#[test]
fn multi_space_run_holds_back_exactly_one_trailing_space() {
assert_eq!(chunks("a b"), vec!["a", " ", " b"]);
}
#[test]
fn trailing_whitespace_at_end_of_string_is_not_split() {
assert_eq!(chunks("a "), vec!["a", " "]);
}
#[test]
fn contractions_are_kept_whole() {
assert_eq!(chunks("don't"), vec!["don", "'t"]);
}
#[test]
fn numbers_and_letters_split_apart() {
assert_eq!(chunks("room101"), vec!["room", "101"]);
}
#[test]
fn lone_newline_before_a_word_does_not_fold_forward() {
assert_eq!(chunks("a\nb"), vec!["a", "\n", "b"]);
}
#[test]
fn newline_runs_do_not_fold_into_the_next_word() {
assert_eq!(chunks("a\n\n\nb"), vec!["a", "\n\n", "\n", "b"]);
}
#[test]
fn tab_run_splits_into_two_standalone_chunks() {
assert_eq!(chunks("a\t\tb"), vec!["a", "\t", "\t", "b"]);
}
#[test]
fn mixed_run_ending_in_space_still_folds_the_final_space_forward() {
assert_eq!(chunks("a \n b"), vec!["a", " \n", " b"]);
}
#[test]
fn empty_string_has_no_chunks() {
assert!(chunks("").is_empty());
}
#[test]
fn cjk_and_emoji_are_treated_as_letter_runs_or_punctuation() {
let joined: String = chunks("你好 🎉 world").concat();
assert_eq!(joined, "你好 🎉 world");
}
}