PronounsToday/src/weighted_table.rs

116 lines
4.1 KiB
Rust

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<Loot: Eq + Ord + Hash + Debug>(BTreeMap<Loot, u8>);
impl<Loot: Eq + Ord + Hash + Copy + Debug> WeightedTable<Loot> {
/// 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<Item=(Loot, u8)>) -> Result<Self, ParseError> {
let table: BTreeMap<Loot, u8> = 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<Loot, u8> {
&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
}
}