1pub use english_numbers::{convert, Formatting};
2use std::collections::HashMap;
3
4fn binomial_coefficient(n: usize, k: usize) -> usize {
5 if k > n {
6 return 0;
7 }
8 let k = k.min(n - k);
9 let mut result = 1;
10 for i in 0..k {
11 result = result * (n - i) / (i + 1);
12 }
13 result
14}
15
16fn find_combination<const N: usize>(total: usize, index: usize) -> Option<[usize; N]> {
17 if N == 1 {
18 return Some([total; N]);
19 }
20
21 let mut current_index = index;
22 let mut result = [0; N];
23 let mut remaining_total = total;
24
25 for i in 0..N {
26 let remaining_terms = N - i;
27
28 if remaining_terms == 1 {
29 result[i] = remaining_total;
30 return Some(result);
31 }
32
33 let mut binomial_sum = 0;
34 for x in 0..=remaining_total {
35 let count = binomial_coefficient(
36 remaining_total - x + remaining_terms - 2,
37 remaining_terms - 2,
38 );
39 binomial_sum += count;
40 if current_index < binomial_sum {
41 result[i] = x;
42 remaining_total -= x;
43 current_index -= binomial_sum - count;
44 break;
45 }
46 }
47 }
48
49 Some(result)
50}
51
52fn smallest_vector_index<const N: usize>(index: usize) -> [usize; N] {
53 if index == 0 {
54 return [0; N];
55 }
56
57 let mut index = index - 1;
58 let mut total_index = 1;
59 let mut total = binomial_coefficient(total_index + N - 1, N - 1);
60
61 while index >= total {
62 index -= total;
63 total_index += 1;
64 total = binomial_coefficient(total_index + N - 1, N - 1);
65 }
66
67 find_combination::<N>(total_index, index).unwrap()
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum OptimizationError {
72 NoSolution,
73}
74
75pub fn optimize<const N: usize>(
76 string_generator: impl Fn([i64; N]) -> String,
77 number_generators: [impl Fn(&str) -> i64; N],
78 condition: impl Fn(&str) -> bool,
79 check_n: usize,
80) -> Result<[i64; N], OptimizationError> {
81 let mut checked: HashMap<[i64; N], String> = HashMap::new();
82
83 let mut current_start_index = 0;
84 let mut current: [i64; N] = [1; N];
85
86 loop {
87 let string = string_generator(current);
88 let mut numbers = [0; N];
89
90 for i in 0..N {
91 numbers[i] = number_generators[i](&string);
92 }
93
94 checked.insert(current, string.clone());
95
96 if numbers == current && condition(&string) {
97 return Ok(numbers);
98 }
99
100 if checked.contains_key(&numbers) {
101 if current_start_index >= check_n {
102 return Err(OptimizationError::NoSolution);
103 }
104
105 current = smallest_vector_index::<N>(current_start_index).map(|x| x as i64 + 1);
106 current_start_index += 1;
107 } else {
108 current = numbers;
109 }
110 }
111}
112
113pub fn count_letters(string: &str) -> i64 {
114 string.chars().filter(|c| c.is_alphabetic()).count() as i64
115}
116
117pub fn count_words(string: &str) -> i64 {
118 string
119 .chars()
120 .map(|c| if c.is_alphabetic() { c } else { ' ' })
121 .collect::<String>()
122 .split_whitespace()
123 .count() as i64
124}
125
126pub fn count_vowels(string: &str) -> i64 {
127 string
128 .to_lowercase()
129 .chars()
130 .filter(|c| "aeiouy".contains(*c))
131 .count() as i64
132}
133
134pub fn count_consonants(string: &str) -> i64 {
135 string
136 .to_lowercase()
137 .chars()
138 .filter(|c| "bcdfghjklmnpqrstvwxz".contains(*c))
139 .count() as i64
140}
141
142pub fn count_vowels_no_y(string: &str) -> i64 {
143 string
144 .to_lowercase()
145 .chars()
146 .filter(|c| "aeiou".contains(*c))
147 .count() as i64
148}
149
150pub fn count_consonants_with_y(string: &str) -> i64 {
151 string
152 .to_lowercase()
153 .chars()
154 .filter(|c| "bcdfghjklmnpqrstvwxyz".contains(*c))
155 .count() as i64
156}
157
158pub fn count_words_of_length_n(n: i64) -> impl Fn(&str) -> i64 {
159 move |string: &str| {
160 string
161 .chars()
162 .map(|c| if c.is_alphabetic() { c } else { ' ' })
163 .collect::<String>()
164 .split_whitespace()
165 .filter(|word| word.chars().count() as i64 == n)
166 .count() as i64
167 }
168}
169
170pub fn no_condition(_: &str) -> bool {
171 true
172}
173
174pub fn has_n_letters(n: i64) -> impl Fn(&str) -> bool {
175 move |string: &str| count_letters(string) == n
176}
177
178#[macro_export]
179macro_rules! carykh_optimize {
180 ($fmt:expr, $($fn:expr),+ $(,)?; { formatting = $formatting:expr, condition = $condition:expr }) => {{
181 let string_generator = move |numbers: [i64; carykh_optimize!(@count $($fn),*)]| {
182 let mut iter = numbers.iter();
183 format!(
184 $fmt,
185 $(
186 {
187 let _ = $fn;
188 $crate::convert(*iter.next().expect("Insufficient number of arguments"), $formatting)
189 },
190 )*
191 )
192 };
193
194 let number_generators = [$($fn),+];
195
196 let result = $crate::optimize(string_generator, number_generators, $condition, 100000);
197
198 result.map(|x| string_generator(x))
199 }};
200
201 ($fmt:expr, $($fn:expr),+ $(,)?; { formatting = $formatting:expr }) => {{
202 carykh_optimize!($fmt, $($fn),+; { formatting = $formatting, condition = $crate::no_condition })
203 }};
204
205 ($fmt:expr, $($fn:expr),+ $(,)?; { condition = $condition:expr }) => {{
206 let default_formatting = $crate::Formatting {
207 title_case: false,
208 spaces: true,
209 conjunctions: true,
210 commas: true,
211 dashes: true,
212 };
213 carykh_optimize!($fmt, $($fn),+; { formatting = default_formatting, condition = $condition })
214 }};
215
216 ($fmt:expr, $($fn:expr),+ $(,)?) => {{
217 let default_formatting = $crate::Formatting {
218 title_case: false,
219 spaces: true,
220 conjunctions: true,
221 commas: true,
222 dashes: true,
223 };
224 carykh_optimize!($fmt, $($fn),+; { formatting = default_formatting, condition = $crate::no_condition })
225 }};
226
227 (@count $($rest:expr),*) => {
228 <[()]>::len(&[$(carykh_optimize!(@substitute $rest)),*])
229 };
230
231 (@substitute $_:expr) => { () };
232}
233
234#[cfg(test)]
235mod test {
236 use super::*;
237
238 #[test]
239 fn test_carykh_optimize() {
240 assert_eq!(
241 carykh_optimize!("This sentence has {} letters.", count_letters),
242 Ok("This sentence has thirty-one letters.".to_string())
243 );
244
245 assert_eq!(
246 carykh_optimize!(
247 "This sentence has {} vowels and {} consonants. Hooray!",
248 count_vowels,
249 count_consonants
250 ),
251 Ok(
252 "This sentence has twenty-two vowels and thirty-eight consonants. Hooray!"
253 .to_string()
254 )
255 );
256
257 assert_eq!(
258 carykh_optimize!(
259 "Number of vowels and consonants in this mesmerizing pie chart:\nVowels: {} percent\nConsonants: {} percent",
260 count_vowels,
261 count_consonants;
262 {
263 formatting = english_numbers::Formatting::all(),
264 condition = has_n_letters(100)
265 }
266 ),
267 Ok("Number of vowels and consonants in this mesmerizing pie chart:\nVowels: Thirty-Four percent\nConsonants: Sixty-Six percent".to_string())
268 );
269
270 assert_eq!(
271 carykh_optimize!(
272 "Number of words of each length in this bar graph: ({}) two-letter-words, ({}) three-letter-words, ({}) four-letter-words, ({}) five-letter-words, ({}) six-letter-words",
273 count_words_of_length_n(2),
274 count_words_of_length_n(3),
275 count_words_of_length_n(4),
276 count_words_of_length_n(5),
277 count_words_of_length_n(6)
278 ),
279 Ok("Number of words of each length in this bar graph: (three) two-letter-words, (three) three-letter-words, (five) four-letter-words, (eleven) five-letter-words, (eight) six-letter-words".to_string())
280 );
281
282 println!(
283 "{}",
284 carykh_optimize!(
285 "A rust macro for finding strings that contain self-referential numbers. Inspired by carykh. This description contains {} words, {} vowels, and {} consonants.",
286 count_words,
287 count_vowels,
288 count_consonants
289 )
290 .unwrap()
291 );
292 }
293}