use imara_diff::TokenSource;
#[derive(Clone, Copy)]
pub struct WordTokenizer<'a>(pub &'a str);
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
impl<'a> TokenSource for WordTokenizer<'a> {
type Token = &'a str;
type Tokenizer = WordTokens<'a>;
fn tokenize(&self) -> Self::Tokenizer {
WordTokens { remaining: self.0 }
}
fn estimate_tokens(&self) -> u32 {
(self.0.len() as u32 / 4).max(1)
}
}
#[derive(Clone, Copy)]
pub struct WordTokens<'a> {
remaining: &'a str,
}
impl<'a> Iterator for WordTokens<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if self.remaining.is_empty() {
return None;
}
let mut char_indices = self.remaining.char_indices();
let (_, first_char) = char_indices.next().expect("checked non-empty above");
let first_is_word = is_word_char(first_char);
let split_at = char_indices
.find(|&(_, c)| is_word_char(c) != first_is_word)
.map_or(self.remaining.len(), |(idx, _)| idx);
let (token, rest) = self.remaining.split_at(split_at);
self.remaining = rest;
Some(token)
}
}
#[derive(Clone, Copy)]
pub struct CharTokenizer<'a>(pub &'a str);
impl<'a> TokenSource for CharTokenizer<'a> {
type Token = char;
type Tokenizer = std::str::Chars<'a>;
fn tokenize(&self) -> Self::Tokenizer {
self.0.chars()
}
fn estimate_tokens(&self) -> u32 {
self.0.len() as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_tokenizer_round_trips_losslessly() {
let input = "hello, world! foo_bar 123";
let tokens: Vec<&str> = WordTokenizer(input).tokenize().collect();
assert_eq!(tokens.concat(), input);
}
#[test]
fn word_tokenizer_groups_word_chars_together() {
let tokens: Vec<&str> = WordTokenizer("foo_bar123 baz").tokenize().collect();
assert_eq!(tokens, vec!["foo_bar123", " ", "baz"]);
}
#[test]
fn char_tokenizer_round_trips_losslessly() {
let input = "héllo wörld";
let tokens: Vec<char> = CharTokenizer(input).tokenize().collect();
let rebuilt: String = tokens.into_iter().collect();
assert_eq!(rebuilt, input);
}
}