use crate::{user_preferences::ParseError, util::{self, pcg64, pcg64_seed, seed_with_date, seed_with_today}}; use std::{collections::BTreeMap, hash::Hash}; use std::fmt::Debug; use std::cmp::Eq; use time::Date; /// A list of pronouns and their associated weights, used for random selection /// /// Weights are typically representative of a user's preference towards a pronoun. A pronoun with /// a weight of 10 is twice as likely to be selected as a pronoun with a weight of 5. /// /// This struct is use to represent these weights before they are used to randomly select a /// pronoun. Additional methods are provided to perform this random selection on a weighted list, /// using as a seed both an arbitrary string of bytes and a Date. #[derive(Clone, PartialEq, Eq, Debug)] pub struct WeightedTable(BTreeMap); impl WeightedTable { /// Attempt to generate a new table /// /// Accepts an iterator of tuples. Each tupple represents one outcome. The left /// element of the tupple is the value that outcome produces (the table loot, if you /// will), and the right element of the tuple is the weight associated with that /// outcome. /// /// Performs a safety check to prevent creating an empty roll table. Raises a parse /// error if the safety check fails. The original tabel can be accessed as a /// [`HashMap`] via the [`WeightedTable::weights()`] method pub fn new(table: impl IntoIterator) -> Result { let table: BTreeMap = table.into_iter() .filter(|(_, weight)| *weight > 0) .collect(); if table.is_empty() { Err(ParseError::EmptyWeightedTable) } else { Ok(WeightedTable(table)) } } /// Access the orginal table of weights underlying this table /// /// Returns a [`HashMap`] mapping pronouns to their respective weights. pub fn weights(&self) -> &BTreeMap { &self.0 } /// Produces a table of cumulative weights for the roll table /// /// Each entry has the weight of itself as well as all of the previous entries. This /// is provided in a [`Vec`] ordered from lowest weight to highest. This table can be /// used to efficiently perform a custom roll. /// /// Two values are returned: The table itself, and the highest value in the table /// (that is, the sum of all of the weights) pub fn rollable_table(&self) -> (Vec<(u64, Loot)>, u64) { let mut cumulative_weights: Vec<(u64, Loot)> = Vec::with_capacity(self.0.len()); let mut weight_so_far = 0; for (pronoun, weight) in &self.0 { weight_so_far += *weight as u64; cumulative_weights.push((weight_so_far, *pronoun)); } (cumulative_weights, weight_so_far) } /// A shorthand for calling [`WeightedTable::select_on_date()`] with today's date /// /// The date is generated for the system's time and timezone pub fn select_today(&self, seed: &[u8]) -> Loot { let mut generator = util::INITIAL_STATE; seed_with_today(&mut generator); pcg64_seed(&mut generator, seed); self.select(&mut generator) } /// Randomly select a pronoun set for a given date and name. /// /// Is a wrapper for calling [`WeightedTable::select`] with the given date mixed into the seed. pub fn select_on_date(&self, seed: &[u8], date: Date) -> Loot { let mut generator = util::INITIAL_STATE; seed_with_date(&mut generator, date); pcg64_seed(&mut generator, seed); self.select(&mut generator) } /// Randomly select a pronoun set from a given pre-seeded genenator /// /// You're probably looking for [`select_today()`] or [`select_on_date()`] /// /// This function is *pure*, and any randomness is produced internally using PRNG seeded with /// the given date and seed. That is to say, for any given seed, this table must always /// produce the same pronoun set. pub fn select(&self, generator: &mut u128) -> Loot { let (rollable_table, sum_weights) = self.rollable_table(); let random = pcg64(generator) % sum_weights; rollable_table.iter() .filter(|(weight, _)| random < *weight) .next() .expect("A table was generated with zero entries. This should be impossible.") .1 } }