carykh_macro_rust/
lib.rs

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}