carykh-macro-rust 0.1.2

A rust macro for finding strings that contain self-referential numbers. Inspired by carykh. This description contains twenty-seven words, fifty-four vowels, and ninety-nine consonants.
Documentation
pub use english_numbers::{convert, Formatting};
use std::collections::HashMap;

fn binomial_coefficient(n: usize, k: usize) -> usize {
    if k > n {
        return 0;
    }
    let k = k.min(n - k);
    let mut result = 1;
    for i in 0..k {
        result = result * (n - i) / (i + 1);
    }
    result
}

fn find_combination<const N: usize>(total: usize, index: usize) -> Option<[usize; N]> {
    if N == 1 {
        return Some([total; N]);
    }

    let mut current_index = index;
    let mut result = [0; N];
    let mut remaining_total = total;

    for i in 0..N {
        let remaining_terms = N - i;

        if remaining_terms == 1 {
            result[i] = remaining_total;
            return Some(result);
        }

        let mut binomial_sum = 0;
        for x in 0..=remaining_total {
            let count = binomial_coefficient(
                remaining_total - x + remaining_terms - 2,
                remaining_terms - 2,
            );
            binomial_sum += count;
            if current_index < binomial_sum {
                result[i] = x;
                remaining_total -= x;
                current_index -= binomial_sum - count;
                break;
            }
        }
    }

    Some(result)
}

