use std::borrow::Cow;
use std::collections::HashMap;
use super::error::TextError;
use super::limits::{ABSOLUTE_MAX_BYTES, TextLimits};
use super::ngram::ngrams;
use super::split::{Split, split};
use super::standardize::{Standardize, standardize};
use super::vocab::{OOV_TOKEN, PADDING_TOKEN, RESERVED_COUNT, Vocabulary};
struct Entry {
freq: u64,
df: u64,
last_doc: usize,
}
fn increment(value: &mut u64) -> Result<(), TextError> {
*value = value.checked_add(1).ok_or(TextError::FrequencyOverflow)?;
Ok(())
}
fn count_token(
map: &mut HashMap<String, Entry>,
tok: Cow<'_, str>,
doc: usize,
limits: &TextLimits,
) -> Result<(), TextError> {
if tok == PADDING_TOKEN || tok == OOV_TOKEN {
return Ok(());
}
if let Some(e) = map.get_mut(tok.as_ref()) {
increment(&mut e.freq)?;
if e.last_doc != doc {
e.last_doc = doc;
increment(&mut e.df)?;
}
return Ok(());
}
let next = map
.len()
.checked_add(1)
.ok_or(TextError::TooManyDistinctTokens {
max: limits.max_distinct_tokens(),
})?;
limits.check_distinct_tokens(next)?;
map.insert(
tok.into_owned(),
Entry {
freq: 1,
df: 1,
last_doc: doc,
},
);
Ok(())
}
pub(crate) fn adapt<S: AsRef<str>>(
corpus: &[S],
standardize_mode: Standardize,
split_mode: Split,
ngrams_n: Option<usize>,
max_tokens: Option<usize>,
limits: &TextLimits,
) -> Result<Vocabulary, TextError> {
adapt_with_df(
corpus,
standardize_mode,
split_mode,
ngrams_n,
max_tokens,
limits,
)
.map(|(vocab, _)| vocab)
}
pub(crate) fn adapt_with_df<S: AsRef<str>>(
corpus: &[S],
standardize_mode: Standardize,
split_mode: Split,
ngrams_n: Option<usize>,
max_tokens: Option<usize>,
limits: &TextLimits,
) -> Result<(Vocabulary, Vec<u64>), TextError> {
let keep = match max_tokens {
Some(m) => Some(
m.checked_sub(RESERVED_COUNT + 1)
.map(|v| v + 1)
.ok_or(TextError::InvalidMaxTokens { max_tokens: m })?,
),
None => None,
};
if let Some(n) = ngrams_n {
limits.check_ngrams(n)?;
}
limits.check_corpus(corpus)?;
let mut map: HashMap<String, Entry> = HashMap::new();
for (doc, s) in corpus.iter().enumerate() {
let std = standardize(s.as_ref(), standardize_mode, limits, doc)?;
match ngrams_n {
None => {
for t in split(&std, split_mode) {
count_token(&mut map, Cow::Borrowed(t), doc, limits)?;
}
}
Some(n) => {
let toks: Vec<&str> = split(&std, split_mode).collect();
for g in ngrams(&toks, n, limits)? {
count_token(&mut map, g, doc, limits)?;
}
}
}
}
let distinct = map.len();
let selected = keep.map_or(distinct, |k| distinct.min(k));
limits.check_vocabulary_size(selected.saturating_add(RESERVED_COUNT))?;
let mut entries: Vec<(String, Entry)> = map.into_iter().collect();
entries.sort_unstable_by(|a, b| b.1.freq.cmp(&a.1.freq).then_with(|| a.0.cmp(&b.0)));
entries.truncate(selected);
let df: Vec<u64> = entries.iter().map(|(_, e)| e.df).collect();
let tokens: Vec<String> = entries.into_iter().map(|(t, _)| t).collect();
let relaxed = limits.with_max_vocabulary_token_bytes(ABSOLUTE_MAX_BYTES)?;
Ok((Vocabulary::from_tokens(&relaxed, &tokens)?, df))
}
#[cfg(test)]
mod tests {
use super::*;
fn run(corpus: &[&str], max_tokens: Option<usize>) -> Result<Vocabulary, TextError> {
adapt(
corpus,
Standardize::None,
Split::Whitespace,
None,
max_tokens,
&TextLimits::default(),
)
}
fn toks(corpus: &[&str], max_tokens: Option<usize>) -> Vec<String> {
match run(corpus, max_tokens) {
Ok(v) => v.tokens().to_vec(),
Err(e) => panic!("adapt が失敗: {e:?}"),
}
}
fn strs(v: &[&str]) -> Vec<String> {
v.iter().map(|s| (*s).to_owned()).collect()
}
fn permutations(items: &[&'static str]) -> Vec<Vec<&'static str>> {
if items.len() <= 1 {
return vec![items.to_vec()];
}
let mut out = Vec::new();
for i in 0..items.len() {
let mut rest = items.to_vec();
let head = rest.remove(i);
for mut p in permutations(&rest) {
p.insert(0, head);
out.push(p);
}
}
out
}
#[test]
fn orders_by_frequency_then_bytes() {
assert_eq!(
toks(&["b a c", "a b", "a"], None),
strs(&["", "[UNK]", "a", "b", "c"])
);
}
#[test]
fn ties_use_byte_order_for_multibyte() {
assert_eq!(
toks(&["日 é z"], None),
strs(&["", "[UNK]", "z", "é", "日"])
);
}
#[test]
fn corpus_order_does_not_change_result() {
let base = ["x y", "y z", "z w", "w x z"];
let expected = toks(&base, None);
for p in permutations(&base) {
assert_eq!(toks(&p, None), expected, "{p:?}");
}
}
#[test]
fn independent_of_hashmap_iteration_order() {
let corpus = ["d c b a", "h g f e", "a e"];
let first = toks(&corpus, None);
for _ in 0..16 {
assert_eq!(toks(&corpus, None), first);
}
}
#[test]
fn max_tokens_boundaries() {
for m in 0..=2 {
assert!(matches!(
run(&["a"], Some(m)),
Err(TextError::InvalidMaxTokens { max_tokens }) if max_tokens == m
));
}
let c = ["b a c", "a b", "a"];
assert_eq!(toks(&c, Some(3)), strs(&["", "[UNK]", "a"]));
assert_eq!(toks(&c, Some(4)), strs(&["", "[UNK]", "a", "b"]));
assert_eq!(toks(&c, Some(100)), toks(&c, None));
}
#[test]
fn settings_are_checked_before_corpus() {
let limits = match TextLimits::default().with_max_batch(1) {
Ok(l) => l,
Err(e) => panic!("{e:?}"),
};
let big = ["a", "b"];
let r = adapt(
&big,
Standardize::None,
Split::Whitespace,
None,
Some(0),
&limits,
);
assert!(matches!(r, Err(TextError::InvalidMaxTokens { .. })));
for n in [0, 9] {
let r = adapt::<&str>(
&[],
Standardize::None,
Split::Whitespace,
Some(n),
None,
&TextLimits::default(),
);
assert!(matches!(r, Err(TextError::InvalidNgrams { .. })), "n={n}");
}
}
#[test]
fn corpus_limits_at_boundaries() {
let go = |c: &[&str], l: &TextLimits| {
adapt(c, Standardize::None, Split::Whitespace, None, None, l)
};
let l = TextLimits::default().with_max_batch(2).unwrap_or_default();
assert!(go(&["a", "b"], &l).is_ok());
assert!(matches!(
go(&["a", "b", "c"], &l),
Err(TextError::BatchTooLarge { .. })
));
let l = TextLimits::default()
.with_max_input_bytes(3)
.unwrap_or_default();
assert!(go(&["abc"], &l).is_ok());
assert!(matches!(
go(&["abcd"], &l),
Err(TextError::InputTooLong { .. })
));
let l = TextLimits::default()
.with_max_corpus_bytes(5)
.unwrap_or_default();
assert!(go(&["abc", "de"], &l).is_ok());
assert!(matches!(
go(&["abc", "def"], &l),
Err(TextError::CorpusTooLarge { .. })
));
}
#[test]
fn distinct_limit_fires_only_on_new_tokens() {
let l = TextLimits::default()
.with_max_distinct_tokens(2)
.unwrap_or_default();
let go = |c: &[&str]| adapt(c, Standardize::None, Split::Whitespace, None, None, &l);
assert!(go(&["a b a b a b"]).is_ok());
assert!(matches!(
go(&["a b c"]),
Err(TextError::TooManyDistinctTokens { max: 2 })
));
}
#[test]
fn vocabulary_size_is_checked_on_selected_count() {
let l = TextLimits::default()
.with_max_vocabulary_size(4)
.unwrap_or_default();
let go = |mt| {
adapt(
&["a b c"],
Standardize::None,
Split::Whitespace,
None,
mt,
&l,
)
};
assert!(matches!(
go(None),
Err(TextError::VocabularyTooLarge { .. })
));
assert!(go(Some(4)).is_ok());
}
#[test]
fn ngrams_share_one_frequency_table() {
let r = adapt(
&["a b", "a b"],
Standardize::None,
Split::Whitespace,
Some(2),
None,
&TextLimits::default(),
);
let t = match r {
Ok(v) => v.tokens().to_vec(),
Err(e) => panic!("{e:?}"),
};
assert_eq!(t, strs(&["", "[UNK]", "a", "a b", "b"]));
}
#[test]
fn reserved_tokens_in_corpus_are_not_counted() {
let r = adapt(
&["", "!!!", "x"],
Standardize::StripPunctuation,
Split::None,
None,
None,
&TextLimits::default(),
);
assert!(matches!(&r, Ok(v) if v.tokens() == strs(&["", "[UNK]", "x"])));
assert_eq!(toks(&["[UNK] a"], None), strs(&["", "[UNK]", "a"]));
}
#[test]
fn long_token_is_accepted_but_not_readable_back_with_defaults() {
let long = "x".repeat(5000);
let r = adapt(
&[long.as_str()],
Standardize::None,
Split::None,
None,
None,
&TextLimits::default(),
);
let v = match r {
Ok(v) => v,
Err(e) => panic!("{e:?}"),
};
assert_eq!(v.tokens().get(2).map(String::len), Some(5000));
let back = Vocabulary::from_tokens(&TextLimits::default(), &v.tokens()[2..]);
assert!(matches!(
back,
Err(TextError::VocabularyTokenTooLong { .. })
));
}
#[test]
fn character_split_ngram_longer_than_input_is_accepted() {
let l = TextLimits::default()
.with_max_input_bytes(5)
.unwrap_or_default();
let r = adapt(
&["a b c"],
Standardize::None,
Split::Character,
Some(5),
None,
&l,
);
assert!(r.is_ok());
}
#[test]
fn frequency_increment_overflow() {
let mut c = u64::MAX - 1;
assert!(increment(&mut c).is_ok());
assert_eq!(c, u64::MAX);
assert!(matches!(
increment(&mut c),
Err(TextError::FrequencyOverflow)
));
assert_eq!(c, u64::MAX);
}
#[test]
fn empty_and_blank_corpus_give_reserved_only() {
assert_eq!(toks(&[], None).len(), 2);
assert_eq!(toks(&[" ", "\t"], None).len(), 2);
}
#[test]
fn lookup_matches_token_index() {
let v = match run(&["b a c", "a b", "a"], None) {
Ok(v) => v,
Err(e) => panic!("{e:?}"),
};
for (i, t) in v.tokens().iter().enumerate() {
assert_eq!(usize::try_from(v.lookup(t)).ok(), Some(i), "{t}");
}
}
#[test]
fn no_corpus_text_in_debug_or_errors() {
let marker = "SECRET_MARKER_TOKEN";
let v = run(&[marker], None);
assert!(matches!(&v, Ok(v) if !format!("{v:?}").contains(marker)));
let l = TextLimits::default()
.with_max_input_bytes(3)
.unwrap_or_default();
let e = adapt(
&[marker],
Standardize::None,
Split::Whitespace,
None,
None,
&l,
);
match e {
Err(e) => {
assert!(!e.to_string().contains(marker));
assert!(!format!("{e:?}").contains(marker));
}
Ok(_) => panic!("上限超過が Err にならない"),
}
}
#[test]
fn df_counts_documents_not_occurrences() {
let (v, df) = adapt_with_df(
&["a a a b", "a c", "b b"],
Standardize::None,
Split::Whitespace,
None,
None,
&TextLimits::default(),
)
.unwrap_or_else(|e| panic!("{e:?}"));
assert_eq!(v.tokens().to_vec(), strs(&["", "[UNK]", "a", "b", "c"]));
assert_eq!(df, vec![2, 2, 1]);
}
#[test]
fn df_stays_aligned_after_max_tokens_truncation() {
let (v, df) = adapt_with_df(
&["a a a b", "a c", "b b"],
Standardize::None,
Split::Whitespace,
None,
Some(4),
&TextLimits::default(),
)
.unwrap_or_else(|e| panic!("{e:?}"));
assert_eq!(v.tokens().to_vec(), strs(&["", "[UNK]", "a", "b"]));
assert_eq!(df, vec![2, 2]);
}
#[test]
fn df_ignores_reserved_tokens_and_counts_ngrams_per_document() {
let (v, df) = adapt_with_df(
&["[UNK] x x", "x"],
Standardize::None,
Split::Whitespace,
Some(2),
None,
&TextLimits::default(),
)
.unwrap_or_else(|e| panic!("{e:?}"));
assert_eq!(
v.tokens().to_vec(),
strs(&["", "[UNK]", "x", "[UNK] x", "x x"])
);
assert_eq!(df, vec![2, 1, 1]);
}
}