improved API intuitiveness and encapsulation

This commit is contained in:
2026-05-13 19:06:28 -04:00
parent 0f5c6610e6
commit 6f41034b3d
+99 -92
View File
@@ -10,6 +10,72 @@
// 3.d. count the average word length // 3.d. count the average word length
// 4. (pretty) print the info // 4. (pretty) print the info
// newtype syntax
// compiler will help enforce the type and prevent internal state manipulation
// makes the API a little more intuitive
pub struct FrequencyVec(Vec<(String, usize)>);
impl FrequencyVec {
pub fn top_n(&self, n: usize) -> &[(String, usize)] {
let n = n.min(self.0.len());
&self.0[..n]
}
pub fn as_slice(&self) -> &[(String, usize)] {
&self.0[..]
}
pub fn analyze_word_count(&self) -> usize {
self.0.iter().map(|(_,c)| c).sum()
}
pub fn analyze_unique_word_count(&self) -> usize {
self.0.len()
}
pub fn average_word_length(&self) -> f64 {
if self.0.is_empty() {
return 0.0;
}
let mut len = 0;
let mut tokens = 0;
self.0.iter().for_each(|(s,c)| {
// this will work for Unicode Scalar value characters
// ie: graphemes with multiple code points (like some emojis) will break
// but not going to pull in unicode-segmentation just to get this right
len += s.chars().count() * *c;
tokens += *c;
});
len as f64 /(tokens as f64)
}
// todo: option
pub fn longest_word(&self) -> usize {
self.0.iter().map(|(w,_)| w.len()).max().unwrap_or(0)
}
pub fn print_analysis(&self) {
if self.0.is_empty() {
println!("No words to analyze");
return;
}
let wc = self.analyze_word_count();
let largest_freq = self.top_n(1)[0].1;
let digits = num_digits(largest_freq);
let col1 = self.longest_word().max(5);
let col2 = digits.max(5);
let col3 = 10;
let end = col1 + col2 + col3.max(11) + 2;
println!("{:<col1$} {:<col2$} {:<col3$}", "Word", "Count", "%_of_total");
println!("{:-<end$}", "");
for (w, c) in self.0.iter() {
let pct = format!("{:.2}%", *c as f64 / wc as f64 * 100.0);
println!("{:<col1$} {:<col2$} {:>col3$}", w, c, pct);
}
}
}
// todo: return Result for whitespace input // todo: return Result for whitespace input
// CTRL chars are preserved in tokens, though whitespace ones will get dropped // CTRL chars are preserved in tokens, though whitespace ones will get dropped
@@ -37,7 +103,7 @@ pub fn tokenize(input: &str) -> Vec<String> {
// Since our tokenize function returns new Strings, there is not much sense in // Since our tokenize function returns new Strings, there is not much sense in
// taking &[&str] here since this would require an extra allocation to move the // taking &[&str] here since this would require an extra allocation to move the
// string slices to a new vector. This doesn't copy heap on the way in anyway. // string slices to a new vector. This doesn't copy heap on the way in anyway.
pub fn create_frequency_vec(input: &[String]) -> Vec<(String,usize)> { pub fn create_frequency_vec(input: &[String]) -> FrequencyVec {
// perhaps unnecessary copy, but this preserves the original token list // perhaps unnecessary copy, but this preserves the original token list
let mut sorted = input.to_vec(); let mut sorted = input.to_vec();
sorted.sort(); // O(n log n) sorted.sort(); // O(n log n)
@@ -46,94 +112,35 @@ pub fn create_frequency_vec(input: &[String]) -> Vec<(String,usize)> {
for w in sorted { // O(n) for w in sorted { // O(n)
match counts.last_mut() { match counts.last_mut() {
Some((last, count)) if last == &w => *count += 1, Some((last, count)) if last == &w => *count += 1,
_ => counts.push((w.clone(), 1)), _ => counts.push((w, 1)),
} }
} }
counts.sort_by(|a,b| b.1.cmp(&a.1)); // sort by frequency, O(m log m) counts.sort_by(|a,b| b.1.cmp(&a.1)); // sort by frequency, O(m log m)
counts FrequencyVec(counts)
} }
pub fn analyze_word_count(input: &[(String,usize)]) -> usize { fn num_digits(n: usize) -> usize {
input.iter().map(|(_,c)| c).sum() if n == 0 { return 1; }
}
pub fn analyze_unique_word_count(input: &[(String,usize)]) -> usize {
input.len()
}
pub fn average_word_length(input: &[(String, usize)]) -> f64 {
if input.is_empty() {
return 0.0;
}
let mut len = 0;
let mut tokens = 0;
input.iter().for_each(|(s,c)| {
// this will work for Unicode Scalar value characters
// ie: graphemes with multiple code points (like some emojis) will break
// but not going to pull in unicode-segmentation just to get this right
len += s.chars().count() * c;
tokens += c;
});
len as f64 /(tokens as f64)
}
pub fn top_n_most_used(input: &[(String, usize)], n: usize) -> &[(String, usize)] {
let n = n.min(input.len());
&input[..n]
}
// todo: option
pub fn longest_word(input: &[(String, usize)]) -> usize {
input.iter().map(|(w,_)| w.len()).max().unwrap_or(0)
}
pub fn num_digits(n: usize) -> usize {
let mut n = n; let mut n = n;
let mut count = 0; let mut count = 0;
while n > 0 { while n > 0 { count += 1; n /= 10; }
count += 1;
n /= 10;
}
count count
} }
pub fn print_analysis(input: &[(String, usize)]) {
if input.is_empty() {
println!("No words to analyze");
return;
}
let counts = input;
let wc = analyze_word_count(&counts);
let largest_freq = top_n_most_used(&counts, 1)[0].1;
let digits = num_digits(largest_freq);
let col1 = longest_word(&counts).max(5);
let col2 = digits.max(5);
let col3 = 5;
let end = col1 + col2 + col3.max(11) + 2;
println!("{:<col1$} {:<col2$} {:<col3$}", "Word", "Count", "%_of_total");
println!("{:-<end$}", "");
for (w, c) in counts {
println!("{:<col1$} {:<col2$} {:<col3$.2}%", w, c, (*c as f64/(wc as f64) * 100.0));
}
}
fn main() { fn main() {
println!("playing around with vecs and slices"); println!("playing around with vecs and slices");
println!("use `cargo test`]\n"); println!("use `cargo test`]\n");
test_print(); test_print();
} }
pub fn test_print() { fn test_print() {
let tokens = tokenize("Hellow, wod! hello hello hello ok bye then oa more plz"); let tokens = tokenize("Hellow, wod! hello hello hello ok bye then oa more plz");
let freq_vec = create_frequency_vec(&tokens); let freq_vec = create_frequency_vec(&tokens);
print_analysis(&freq_vec); freq_vec.print_analysis();
} }
#[cfg(test)] #[cfg(test)]
pub mod test { mod test {
use super::*; use super::*;
#[test] #[test]
@@ -142,21 +149,21 @@ pub mod test {
assert_eq!(tokens.len(), 0); assert_eq!(tokens.len(), 0);
let freq_vec = create_frequency_vec(&tokens); let freq_vec = create_frequency_vec(&tokens);
assert_eq!(freq_vec.len(), 0); assert_eq!(freq_vec.as_slice().len(), 0);
let wc = analyze_word_count(&freq_vec); let wc = freq_vec.analyze_word_count();
assert_eq!(wc, 0); assert_eq!(wc, 0);
let uwc = analyze_unique_word_count(&freq_vec); let uwc = freq_vec.analyze_unique_word_count();
assert_eq!(uwc, 0); assert_eq!(uwc, 0);
let avg_len = average_word_length(&freq_vec); let avg_len = freq_vec.average_word_length();
assert_eq!(avg_len, 0.0); assert_eq!(avg_len, 0.0);
let top_n = top_n_most_used(&freq_vec, 4); let top_n = freq_vec.top_n(4);
assert_eq!(top_n.len(), 0); assert_eq!(top_n.len(), 0);
print_analysis(&freq_vec); freq_vec.print_analysis();
} }
#[test] #[test]
@@ -165,21 +172,21 @@ pub mod test {
assert_eq!(tokens.len(), 3); assert_eq!(tokens.len(), 3);
let freq_vec = create_frequency_vec(&tokens); let freq_vec = create_frequency_vec(&tokens);
assert_eq!(freq_vec.len(), 2); assert_eq!(freq_vec.as_slice().len(), 2);
let wc = analyze_word_count(&freq_vec); let wc = freq_vec.analyze_word_count();
assert_eq!(wc, 3); assert_eq!(wc, 3);
let uwc = analyze_unique_word_count(&freq_vec); let uwc = freq_vec.analyze_unique_word_count();
assert_eq!(uwc, 2); assert_eq!(uwc, 2);
let avg_len = average_word_length(&freq_vec); let avg_len = freq_vec.average_word_length();
assert_eq!(avg_len, 5.0); assert_eq!(avg_len, 5.0);
let top_n = top_n_most_used(&freq_vec, 4); let top_n = freq_vec.top_n(4);
assert_eq!(top_n[0].0, "hello"); assert_eq!(top_n[0].0, "hello");
print_analysis(&freq_vec); freq_vec.print_analysis();
} }
#[test] #[test]
@@ -188,21 +195,21 @@ pub mod test {
assert_eq!(tokens.len(), 4); assert_eq!(tokens.len(), 4);
let freq_vec = create_frequency_vec(&tokens); let freq_vec = create_frequency_vec(&tokens);
assert_eq!(freq_vec.len(), 4); assert_eq!(freq_vec.as_slice().len(), 4);
let wc = analyze_word_count(&freq_vec); let wc = freq_vec.analyze_word_count();
assert_eq!(wc, 4); assert_eq!(wc, 4);
let uwc = analyze_unique_word_count(&freq_vec); let uwc = freq_vec.analyze_unique_word_count();
assert_eq!(uwc, 4); assert_eq!(uwc, 4);
let avg_len = average_word_length(&freq_vec); let avg_len = freq_vec.average_word_length();
assert_eq!(avg_len, 4.25); assert_eq!(avg_len, 4.25);
let top_n = top_n_most_used(&freq_vec, 4); let top_n = freq_vec.top_n(4);
assert_eq!(top_n[0].0, "hello"); assert_eq!(top_n[0].0, "hello");
print_analysis(&freq_vec); freq_vec.print_analysis();
} }
#[test] #[test]
@@ -211,20 +218,20 @@ pub mod test {
assert_eq!(tokens.len(), 1); assert_eq!(tokens.len(), 1);
let freq_vec = create_frequency_vec(&tokens); let freq_vec = create_frequency_vec(&tokens);
assert_eq!(freq_vec.len(), 1); assert_eq!(freq_vec.as_slice().len(), 1);
let wc = analyze_word_count(&freq_vec); let wc = freq_vec.analyze_word_count();
assert_eq!(wc, 1); assert_eq!(wc, 1);
let uwc = analyze_unique_word_count(&freq_vec); let uwc = freq_vec.analyze_unique_word_count();
assert_eq!(uwc, 1); assert_eq!(uwc, 1);
let avg_len = average_word_length(&freq_vec); let avg_len = freq_vec.average_word_length();
assert_eq!(avg_len, 4.0); assert_eq!(avg_len, 4.0);
let top_n = top_n_most_used(&freq_vec, 4); let top_n = freq_vec.top_n(4);
assert_eq!(top_n[0].0, "café"); assert_eq!(top_n[0].0, "café");
print_analysis(&freq_vec); freq_vec.print_analysis();
} }
} }