Skip to main content

sim_lib_discrete_comb/
cookbook.rs

1//! Deterministic cookbook builders for discrete combinatorics recipes.
2
3use crate::{
4    CombError, canonical_cycles, combination_rank, combination_unrank, longest_only,
5    permutation_rank, permutation_unrank, word_rank, word_to_digits, words,
6};
7
8/// Report produced by the rankable values cookbook recipe.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct RankableValuesDemo {
11    /// Combination value used by the recipe.
12    pub combination: Vec<usize>,
13    /// Combination rank rendered as a decimal ordinal.
14    pub combination_rank: String,
15    /// Combination recovered from the ordinal.
16    pub combination_unranked: Vec<usize>,
17    /// Permutation value used by the recipe.
18    pub permutation: Vec<usize>,
19    /// Permutation rank rendered as a decimal ordinal.
20    pub permutation_rank: String,
21    /// Permutation recovered from the ordinal.
22    pub permutation_unranked: Vec<usize>,
23}
24
25/// Report produced by the finite enumeration cookbook recipe.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct FiniteEnumerationDemo {
28    /// Alphabet supplied by the caller.
29    pub alphabet: Vec<String>,
30    /// Alphabet after applying the supplied order.
31    pub ordered_alphabet: Vec<String>,
32    /// Fixed word length supplied by the caller.
33    pub length: usize,
34    /// Number of words requested by the caller.
35    pub limit: usize,
36    /// Exact finite catalog size rendered as a decimal integer.
37    pub total_words: String,
38    /// First words emitted by the lazy iterator.
39    pub first_words: Vec<Vec<String>>,
40    /// Mixed-radix digits for each emitted word.
41    pub first_digits: Vec<Vec<u64>>,
42    /// Mixed-radix ranks for each emitted word.
43    pub first_ranks: Vec<String>,
44    /// Unique canonical rotations for one non-constant emitted word.
45    pub canonical_cycles: Vec<Vec<String>>,
46    /// Longest emitted words under the word-length selector.
47    pub longest_words: Vec<Vec<String>>,
48}
49
50/// Build the modeled combination/permutation rank report used by the cookbook.
51pub fn rankable_values_demo() -> Result<RankableValuesDemo, CombError> {
52    let combination = vec![0, 2, 4];
53    let combination_rank = combination_rank(&combination, 5)?;
54    let combination_unranked = combination_unrank(&combination_rank, 5, 3)?;
55
56    let permutation = vec![2, 0, 1];
57    let permutation_rank = permutation_rank(&permutation)?;
58    let permutation_unranked = permutation_unrank(&permutation_rank, 3)?;
59
60    Ok(RankableValuesDemo {
61        combination,
62        combination_rank: combination_rank.to_string(),
63        combination_unranked,
64        permutation,
65        permutation_rank: permutation_rank.to_string(),
66        permutation_unranked,
67    })
68}
69
70/// Build the fixed-alphabet finite enumeration report used by the cookbook.
71pub fn finite_enumeration_demo(
72    alphabet: Vec<String>,
73    length: usize,
74    order: Vec<usize>,
75    limit: usize,
76) -> Result<FiniteEnumerationDemo, CombError> {
77    let ordered_alphabet = ordered_alphabet(&alphabet, &order)?;
78    let iterator = words(&ordered_alphabet, length);
79    let total_words = iterator.total_ordinals().to_string();
80    let first_words = iterator.take(limit).collect::<Vec<_>>();
81    let first_digits = first_words
82        .iter()
83        .map(|word| word_to_digits(&ordered_alphabet, word))
84        .collect::<Result<Vec<_>, _>>()?;
85    let first_ranks = first_words
86        .iter()
87        .map(|word| word_rank(&ordered_alphabet, word).map(|rank| rank.to_string()))
88        .collect::<Result<Vec<_>, _>>()?;
89    let cycle_source = first_words
90        .iter()
91        .find(|word| word.windows(2).any(|window| window[0] != window[1]))
92        .or_else(|| first_words.first());
93    let canonical_cycles = cycle_source
94        .map(|word| canonical_cycles(word))
95        .unwrap_or_else(Vec::new);
96    let longest_words = longest_only(first_words.clone(), |word| word.len());
97
98    Ok(FiniteEnumerationDemo {
99        alphabet,
100        ordered_alphabet,
101        length,
102        limit,
103        total_words,
104        first_words,
105        first_digits,
106        first_ranks,
107        canonical_cycles,
108        longest_words,
109    })
110}
111
112fn ordered_alphabet(alphabet: &[String], order: &[usize]) -> Result<Vec<String>, CombError> {
113    if order.len() != alphabet.len() {
114        return Err(CombError::InvalidParameters(
115            "order length must match alphabet length".to_string(),
116        ));
117    }
118    let mut seen = vec![false; alphabet.len()];
119    let mut ordered = Vec::with_capacity(alphabet.len());
120    for &index in order {
121        if index >= alphabet.len() {
122            return Err(CombError::OutOfRange {
123                value: index.to_string(),
124                bound: alphabet.len().to_string(),
125            });
126        }
127        if seen[index] {
128            return Err(CombError::InvalidParameters(
129                "order must be a permutation".to_string(),
130            ));
131        }
132        seen[index] = true;
133        ordered.push(alphabet[index].clone());
134    }
135    Ok(ordered)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn rankable_values_round_trip_ordinals() {
144        let demo = rankable_values_demo().expect("valid rankable values demo");
145
146        assert_eq!(demo.combination_unranked, demo.combination);
147        assert_eq!(demo.permutation_unranked, demo.permutation);
148        assert_eq!(demo.combination_rank, "4");
149        assert_eq!(demo.permutation_rank, "4");
150    }
151
152    #[test]
153    fn finite_enumeration_demo_uses_supplied_data() {
154        let demo = finite_enumeration_demo(
155            vec!["C".to_string(), "D".to_string(), "E".to_string()],
156            2,
157            vec![2, 0, 1],
158            5,
159        )
160        .expect("valid finite enumeration demo");
161
162        assert_eq!(demo.ordered_alphabet, vec!["E", "C", "D"]);
163        assert_eq!(demo.total_words, "9");
164        assert_eq!(
165            demo.first_words,
166            vec![
167                vec!["E", "E"],
168                vec!["E", "C"],
169                vec!["E", "D"],
170                vec!["C", "E"],
171                vec!["C", "C"],
172            ]
173        );
174        assert_eq!(
175            demo.first_digits,
176            vec![vec![0, 0], vec![0, 1], vec![0, 2], vec![1, 0], vec![1, 1]]
177        );
178        assert_eq!(demo.first_ranks, vec!["0", "1", "2", "3", "4"]);
179        assert_eq!(
180            demo.canonical_cycles,
181            vec![
182                vec!["C".to_string(), "E".to_string()],
183                vec!["E".to_string(), "C".to_string()]
184            ]
185        );
186        assert_eq!(demo.longest_words, demo.first_words);
187    }
188}