Skip to main content

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}