fn smallest_vector_index<const N: usize>(index: usize) -> [usize; N] {
    if index == 0 {
        return [0; N];
    }

    let mut index = index - 1;
    let mut total_index = 1;
    let mut total = binomial_coefficient(total_index + N - 1, N - 1);

    while index >= total {
        index -= total;
        total_index += 1;
        total = binomial_coefficient(total_index + N - 1, N - 1);
    }

    find_combination::<N>(total_index, index).unwrap()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptimizationError {
    NoSolution,
}

pub fn optimize<const N: usize>(
    string_generator: impl Fn([i64; N]) -> String,
    number_generators: [impl Fn(&str) -> i64; N],
    condition: impl Fn(&str) -> bool,
    check_n: usize,
) -> Result<[i64; N], OptimizationError> {
    let mut checked: HashMap<[i64; N], String> = HashMap::new();

    let mut current_start_index = 0;
    let mut current: [i64; N] = [1; N];

    loop {
        let string = string_generator(current);
        let mut numbers = [0; N];

        for i in 0..N {
            numbers[i] = number_generators[i](&string);
        }

        checked.insert(current, string.clone());

        if numbers == current && condition(&string) {
            return Ok(numbers);
        }

        if checked.contains_key(&numbers) {
            if current_start_index >= check_n {
                return Err(OptimizationError::NoSolution);
            }

            current = smallest_vector_index::<N>(current_start_index).map(|x| x as i64 + 1);
            current_start_index += 1;
        } else {
            current = numbers;
        }
    }
}

pub fn count_letters(string: &str) -> i64 {
    string.chars().filter(|c| c.is_alphabetic()).count() as i64
}

pub fn count_words(string: &str) -> i64 {
    string
        .chars()
        .map(|c| if c.is_alphabetic() { c } else { ' ' })
        .collect::<String>()
        .split_whitespace()
        .count() as i64
}

pub fn count_vowels(string: &str) -> i64 {
    string
        .to_lowercase()
        .chars()
        .filter(|c| "aeiouy".contains(*c))
        .count() as i64
}

pub fn count_consonants(string: &str) -> i64 {
    string
        .to_lowercase()
        .chars()
        .filter(|c| "bcdfghjklmnpqrstvwxz".contains(*c))
        .count() as i64
}

pub fn count_vowels_no_y(string: &str) -> i64 {
    string
        .to_lowercase()
        .chars()
        .filter(|c| "aeiou".contains(*c))
        .count() as i64
}

pub fn count_consonants_with_y(string: &str) -> i64 {
    string
        .to_lowercase()
        .chars()
        .filter(|c| "bcdfghjklmnpqrstvwxyz".contains(*c))
        .count() as i64
}

pub fn count_words_of_length_n(n: i64) -> impl Fn(&str) -> i64 {
    move |string: &str| {
        string
            .chars()
            .map(|c| if c.is_alphabetic() { c } else { ' ' })
            .collect::<String>()
            .split_whitespace()
            .filter(|word| word.chars().count() as i64 == n)
            .count() as i64
    }
}

pub fn no_condition(_: &str) -> bool {
    true
}

pub fn has_n_letters(n: i64) -> impl Fn(&str) -> bool {
    move |string: &str| count_letters(string) == n
}

#[macro_export]
macro_rules! carykh_optimize {
    ($fmt:expr, $($fn:expr),+ $(,)?; { formatting = $formatting:expr, condition = $condition:expr }) => {{
        let string_generator = move |numbers: [i64; carykh_optimize!(@count $($fn),*)]| {
            let mut iter = numbers.iter();
            format!(
                $fmt,
                $(
                    {
                        let _ = $fn;
                        $crate::convert(*iter.next().expect("Insufficient number of arguments"), $formatting)
                    },
                )*
            )
        };

        let number_generators = [$($fn),+];

        let result = $crate::optimize(string_generator, number_generators, $condition, 100000);

        result.map(|x| string_generator(x))
    }};

    ($fmt:expr, $($fn:expr),+ $(,)?; { formatting = $formatting:expr }) => {{
        carykh_optimize!($fmt, $($fn),+; { formatting = $formatting, condition = $crate::no_condition })
    }};

    ($fmt:expr, $($fn:expr),+ $(,)?; { condition = $condition:expr }) => {{
        let default_formatting = $crate::Formatting {
            title_case: false,
            spaces: true,
            conjunctions: true,
            commas: true,
            dashes: true,
        };
        carykh_optimize!($fmt, $($fn),+; { formatting = default_formatting, condition = $condition })
    }};

    ($fmt:expr, $($fn:expr),+ $(,)?) => {{
        let default_formatting = $crate::Formatting {
            title_case: false,
            spaces: true,
            conjunctions: true,
            commas: true,
            dashes: true,
        };
        carykh_optimize!($fmt, $($fn),+; { formatting = default_formatting, condition = $crate::no_condition })
    }};

    (@count $($rest:expr),*) => {
        <[()]>::len(&[$(carykh_optimize!(@substitute $rest)),*])
    };

    (@substitute $_:expr) => { () };
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_carykh_optimize() {
        assert_eq!(
            carykh_optimize!("This sentence has {} letters.", count_letters),
            Ok("This sentence has thirty-one letters.".to_string())
        );

        assert_eq!(
            carykh_optimize!(
                "This sentence has {} vowels and {} consonants. Hooray!",
                count_vowels,
                count_consonants
            ),
            Ok(
                "This sentence has twenty-two vowels and thirty-eight consonants. Hooray!"
                    .to_string()
            )
        );

        assert_eq!(
            carykh_optimize!(
                "Number of vowels and consonants in this mesmerizing pie chart:\nVowels: {} percent\nConsonants: {} percent",
                count_vowels,
                count_consonants;
                {
                    formatting = english_numbers::Formatting::all(),
                    condition = has_n_letters(100)
                }
            ),
            Ok("Number of vowels and consonants in this mesmerizing pie chart:\nVowels: Thirty-Four percent\nConsonants: Sixty-Six percent".to_string())
        );

        assert_eq!(
            carykh_optimize!(
                "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",
                count_words_of_length_n(2),
                count_words_of_length_n(3),
                count_words_of_length_n(4),
                count_words_of_length_n(5),
                count_words_of_length_n(6)
            ),
            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())
        );

        println!(
            "{}",
            carykh_optimize!(
                "A rust macro for finding strings that contain self-referential numbers. Inspired by carykh. This description contains {} words, {} vowels, and {} consonants.",
                count_words,
                count_vowels,
                count_consonants
            )
            .unwrap()
        );
    }
}