rand_select/lib.rs
1//!
2//! The RandomSelector selects among weighted choices, without bias.
3//!
4//! ```
5//! use rand_select::RandomSelector;
6//! let selector = RandomSelector::default()
7//! .with(1.0, 'A')
8//! .with(1.5, 'B')
9//! .with_none(3.0);
10//! let l = selector.select();
11//! // l has half a chance to be None, and is 50% more likely to be 'B' than 'A'
12//! ```
13//!
14//! If you set a value and call neither `with_none` nor `with_none_up_to`, the selector will always return a value.
15//!
16//! If you have already normalized weight, `with_none_up_to` is a convenient way to set the total weight of the selector:
17//!
18//! ```
19//! use rand_select::RandomSelector;
20//! let selector = RandomSelector::default()
21//! .with(0.1, 'A')
22//! .with(0.2, 'B')
23//! .with_none_up_to(1.0);
24//! ```
25//! The RandomSelector is designed for reuse, and can use the RNG of your choice.
26
27use {
28 rand::Rng,
29};
30
31#[derive(Clone)]
32struct Choice<T> {
33 weight: f64,
34 value: T,
35}
36
37/// A selector allowing to randomly select a value from a set of choices, each with an associated weight.
38///
39/// ```
40/// use rand_select::RandomSelector;
41/// let selector = RandomSelector::default()
42/// .with(1.0, 'A')
43/// .with(1.5, 'B')
44/// .with_none(3.0);
45/// let l = selector.select();
46/// // l has half a chance to be None, and is 50% more likely to be 'B' than 'A'
47/// ```
48#[derive(Clone)]
49pub struct RandomSelector<T> {
50 choices: Vec<Choice<T>>,
51 total_weight: f64,
52}
53
54impl<T> Default for RandomSelector<T> {
55 fn default() -> Self {
56 Self {
57 choices: Vec::new(),
58 total_weight: 0.0,
59 }
60 }
61}
62
63impl<T> RandomSelector<T> {
64 pub fn with(mut self, weight: f64, value: T) -> Self {
65 self.choices.push(Choice { weight, value });
66 self.total_weight += weight.abs();
67 self
68 }
69 pub fn with_none(mut self, weight: f64) -> Self {
70 self.total_weight += weight.abs();
71 self
72 }
73 /// Complete choices to be None up to the given weight.
74 ///
75 /// This is convenient where all choices set are conventionnaly already normalized:
76 ///
77 /// ```
78 /// use rand_select::RandomSelector;
79 /// let selector = RandomSelector::default()
80 /// .with(0.1, 'A')
81 /// .with(0.2, 'B')
82 /// .with_none_up_to(1.0);
83 /// ```
84 pub fn with_none_up_to(mut self, total_weight: f64) -> Self {
85 self.total_weight = total_weight.abs();
86 self
87 }
88 /// Select a random value among the provided ones.
89 pub fn select(&self) -> Option<&T> {
90 let mut rng = rand::rng();
91 self.select_with_rng(&mut rng)
92 }
93 /// Select a random value among the provided ones, with the generator of your choice.
94 pub fn select_with_rng<R: Rng>(&self, mut r: R) -> Option<&T> {
95 if self.total_weight == 0.0 {
96 return None;
97 }
98 let random_value: f64 = r.random_range(0.0..self.total_weight);
99 let mut cumulative_weight = 0.0;
100 for choice in &self.choices {
101 cumulative_weight += choice.weight;
102 if random_value < cumulative_weight {
103 return Some(&choice.value);
104 }
105 }
106 None
107 }
108}