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}