1use anyhow::Result;
2use clap::Subcommand;
3use std::collections::HashMap;
4
5#[derive(Subcommand)]
6pub enum FrequencyAction {
7 #[command(about = "Analyze character frequency")]
8 Analyze {
9 #[arg(help = "Input text")]
10 input: String,
11 #[arg(short, long, help = "Show only alphabetic characters")]
12 alpha_only: bool,
13 },
14 #[command(about = "Chi-squared test against English letter frequencies")]
15 ChiSquared {
16 #[arg(help = "Input text")]
17 input: String,
18 },
19 #[command(about = "Calculate Index of Coincidence (IoC)")]
20 Ioc {
21 #[arg(help = "Input text")]
22 input: String,
23 },
24}
25
26pub fn run(action: FrequencyAction) -> Result<()> {
27 match action {
28 FrequencyAction::Analyze { input, alpha_only } => {
29 let result = analyze(&input, alpha_only);
30 print_analysis(&result);
31 }
32 FrequencyAction::ChiSquared { input } => {
33 let score = chi_squared(&input);
34 println!("Chi-squared score: {:.4}", score);
35 println!("(Lower score = closer to English letter distribution)");
36 if score < 50.0 {
37 println!("Likely English text");
38 } else if score < 100.0 {
39 println!("Possibly English text");
40 } else {
41 println!("Unlikely to be English text");
42 }
43 }
44 FrequencyAction::Ioc { input } => {
45 let ioc = index_of_coincidence(&input);
46 println!("Index of Coincidence: {:.6}", ioc);
47 println!("English text ~0.0667, random text ~0.0385");
48 if (ioc - 0.0667).abs() < 0.01 {
49 println!("Consistent with monoalphabetic cipher or English text");
50 } else if (ioc - 0.0385).abs() < 0.01 {
51 println!("Consistent with polyalphabetic cipher or random text");
52 }
53 }
54 }
55 Ok(())
56}
57
58const ENGLISH_FREQ: &[(char, f64)] = &[
60 ('E', 12.7),
61 ('T', 9.1),
62 ('A', 8.2),
63 ('O', 7.5),
64 ('I', 7.0),
65 ('N', 6.7),
66 ('S', 6.3),
67 ('H', 6.1),
68 ('R', 6.0),
69 ('D', 4.3),
70 ('L', 4.0),
71 ('C', 2.8),
72 ('U', 2.8),
73 ('M', 2.4),
74 ('W', 2.4),
75 ('F', 2.2),
76 ('G', 2.0),
77 ('Y', 2.0),
78 ('P', 1.9),
79 ('B', 1.5),
80 ('V', 1.0),
81 ('K', 0.8),
82 ('J', 0.15),
83 ('X', 0.15),
84 ('Q', 0.10),
85 ('Z', 0.07),
86];
87
88#[derive(Debug)]
89pub struct FrequencyResult {
90 pub frequencies: Vec<(char, usize, f64)>, pub total_chars: usize,
92}
93
94pub fn analyze(input: &str, alpha_only: bool) -> FrequencyResult {
95 let mut frequencies: Vec<(char, usize, f64)> = Vec::new();
96 let mut total = 0usize;
97
98 if alpha_only {
99 let mut counts = [0usize; 26];
101
102 for c in input.chars() {
103 if c.is_ascii_alphabetic() {
104 counts[(c.to_ascii_uppercase() as u8 - b'A') as usize] += 1;
105 total += 1;
106 }
107 }
108
109 for (i, &count) in counts.iter().enumerate() {
110 if count > 0 {
111 let percentage = if total > 0 {
112 (count as f64 / total as f64) * 100.0
113 } else {
114 0.0
115 };
116 frequencies.push(((b'A' + i as u8) as char, count, percentage));
117 }
118 }
119 } else {
120 let mut counts: HashMap<char, usize> = HashMap::new();
121 for c in input.chars() {
122 *counts.entry(c).or_insert(0) += 1;
123 total += 1;
124 }
125
126 frequencies = counts
127 .into_iter()
128 .map(|(c, count)| {
129 let percentage = if total > 0 {
130 (count as f64 / total as f64) * 100.0
131 } else {
132 0.0
133 };
134 (c, count, percentage)
135 })
136 .collect();
137 }
138
139 frequencies.sort_by_key(|b| std::cmp::Reverse(b.1));
141
142 FrequencyResult {
143 frequencies,
144 total_chars: total,
145 }
146}
147
148pub fn chi_squared(input: &str) -> f64 {
151 let mut counts = [0u32; 26];
152 let mut total = 0u32;
153
154 for c in input.chars() {
155 if c.is_ascii_alphabetic() {
156 counts[(c.to_ascii_uppercase() as u8 - b'A') as usize] += 1;
157 total += 1;
158 }
159 }
160
161 if total == 0 {
162 return f64::INFINITY;
163 }
164
165 let total_f = total as f64;
166 ENGLISH_FREQ
167 .iter()
168 .map(|&(ch, expected_pct)| {
169 let observed = counts[(ch as u8 - b'A') as usize] as f64;
170 let expected = expected_pct / 100.0 * total_f;
171 if expected > 0.0 {
172 (observed - expected).powi(2) / expected
173 } else {
174 0.0
175 }
176 })
177 .sum()
178}
179
180pub fn index_of_coincidence(input: &str) -> f64 {
183 let mut counts = [0u64; 26];
184 let mut total = 0u64;
185
186 for c in input.chars() {
187 if c.is_ascii_alphabetic() {
188 counts[(c.to_ascii_uppercase() as u8 - b'A') as usize] += 1;
189 total += 1;
190 }
191 }
192
193 if total <= 1 {
194 return 0.0;
195 }
196
197 let numerator: u64 = counts.iter().map(|&n| n * n.saturating_sub(1)).sum();
198 numerator as f64 / (total * (total - 1)) as f64
199}
200
201fn print_analysis(result: &FrequencyResult) {
202 println!("Character Frequency Analysis");
203 println!("============================");
204 println!("Total characters: {}", result.total_chars);
205 println!();
206
207 println!("{:<6} {:>6} {:>8} English %", "Char", "Count", "Freq %");
208 println!("{}", "-".repeat(40));
209
210 for (c, count, percentage) in &result.frequencies {
211 let english_freq = ENGLISH_FREQ
212 .iter()
213 .find(|(ch, _)| *ch == c.to_ascii_uppercase())
214 .map(|(_, f)| format!("{:.1}%", f))
215 .unwrap_or_default();
216
217 let display_char = if *c == ' ' {
218 "SPACE".to_string()
219 } else if *c == '\n' {
220 "\\n".to_string()
221 } else if *c == '\t' {
222 "\\t".to_string()
223 } else {
224 c.to_string()
225 };
226
227 println!(
228 "{:<6} {:>6} {:>7.1}% {}",
229 display_char, count, percentage, english_freq
230 );
231 }
232}