use std::borrow::Cow;
use super::error::TextError;
use super::limits::TextLimits;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Standardize {
None,
Lower,
StripPunctuation,
#[default]
LowerAndStripPunctuation,
UnicodeLower,
}
pub(crate) fn standardize<'a>(
input: &'a str,
mode: Standardize,
limits: &TextLimits,
index: usize,
) -> Result<Cow<'a, str>, TextError> {
let (lower, strip) = match mode {
Standardize::None => return Ok(Cow::Borrowed(input)),
Standardize::UnicodeLower => return unicode_lower(input, limits, index),
Standardize::Lower => (true, false),
Standardize::StripPunctuation => (false, true),
Standardize::LowerAndStripPunctuation => (true, true),
};
let needs_change = input
.chars()
.any(|c| (lower && c.is_ascii_uppercase()) || (strip && c.is_ascii_punctuation()));
if !needs_change {
return Ok(Cow::Borrowed(input));
}
let mut out = String::with_capacity(input.len());
for c in input.chars() {
if strip && c.is_ascii_punctuation() {
continue;
}
out.push(if lower { c.to_ascii_lowercase() } else { c });
}
Ok(Cow::Owned(out))
}
fn unicode_lower<'a>(
input: &'a str,
limits: &TextLimits,
index: usize,
) -> Result<Cow<'a, str>, TextError> {
let changes = |c: char| {
let mut it = c.to_lowercase();
!(it.next() == Some(c) && it.next().is_none())
};
if !input.chars().any(changes) {
return Ok(Cow::Borrowed(input));
}
let mut len = 0usize;
for c in input.chars() {
for l in c.to_lowercase() {
len = len.saturating_add(l.len_utf8());
}
}
limits.check_input_len(index, len)?;
let mut out = String::with_capacity(len);
for c in input.chars() {
out.extend(c.to_lowercase());
}
Ok(Cow::Owned(out))
}
#[cfg(test)]
mod tests {
use super::*;
const MODES: [Standardize; 5] = [
Standardize::UnicodeLower,
Standardize::None,
Standardize::Lower,
Standardize::StripPunctuation,
Standardize::LowerAndStripPunctuation,
];
fn std_ok(s: &str, m: Standardize) -> Cow<'_, str> {
let Ok(c) = standardize(s, m, &TextLimits::default(), 0) else {
panic!("default limits must accept");
};
c
}
fn run(s: &str, m: Standardize) -> String {
std_ok(s, m).into_owned()
}
fn punct_count(s: &str) -> usize {
s.chars().filter(|c| c.is_ascii_punctuation()).count()
}
fn all_strings() -> Vec<String> {
let alphabet = ['a', 'A', 'Z', ' ', '\t', '.', '!', 'é', 'あ', '\u{3000}'];
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 代表入力の期待値() {
let s = "Hello, World!";
assert_eq!(run(s, Standardize::None), s);
assert_eq!(run(s, Standardize::Lower), "hello, world!");
assert_eq!(run(s, Standardize::StripPunctuation), "Hello World");
assert_eq!(run(s, Standardize::LowerAndStripPunctuation), "hello world");
}
#[test]
fn 空入力と空白のみは不変() {
for m in MODES {
assert_eq!(run("", m), "");
assert_eq!(run(" \t\n", m), " \t\n");
}
assert_eq!(run("!?.,", Standardize::StripPunctuation), "");
assert_eq!(run("!?.,", Standardize::LowerAndStripPunctuation), "");
}
#[test]
fn ascii_限定の帰結を固定する() {
assert_eq!(run("ÉSSAİ ß", Standardize::Lower), "Éssaİ ß");
assert_eq!(run("!、。", Standardize::StripPunctuation), "!、。");
assert_eq!(
run("ABC", Standardize::LowerAndStripPunctuation),
"ABC"
);
assert_eq!(run("日本語ABC", Standardize::Lower), "日本語abc");
}
#[test]
fn 変更不要なら借用を返す() {
assert!(matches!(
std_ok("Abc!", Standardize::None),
Cow::Borrowed(_)
));
assert!(matches!(
std_ok("abc", Standardize::Lower),
Cow::Borrowed(_)
));
}
#[test]
fn 長さ不変条件_lower() {
for s in all_strings() {
let o = run(&s, Standardize::Lower);
assert_eq!(o.len(), s.len());
assert_eq!(o.chars().count(), s.chars().count());
assert_eq!(run(&o, Standardize::Lower), o);
}
}
#[test]
fn 長さ不変条件_strip() {
for s in all_strings() {
let o = run(&s, Standardize::StripPunctuation);
assert_eq!(o.len(), s.len() - punct_count(&s));
assert_eq!(punct_count(&o), 0);
assert_eq!(run(&o, Standardize::StripPunctuation), o);
}
}
#[test]
fn 長さ不変条件_両方() {
for s in all_strings() {
let both = run(&s, Standardize::LowerAndStripPunctuation);
assert!(both.len() <= s.len());
let a = run(&run(&s, Standardize::Lower), Standardize::StripPunctuation);
let b = run(&run(&s, Standardize::StripPunctuation), Standardize::Lower);
assert_eq!(a, b);
assert_eq!(both, a);
}
}
#[test]
fn unicode_小文字化の代表値() {
let u = |s| run(s, Standardize::UnicodeLower);
assert_eq!(u("ß"), "ß");
assert_eq!(u("ẞ"), "ß");
assert_eq!(u("İ"), "i\u{307}");
assert_eq!(u("ÉSSAİ"), "éssai\u{307}");
assert_eq!(u("Hello, World!"), "hello, world!");
assert_eq!(u("ΟΔΟΣ"), "οδοσ");
assert_eq!("ΟΔΟΣ".to_lowercase(), "οδος");
}
#[test]
fn unicode_小文字化は変更不要なら借用() {
for s in ["", " \t", "abc", "ß", "日本語"] {
assert!(matches!(
std_ok(s, Standardize::UnicodeLower),
Cow::Borrowed(_)
));
}
}
#[test]
fn unicode_小文字化は冪等() {
for s in all_strings() {
let o = run(&s, Standardize::UnicodeLower);
assert_eq!(run(&o, Standardize::UnicodeLower), o);
}
}
#[test]
fn unicode_小文字化の上限は標準化後のバイト数で境界検査() {
let lim = |n: usize| {
let Ok(l) = TextLimits::default().with_max_input_bytes(n) else {
panic!("limit must build");
};
l
};
let m = Standardize::UnicodeLower;
assert!(standardize("İ", m, &lim(3), 0).is_ok());
assert!(matches!(
standardize("İ", m, &lim(2), 7),
Err(TextError::InputTooLong {
index: 7,
len: 3,
max: 2
})
));
}
#[test]
fn unicode_小文字化の膨張率は_3_2_以下() {
for c in char::MIN..=char::MAX {
let o: usize = c.to_lowercase().map(char::len_utf8).sum();
assert!(o * 2 <= c.len_utf8() * 3, "{c:?}");
}
}
#[test]
fn 既定値は両方() {
assert_eq!(
Standardize::default(),
Standardize::LowerAndStripPunctuation
);
}
}