random_picker/
config.rs

1use crate::*;
2use std::{collections::HashMap, fmt::Display, hash::Hash, str::FromStr};
3
4/// Alias of `HashMap`. The weight value type is always `f64`.
5pub type Table<T> = HashMap<T, f64, std::hash::RandomState>;
6
7/// Configuration required by `Picker`. All members are public
8/// and are supposed to be modified by the user.
9#[derive(Clone, PartialEq, Debug)]
10#[cfg_attr(feature = "serde-config", derive(serde::Serialize, serde::Deserialize))]
11pub struct Config<T: Clone + Eq + Hash> {
12    /// Table of choices and weights which are proportional to the probabilities
13    /// on repetitive mode or single-item mode.
14    pub table: Table<T>,
15
16    /// Do multiplicative inversion for each value in the table.
17    pub inversed: bool,
18
19    /// Allow the same item to be picked for multiple times in the result.
20    pub repetitive: bool,
21}
22
23impl<T: Clone + Eq + Hash> Config<T> {
24    /// Returns an invalid configuration with an empty table.
25    /// Please add items into the table before using it to construct `Picker`.
26    ///
27    /// ```
28    /// let conf = random_picker::Config::<String>::new();
29    /// assert!(!conf.inversed && !conf.repetitive);
30    /// assert!(conf.check().is_err());
31    /// ```
32    #[inline]
33    pub fn new() -> Self {
34        Self {
35            table: HashMap::new(),
36            inversed: false,
37            repetitive: false,
38        }
39    }
40
41    /// Checks whether or not the table can be used by `Picker`.
42    ///
43    /// ```
44    /// let mut conf: random_picker::Config<String> = "
45    ///     a = -1; b = 0; c = 2
46    /// ".parse().unwrap();
47    /// assert!(conf.check().is_err());
48    /// conf.table.insert("a".to_string(), 1.);
49    /// assert!(conf.check().is_ok());
50    /// conf.inversed = true;
51    /// assert!(conf.check().is_err());
52    /// conf.table.insert("b".to_string(), 0.1);
53    /// assert!(conf.check().is_ok());
54    /// ```
55    pub fn check(&self) -> Result<(), Error> {
56        let mut non_empty = false;
57        for &v in self.table.values() {
58            if v < 0. || (self.inversed && v == 0.) {
59                return Err(Error::InvalidTable);
60            }
61            if v > 0. {
62                non_empty = true;
63            }
64        }
65        non_empty.then_some(()).ok_or(Error::InvalidTable)
66    }
67
68    /// Returns `true` if all items have equal (and valid) weight values.
69    ///
70    /// ```
71    /// let mut conf: random_picker::Config<String> = "
72    ///     a = -1; b = 1; c = 1.1
73    /// ".parse().unwrap();
74    /// assert!(!conf.is_fair());
75    /// conf.table.insert("a".to_string(), 1.);
76    /// assert!(!conf.is_fair());
77    /// conf.table.insert("c".to_string(), 1.);
78    /// assert!(conf.is_fair());
79    /// ```
80    pub fn is_fair(&self) -> bool {
81        if self.check().is_err() {
82            return false;
83        }
84        let mut v_prev = None;
85        for &v in self.table.values() {
86            if let Some(v_prev) = v_prev {
87                if v != v_prev {
88                    return false;
89                }
90            }
91            v_prev.replace(v);
92        }
93        true
94    }
95
96    #[inline]
97    pub(crate) fn vec_table(&self) -> Result<Vec<(T, f64)>, Error> {
98        self.check()?;
99        let vec = if !self.inversed {
100            self.table
101                .clone()
102                .into_iter()
103                .filter(|&(_, v)| v > 0.)
104                .collect()
105        } else {
106            self.table
107                .iter()
108                .map(|(k, &v)| (k.clone(), 1. / v))
109                .collect()
110        };
111        Ok(vec)
112    }
113}
114
115impl<T: Clone + Eq + Hash> Default for Config<T> {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl Config<String> {
122    /// Appends, modifies or deletes items in the table
123    /// according to the configuration input string.
124    ///
125    /// ```
126    /// let mut conf: random_picker::Config<String> = "
127    /// ## 'repetitive' and 'inversed' are special items
128    /// repetitive = true
129    /// inversed = false
130    /// ## this line can be ignored
131    /// [items]
132    /// oxygen = 47
133    /// silicon = 28
134    /// aluminium=8; iron=5; magnesium=4;
135    /// calcium=2; potassium=2; sodium=2
136    /// others = 2; nonexistium = 31
137    ///    aluminium 7.9; delete nonexistium
138    /// ".parse().unwrap();
139    /// assert_eq!(conf.table.len(), 9);
140    /// assert_eq!(conf.repetitive, true);
141    /// assert_eq!(conf.inversed, false);
142    /// assert_eq!(conf.table.get("aluminium"), Some(&7.9));
143    ///
144    /// conf.append_str("\
145    /// ## power_inversed/repetitive_picking without '=' are for the old format
146    /// power_inversed
147    /// ## invalid: repetitive = 0 (0 is not bool)
148    /// repetitive = 0
149    /// silicon = 28.1
150    /// ");
151    /// assert_eq!(conf.inversed, true);
152    ///
153    /// conf.append_str("inversed = false");
154    /// assert_eq!(conf, random_picker::Config {
155    ///     table: [
156    ///         ("oxygen", 47.), ("silicon", 28.1), ("aluminium", 7.9),
157    ///         ("iron", 5.), ("magnesium", 4.), ("calcium", 2.),
158    ///         ("sodium", 2.), ("potassium", 2.), ("others", 2.),
159    ///     ].iter().map(|&(k, v)| (k.to_string(), v)).collect(),
160    ///     inversed: false,
161    ///     repetitive: true
162    /// });
163    /// ```
164    pub fn append_str(&mut self, str_items: &str) {
165        for line in str_items.split(&['\r', '\n', ';']) {
166            let mut spl = line.split(&[' ', '\t', '=']).filter(|s| !s.is_empty());
167            let item_name;
168            if let Some(s) = spl.next() {
169                if let Some('#') = s.chars().nth(0) {
170                    continue;
171                }
172                item_name = s;
173            } else {
174                continue;
175            }
176
177            // compatible with the old table format
178            if item_name == "power_inversed" {
179                self.inversed = true;
180            } else if item_name == "repetitive_picking" {
181                self.repetitive = true;
182            } else if let Some(s) = spl.last() {
183                if item_name == "delete" {
184                    let _ = self.table.remove(s);
185                } else if item_name == "inversed" {
186                    if let Ok(b) = bool::from_str(s) {
187                        self.inversed = b;
188                    }
189                } else if item_name == "repetitive" {
190                    if let Ok(b) = bool::from_str(s) {
191                        self.repetitive = b;
192                    }
193                } else if let Ok(v) = f64::from_str(s) {
194                    self.table.insert(item_name.to_string(), v);
195                }
196            }
197        }
198    }
199}
200
201impl FromStr for Config<String> {
202    type Err = Error;
203    #[inline(always)]
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        let mut conf = Self::new();
206        conf.append_str(s);
207        Ok(conf)
208    }
209}
210
211impl Display for Config<String> {
212    #[inline(always)]
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        if self.check().is_err() {
215            writeln!(f, "# INVALID!!!")?;
216        }
217        writeln!(f, "[random-picker]")?;
218        writeln!(f, "repetitive = {}", self.repetitive)?;
219        writeln!(f, "inversed = {}\n", self.inversed)?;
220        writeln!(f, "[items]")?;
221        format_table(f, &self.table)?;
222        Ok(())
223    }
224}
225
226/// Prints the weight table to the standard output.
227#[inline(always)]
228pub fn print_table(table: &Table<String>) {
229    let mut s = String::new();
230    let _ = format_table(&mut s, table);
231    print!("{s}");
232}
233
234fn format_table(f: &mut impl std::fmt::Write, table: &Table<String>) -> std::fmt::Result {
235    let name_len_max;
236    if let Some(n) = table.keys().map(|s| s.len()).max() {
237        name_len_max = n;
238    } else {
239        // empty?
240        return Ok(());
241    }
242
243    let mut vec_table: Vec<_> = table.iter().collect();
244    vec_table.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
245
246    for (k, v) in vec_table {
247        writeln!(f, "{:>2$} = {:>9.6}", k, v, name_len_max)?;
248    }
249    Ok(())
250}