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, Default)]
49pub struct RandomSelector<T> {
50    choices: Vec<Choice<T>>,
51    total_weight: f64,
52}
53
54impl<T> RandomSelector<T> {
55    pub fn with(mut self, weight: f64, value: T) -> Self {
56        self.choices.push(Choice { weight, value });
57        self.total_weight += weight.abs();
58        self
59    }
60    pub fn with_none(mut self, weight: f64) -> Self {
61        self.total_weight += weight.abs();
62        self
63    }
64    /// Complete choices to be None up to the given weight.
65    ///
66    /// This is convenient where all choices set are conventionnaly already normalized:
67    ///
68    /// ```
69    /// use rand_select::RandomSelector;
70    /// let selector = RandomSelector::default()
71    ///    .with(0.1, 'A')
72    ///    .with(0.2, 'B')
73    ///    .with_none_up_to(1.0);
74    /// ```
75    pub fn with_none_up_to(mut self, total_weight: f64) -> Self {
76        self.total_weight = total_weight.abs();
77        self
78    }
79    /// Select a random value among the provided ones.
80    pub fn select(&self) -> Option<&T> {
81        let mut rng = rand::rng();
82        self.select_with_rng(&mut rng)
83    }
84    /// Select a random value among the provided ones, with the generator of your choice.
85    pub fn select_with_rng<R: Rng>(&self, mut r: R) -> Option<&T> {
86        if self.total_weight == 0.0 {
87            return None;
88        }
89        let random_value: f64 = r.random_range(0.0..self.total_weight);
90        let mut cumulative_weight = 0.0;
91        for choice in &self.choices {
92            cumulative_weight += choice.weight;
93            if random_value < cumulative_weight {
94                return Some(&choice.value);
95            }
96        }
97        None
98    }
99}