use std::str::{CharIndices, SplitAsciiWhitespace, SplitWhitespace};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Split {
None,
#[default]
Whitespace,
Character,
UnicodeWhitespace,
}
pub(crate) enum SplitIter<'a> {
Whole(Option<&'a str>),
Whitespace(SplitAsciiWhitespace<'a>),
UnicodeWhitespace(SplitWhitespace<'a>),
Character(&'a str, CharIndices<'a>),
}
impl<'a> Iterator for SplitIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
match self {
SplitIter::Whole(rest) => rest.take(),
SplitIter::Whitespace(it) => it.next(),
SplitIter::UnicodeWhitespace(it) => it.next(),
SplitIter::Character(src, it) => {
let (i, c) = it.next()?;
src.get(i..i + c.len_utf8())
}
}
}
}
pub(crate) fn split(input: &str, mode: Split) -> SplitIter<'_> {
match mode {
Split::None => SplitIter::Whole(Some(input)),
Split::Whitespace => SplitIter::Whitespace(input.split_ascii_whitespace()),
Split::UnicodeWhitespace => SplitIter::UnicodeWhitespace(input.split_whitespace()),
Split::Character => SplitIter::Character(input, input.char_indices()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn toks(s: &str, m: Split) -> Vec<&str> {
split(s, m).collect()
}
fn is_ws(c: char) -> bool {
c.is_ascii_whitespace()
}
fn all_strings() -> Vec<String> {
let alphabet = ['a', 'é', ' ', '\t', '\n', '\u{3000}', '\u{A0}', '日'];
let mut all = vec![String::new()];
let mut frontier = vec![String::new()];
for _ in 0..4 {
let mut next = Vec::new();
for s in &frontier {
for c in alphabet {
let mut t = s.clone();
t.push(c);
next.push(t);
}
}
all.extend(next.iter().cloned());
frontier = next;
}
all
}
#[test]
fn 空白分割() {
assert_eq!(toks(" a b\tc\n", Split::Whitespace), ["a", "b", "c"]);
assert!(toks("", Split::Whitespace).is_empty());
assert!(toks(" \t\n", Split::Whitespace).is_empty());
assert_eq!(toks("a\u{3000}b", Split::Whitespace), ["a\u{3000}b"]);
assert_eq!(toks("a\u{00A0}b", Split::Whitespace), ["a\u{00A0}b"]);
assert_eq!(toks("a\u{0B}b", Split::Whitespace), ["a\u{0B}b"]);
assert_eq!(toks("日本 語", Split::Whitespace), ["日本", "語"]);
}
#[test]
fn 文字分割() {
assert_eq!(toks("aé日", Split::Character), ["a", "é", "日"]);
assert!(toks("", Split::Character).is_empty());
assert_eq!(toks("e\u{301}", Split::Character).len(), 2);
assert_eq!(toks("👨\u{200D}👩", Split::Character).len(), 3);
assert_eq!(toks("a b", Split::Character), ["a", " ", "b"]);
}
#[test]
fn 分割なし() {
assert_eq!(toks("a b", Split::None), ["a b"]);
assert_eq!(toks("", Split::None), [""]);
assert_eq!(toks(" \t", Split::None), [" \t"]);
}
#[test]
fn 長さ不変条件() {
for s in all_strings() {
let ch = toks(&s, Split::Character);
assert_eq!(ch.len(), s.chars().count());
assert!(ch.iter().all(|t| t.chars().count() == 1));
assert_eq!(ch.concat(), s);
assert_eq!(ch.iter().map(|t| t.len()).sum::<usize>(), s.len());
let ws = toks(&s, Split::Whitespace);
assert!(ws.iter().all(|t| !t.is_empty() && !t.contains(is_ws)));
let stripped: String = s.chars().filter(|c| !is_ws(*c)).collect();
assert_eq!(ws.concat(), stripped);
assert!(ws.iter().map(|t| t.len()).sum::<usize>() <= s.len());
assert_eq!(toks(&s, Split::None), [s.as_str()]);
}
}
#[test]
fn unicode_空白分割() {
let u = |s| toks(s, Split::UnicodeWhitespace);
assert_eq!(u("a\u{3000}b"), ["a", "b"]);
assert_eq!(u("a\u{00A0}b"), ["a", "b"]);
assert_eq!(u("a\u{0B}b"), ["a", "b"]);
assert!(u("").is_empty());
assert!(u("\u{3000} \u{A0}").is_empty());
assert_eq!(u("a\u{200B}b"), ["a\u{200B}b"]);
}
#[test]
fn unicode_空白分割の不変条件() {
for s in all_strings() {
let ws = toks(&s, Split::UnicodeWhitespace);
assert!(
ws.iter()
.all(|t| !t.is_empty() && !t.contains(char::is_whitespace))
);
let stripped: String = s.chars().filter(|c| !c.is_whitespace()).collect();
assert_eq!(ws.concat(), stripped);
}
}
#[test]
fn 既定値は空白() {
assert_eq!(Split::default(), Split::Whitespace);
}
}