use std::borrow::Cow;
pub fn tokens(text: &str) -> impl Iterator<Item = &str> {
text.split(|c: char| !c.is_alphanumeric()).filter(|t| !t.is_empty())
}
#[must_use]
pub fn normalize(token: &str) -> Cow<'_, str> {
if token.chars().all(|c| !c.is_uppercase()) {
Cow::Borrowed(token)
} else if token.is_ascii() {
Cow::Owned(token.to_ascii_lowercase())
} else {
Cow::Owned(token.to_lowercase())
}
}
pub fn for_each_term(text: &str, mut f: impl FnMut(&str)) {
let mut buf = String::new();
for t in tokens(text) {
match normalize(t) {
Cow::Borrowed(t) => f(t),
Cow::Owned(o) => {
buf.clear();
buf.push_str(&o);
f(&buf);
}
}
}
}
pub fn for_each_char_ngram(token: &str, n: usize, mut f: impl FnMut(&str)) {
if n == 0 || token.is_empty() {
return;
}
let mut ends = token.char_indices().map(|(i, _)| i).chain(std::iter::once(token.len()));
let mut starts = token.char_indices().map(|(i, _)| i);
let Some(mut end) = ends.nth(n) else {
f(token);
return;
};
for start in starts.by_ref() {
f(&token[start..end]);
match ends.next() {
Some(e) => end = e,
None => break,
}
}
}
static STOPWORDS: &[&str] = &[
"a", "about", "above", "after", "again", "all", "am", "an", "and", "any", "are", "as", "at",
"be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can",
"did", "do", "does", "doing", "down", "during", "each", "few", "for", "from", "further", "had",
"has", "have", "having", "he", "her", "here", "hers", "him", "his", "how", "i", "if", "in",
"into", "is", "it", "its", "itself", "just", "me", "more", "most", "my", "no", "nor", "not",
"of", "off", "on", "once", "only", "or", "other", "our", "out", "over", "own", "same", "she",
"should", "so", "some", "such", "than", "that", "the", "their", "them", "then", "there",
"these", "they", "this", "those", "through", "to", "too", "under", "until", "up", "very",
"was", "we", "were", "what", "when", "where", "which", "while", "who", "whom", "why", "will",
"with", "you", "your",
];
#[must_use]
pub fn is_stopword(term: &str) -> bool {
STOPWORDS.binary_search(&term).is_ok()
}
#[must_use]
#[inline]
pub fn estimate_tokens(bytes: usize) -> u64 {
(bytes as u64).div_ceil(4)
}
#[must_use]
#[inline]
pub fn bytes_for_tokens(tokens: u64) -> usize {
usize::try_from(tokens.saturating_mul(4)).unwrap_or(usize::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokens_are_zero_copy_unicode_runs() {
assert_eq!(
tokens("Hello, world! foo_bar.baz").collect::<Vec<_>>(),
["Hello", "world", "foo", "bar", "baz"]
);
assert_eq!(tokens("café—über").collect::<Vec<_>>(), ["café", "über"]);
}
#[test]
fn terms_fold_case_and_borrow_when_they_can() {
assert!(matches!(normalize("already"), Cow::Borrowed(_)));
assert_eq!(normalize("ÜBER"), "über");
let mut t = Vec::new();
for_each_term("Install FooBar v2.1 (Löwe)", |x| t.push(x.to_owned()));
assert_eq!(t, ["install", "foobar", "v2", "1", "löwe"]);
}
#[test]
fn char_ngrams_window_on_char_boundaries() {
let grams = |s: &str, n| {
let mut v = Vec::new();
for_each_char_ngram(s, n, |g| v.push(g.to_owned()));
v
};
assert_eq!(grams("hello", 3), ["hel", "ell", "llo"]);
assert_eq!(grams("hi", 3), ["hi"]);
assert_eq!(grams("abc", 3), ["abc"]);
assert_eq!(grams("café", 2), ["ca", "af", "fé"]);
assert!(grams("", 2).is_empty());
}
#[test]
fn stopwords_and_estimate() {
assert!(is_stopword("the") && !is_stopword("bigquery"));
assert!(STOPWORDS.windows(2).all(|w| w[0] < w[1]));
assert_eq!((estimate_tokens(0), estimate_tokens(1), estimate_tokens(8)), (0, 1, 2));
assert_eq!(estimate_tokens(bytes_for_tokens(250)), 250);
}
}