use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct FrequencyMap {
pub counts: HashMap<String, u64>,
pub total: u64,
}
impl FrequencyMap {
pub fn from_tokens(tokens: &[String]) -> Self {
let mut counts = HashMap::new();
for t in tokens {
*counts.entry(t.clone()).or_insert(0u64) += 1;
}
let total = tokens.len() as u64;
Self { counts, total }
}
pub fn top_n(&self, n: usize) -> Vec<(String, u64)> {
let mut pairs: Vec<(String, u64)> = self.counts.iter().map(|(k, v)| (k.clone(), *v)).collect();
pairs.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
pairs.truncate(n);
pairs
}
pub fn entropy(&self) -> f64 {
if self.total == 0 {
return 0.0;
}
let total = self.total as f64;
self.counts
.values()
.filter(|&&c| c > 0)
.map(|&c| {
let p = c as f64 / total;
-p * p.log2()
})
.sum()
}
}
#[derive(Debug, Clone)]
pub struct SequenceStats {
pub length: usize,
pub unique_tokens: usize,
pub vocab_richness: f64,
pub avg_token_len: f64,
pub type_token_ratio: f64,
pub hapax_legomena: usize,
}
impl SequenceStats {
pub fn compute(tokens: &[String]) -> Self {
let length = tokens.len();
if length == 0 {
return Self {
length: 0,
unique_tokens: 0,
vocab_richness: 0.0,
avg_token_len: 0.0,
type_token_ratio: 0.0,
hapax_legomena: 0,
};
}
let freq = FrequencyMap::from_tokens(tokens);
let unique_tokens = freq.counts.len();
let vocab_richness = unique_tokens as f64 / length as f64;
let avg_token_len =
tokens.iter().map(|t| t.chars().count()).sum::<usize>() as f64 / length as f64;
let type_token_ratio = vocab_richness;
let hapax_legomena = freq.counts.values().filter(|&&c| c == 1).count();
Self {
length,
unique_tokens,
vocab_richness,
avg_token_len,
type_token_ratio,
hapax_legomena,
}
}
}
pub struct NgramAnalyzer {
pub n: usize,
}
impl NgramAnalyzer {
pub fn new(n: usize) -> Self {
Self { n }
}
pub fn compute(&self, tokens: &[String]) -> HashMap<Vec<String>, u64> {
let mut counts: HashMap<Vec<String>, u64> = HashMap::new();
if tokens.len() < self.n {
return counts;
}
for window in tokens.windows(self.n) {
*counts.entry(window.to_vec()).or_insert(0) += 1;
}
counts
}
pub fn top_ngrams(tokens: &[String], n: usize) -> Vec<(Vec<String>, u64)> {
let analyzer = NgramAnalyzer::new(2);
let counts = analyzer.compute(tokens);
let mut pairs: Vec<(Vec<String>, u64)> =
counts.into_iter().map(|(k, v)| (k, v)).collect();
pairs.sort_by(|a, b| b.1.cmp(&a.1));
pairs.truncate(n);
pairs
}
pub fn top_ngrams_n(tokens: &[String], window: usize, n: usize) -> Vec<(Vec<String>, u64)> {
let analyzer = NgramAnalyzer::new(window);
let counts = analyzer.compute(tokens);
let mut pairs: Vec<(Vec<String>, u64)> = counts.into_iter().collect();
pairs.sort_by(|a, b| b.1.cmp(&a.1));
pairs.truncate(n);
pairs
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RepetitionSpan {
pub pattern: Vec<String>,
pub positions: Vec<usize>,
pub count: usize,
}
pub struct RepetitionDetector;
impl RepetitionDetector {
pub fn find(tokens: &[String], min_len: usize) -> Vec<RepetitionSpan> {
let min_len = min_len.max(1);
let mut results: Vec<RepetitionSpan> = Vec::new();
let max_len = tokens.len() / 2;
if max_len < min_len {
return results;
}
for window_size in min_len..=max_len {
let mut map: HashMap<Vec<String>, Vec<usize>> = HashMap::new();
for (i, window) in tokens.windows(window_size).enumerate() {
map.entry(window.to_vec()).or_default().push(i);
}
for (pattern, positions) in map {
if positions.len() >= 2 {
let count = positions.len();
results.push(RepetitionSpan { pattern, positions, count });
}
}
}
results.sort_by(|a, b| b.pattern.len().cmp(&a.pattern.len()).then_with(|| b.count.cmp(&a.count)));
results
}
}
pub struct TokenStats {
pub sequence: SequenceStats,
pub frequency: FrequencyMap,
}
impl TokenStats {
pub fn compute(tokens: &[String]) -> Self {
Self {
sequence: SequenceStats::compute(tokens),
frequency: FrequencyMap::from_tokens(tokens),
}
}
pub fn print_summary(&self) {
println!("=== Token Statistics ===");
println!(" length : {}", self.sequence.length);
println!(" unique tokens : {}", self.sequence.unique_tokens);
println!(" vocab richness : {:.4}", self.sequence.vocab_richness);
println!(" avg token len : {:.2}", self.sequence.avg_token_len);
println!(" type-token ratio: {:.4}", self.sequence.type_token_ratio);
println!(" hapax legomena : {}", self.sequence.hapax_legomena);
println!(" entropy (bits) : {:.4}", self.frequency.entropy());
println!(" top-5 tokens :");
for (tok, cnt) in self.frequency.top_n(5) {
println!(" {:20} {}", tok, cnt);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn toks(s: &str) -> Vec<String> {
s.split_whitespace().map(|t| t.to_string()).collect()
}
#[test]
fn test_freq_empty() {
let f = FrequencyMap::from_tokens(&[]);
assert_eq!(f.total, 0);
assert!(f.counts.is_empty());
}
#[test]
fn test_freq_counts() {
let tokens = toks("a b a c a b");
let f = FrequencyMap::from_tokens(&tokens);
assert_eq!(f.counts["a"], 3);
assert_eq!(f.counts["b"], 2);
assert_eq!(f.counts["c"], 1);
assert_eq!(f.total, 6);
}
#[test]
fn test_freq_top_n() {
let tokens = toks("a b a c a b");
let f = FrequencyMap::from_tokens(&tokens);
let top = f.top_n(2);
assert_eq!(top[0], ("a".to_string(), 3));
assert_eq!(top[1], ("b".to_string(), 2));
}
#[test]
fn test_freq_top_n_more_than_vocab() {
let tokens = toks("x y");
let f = FrequencyMap::from_tokens(&tokens);
assert_eq!(f.top_n(10).len(), 2);
}
#[test]
fn test_entropy_uniform() {
let tokens = toks("a b c d");
let f = FrequencyMap::from_tokens(&tokens);
let e = f.entropy();
assert!((e - 2.0).abs() < 1e-9, "entropy={}", e);
}
#[test]
fn test_entropy_single_token() {
let tokens: Vec<String> = vec!["a".to_string(); 10];
let f = FrequencyMap::from_tokens(&tokens);
assert_eq!(f.entropy(), 0.0);
}
#[test]
fn test_entropy_empty() {
let f = FrequencyMap::from_tokens(&[]);
assert_eq!(f.entropy(), 0.0);
}
#[test]
fn test_seq_stats_empty() {
let s = SequenceStats::compute(&[]);
assert_eq!(s.length, 0);
assert_eq!(s.unique_tokens, 0);
}
#[test]
fn test_seq_stats_basic() {
let tokens = toks("the quick brown fox the fox");
let s = SequenceStats::compute(&tokens);
assert_eq!(s.length, 6);
assert_eq!(s.unique_tokens, 4);
assert!((s.vocab_richness - 4.0 / 6.0).abs() < 1e-9);
assert_eq!(s.hapax_legomena, 2); }
#[test]
fn test_seq_avg_token_len() {
let tokens = toks("ab cd");
let s = SequenceStats::compute(&tokens);
assert!((s.avg_token_len - 2.0).abs() < 1e-9);
}
#[test]
fn test_type_token_ratio() {
let tokens = toks("a a a a");
let s = SequenceStats::compute(&tokens);
assert!((s.type_token_ratio - 0.25).abs() < 1e-9);
}
#[test]
fn test_ngram_bigrams() {
let tokens = toks("a b c a b");
let analyzer = NgramAnalyzer::new(2);
let counts = analyzer.compute(&tokens);
assert_eq!(counts[&vec!["a".to_string(), "b".to_string()]], 2);
assert_eq!(counts[&vec!["b".to_string(), "c".to_string()]], 1);
}
#[test]
fn test_ngram_trigrams() {
let tokens = toks("a b c a b c");
let analyzer = NgramAnalyzer::new(3);
let counts = analyzer.compute(&tokens);
assert_eq!(
counts[&vec!["a".to_string(), "b".to_string(), "c".to_string()]],
2
);
}
#[test]
fn test_ngram_too_short() {
let tokens = toks("a b");
let analyzer = NgramAnalyzer::new(5);
assert!(analyzer.compute(&tokens).is_empty());
}
#[test]
fn test_top_ngrams() {
let tokens = toks("a b a b c d");
let top = NgramAnalyzer::top_ngrams(&tokens, 1);
assert_eq!(top.len(), 1);
assert_eq!(top[0].1, 2);
}
#[test]
fn test_repetition_basic() {
let tokens = toks("a b c a b c");
let spans = RepetitionDetector::find(&tokens, 3);
let found = spans.iter().any(|s| {
s.pattern == vec!["a".to_string(), "b".to_string(), "c".to_string()]
&& s.count >= 2
});
assert!(found, "expected 'a b c' repetition");
}
#[test]
fn test_repetition_no_repeat() {
let tokens = toks("a b c d e f");
let spans = RepetitionDetector::find(&tokens, 3);
assert!(spans.is_empty());
}
#[test]
fn test_repetition_min_len() {
let tokens = toks("x y x y x y");
let spans = RepetitionDetector::find(&tokens, 2);
let found = spans.iter().any(|s| {
s.pattern == vec!["x".to_string(), "y".to_string()] && s.count >= 2
});
assert!(found);
}
#[test]
fn test_repetition_count() {
let tokens = toks("a b a b a b");
let spans = RepetitionDetector::find(&tokens, 2);
let pair_span = spans
.iter()
.find(|s| s.pattern == vec!["a".to_string(), "b".to_string()]);
assert!(pair_span.is_some());
assert!(pair_span.unwrap().count >= 2);
}
#[test]
fn test_repetition_empty() {
assert!(RepetitionDetector::find(&[], 3).is_empty());
}
#[test]
fn test_token_stats_smoke() {
let tokens = toks("hello world hello rust world");
let stats = TokenStats::compute(&tokens);
assert_eq!(stats.sequence.length, 5);
assert!(stats.frequency.entropy() > 0.0);
}
}