Compare commits

...

3 Commits

Author SHA1 Message Date
Emi Simpson eaad02b4e7
Add crate-level docs 2021-10-21 14:43:04 -04:00
Emi Simpson b04d8ed8bc
Expand Pronoun struct 2021-10-21 14:38:20 -04:00
Emi Simpson 4ce602c14a
Add weighted tables, use better time lib, use b32 2021-10-21 14:38:11 -04:00
6 changed files with 180 additions and 38 deletions

View File

@ -11,9 +11,12 @@ license-file = "LICENSE.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "0.4.19"
base64 = "0.13.0"
data-encoding = "2.3.2"
[dependencies.serde]
version = "1.0"
features = ["derive"]
[dependencies.time]
version = "0.3"
features = ["local-offset"]

View File

@ -1,4 +1,4 @@
User preferences are an unpadded base64 string, whose contents are defined in this
User preferences are an unpadded base32 string, whose contents are defined in this
document. Because the pref string is passed through the page URL, a small size is a top
priority.

View File

@ -1,7 +1,53 @@
//! The algorithms powering pronouns.today - Random pronouns generated daily
//!
//! This library contains all of the functionality for selecting a random pronoun based off of a
//! user's preferences, including parsing various preference string (prefstring) versions,
//! generating a weighted table based off of user preferences, and selecting a pronoun set based
//! off of that weighted table.
//!
//! ## Basic Usage
//!
//! ```
//! use pronouns_today::InstanceSettings;
//!
//! let instance_settings = InstanceSettings::default() // Or load from a config file
//!
//! // When you receive a request
//! let user_name = Some("Emi");
//! let user_prefstr = Some("acaqqbykawbag");
//! let pronouns = instance_settings.select_pronouns(user_name, user_prefstr);
//!
//! println!("Your pronouns are: {}", pronouns);
//! ```
//!
//! ## Advanced Usage
//!
//! The `InstanceSettings::select_pronouns()` method is really just a shorthand for the
//! more complex process going on behind the scenes. In reality, there are several steps
//! used to select the pronouns. Each step can be modified or run individually for
//! greater control.
//!
//! 1. Configure the [`InstanceSettings`] from a config or default
//! 2. Parse the user's prefstring with [`UserPreferences::from_prefstring()`][up]
//! 3. Produce a weighted table from the preferences using
//! [`UserPreferences::into_weighted_table()`][up]
//! 4. Roll a pronoun set from the weighted table with one of the methods in the
//! [`WeightedTable`] struct.
//! 5. Render the [`Pronoun`]s with one of the provided methods, or use the forms
//! individually.
//!
//! [up]: UserPreferences
pub mod user_preferences;
mod weighted_table;
use std::fmt;
use serde::{Serialize, Deserialize, self};
pub use weighted_table::WeightedTable;
pub use user_preferences::UserPreferences;
/// Runtime-constant setting that apply to an entire pronouns.today instance
///
/// These are values specified by the instance operator through the pronouns.today config file.
@ -34,10 +80,26 @@ impl InstanceSettings {
todo!()
}
pub fn parse_prefstring(&self, pref_string: Option<impl AsRef<str>>) -> &str {
todo!()
}
}
impl Default for InstanceSettings {
fn default() -> Self {
let pronouns: Vec<Pronoun> = vec![
["she", "her", "her", "hers", "herself" ].into(),
["he", "him", "his", "his", "himself" ].into(),
["they", "them", "their", "theirs", "themself" ].into(),
["it", "it", "its", "its", "itself" ].into(),
["xe", "xem", "xyr", "xyrs", "xemself" ].into(),
["ze", "zem", "zyr", "zyrs", "zemself" ].into(),
["fae", "faer", "faer", "faers", "faerself" ].into(),
["ne", "nem", "nir", "nirs", "nirself" ].into(),
["e", "em", "eir", "eirs", "eirself" ].into(),
["vey", "vem", "ver", "vers", "verself" ].into(),
];
InstanceSettings {
pronoun_list: pronouns
}
}
}
/// A standard five-form pronoun set
@ -89,6 +151,18 @@ pub struct Pronoun {
}
impl Pronoun {
pub fn render_threeform(&self) -> String {
format!("{}/{}/{}", self.subject_pronoun, self.object_pronoun, self.possesive_pronoun)
}
}
impl fmt::Display for Pronoun {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.render_threeform())
}
}
impl From<[String; 5]> for Pronoun {
fn from(five_form: [String; 5]) -> Self {
let mut five_form = IntoIterator::into_iter(five_form);
@ -103,15 +177,20 @@ impl From<[String; 5]> for Pronoun {
}
}
impl From<[&str; 5]> for Pronoun {
fn from(five_form: [&str; 5]) -> Self {
(&five_form).into()
}
}
impl From<&[&str; 5]> for Pronoun {
fn from(five_form: &[&str; 5]) -> Self {
let mut five_form = IntoIterator::into_iter(five_form);
Pronoun {
subject_pronoun: five_form.next().unwrap().to_string(),
object_pronoun: five_form.next().unwrap().to_string(),
possesive_determiner: five_form.next().unwrap().to_string(),
possesive_pronoun: five_form.next().unwrap().to_string(),
reflexive_pronoun: five_form.next().unwrap().to_string(),
subject_pronoun: five_form[0].to_string(),
object_pronoun: five_form[1].to_string(),
possesive_determiner: five_form[2].to_string(),
possesive_pronoun: five_form[3].to_string(),
reflexive_pronoun: five_form[4].to_string(),
}
}
}

View File

@ -6,16 +6,15 @@
pub mod v0;
use crate::{InstanceSettings, Pronoun};
use crate::{InstanceSettings, WeightedTable};
use base64::{decode, encode};
use chrono::{Local, NaiveDate};
use data_encoding::{BASE32_NOPAD, DecodeError};
/// A user's preferences for the probabilities of certain pronouns
///
/// This is the parsed version of a prefstring. The actual implementation details may vary across
/// versions, but universally they must be able to at least randomly select a pronoun set unique to
/// a given date and name.
/// versions, but universally they must be able to at least produce a weighted list of pronouns,
/// representing the probabilities the user wants for each pronoun set.
///
/// To this end, all versions of the user preferences implement [`Preference`]. For convenience,
/// `UserPreferences` also implements [`Preference`].
@ -32,19 +31,13 @@ pub enum UserPreferences<'a> {
/// See also: [`UserPreferences`]
pub trait Preference<'a> {
/// Randomly select a pronoun set for a given date and name.
/// Produce a weighted list of pronouns based on these preferences
///
/// This function should be *pure*, and any randomness must be emulating using PRNG. That is
/// to say, for any given date and name, this preference object must always produce the same
/// pronoun set.
fn select_pronouns_on_date(&self, date: NaiveDate, name: Option<&str>) -> &'a Pronoun;
/// A shorthand for calling [`Preference::select_pronouns_on_date()`] with today's date
///
/// The date is generated for the system's time and timezone
fn select_pronouns(&self, name: Option<&str>) -> &'a Pronoun {
self.select_pronouns_on_date(Local::today().naive_local(), name)
}
/// This is a one-directional conversion to a [`WeightedTable`]. This method is a
/// crucial step to randomly selecting a pronoun set based on a user's preferences, as
/// any selection is done by using a [`WeightedTable`]. All preference versions must
/// implement this method.
fn into_weighted_table(&self) -> WeightedTable;
/// Parse a given prefstring, after it's extraction from base64
///
@ -67,8 +60,9 @@ pub trait Preference<'a> {
///
/// This is the primary method of creating a `Preference` object from a prefstring. The
/// default implementation calls the underlying [`Preference::from_prefstring_bytes()`] method.
fn from_prefstring(prefstring: &str, settings: &'a InstanceSettings) -> Result<Self, base64::DecodeError> where Self: Sized {
decode(prefstring).map(|ps| Self::from_prefstring_bytes(ps.as_ref(), settings))
fn from_prefstring(prefstring: &str, settings: &'a InstanceSettings) -> Result<Self, DecodeError> where Self: Sized {
BASE32_NOPAD.decode(prefstring.as_ref())
.map(|ps| Self::from_prefstring_bytes(&ps, settings))
}
/// Serialize into a base64 prefstring
@ -76,15 +70,15 @@ pub trait Preference<'a> {
/// This is the primary method of creating a prefstring from a `Preference` object. The
/// default implementation calls the underlying [`Preference::into_prefstring_bytes()`] method.
fn into_prefstring(&self) -> String {
encode(self.into_prefstring_bytes())
BASE32_NOPAD.encode(&self.into_prefstring_bytes())
}
}
impl<'a> Preference<'a> for UserPreferences<'a> {
fn select_pronouns_on_date(&self, date: NaiveDate, name: Option<&str>) -> &'a Pronoun {
fn into_weighted_table(&self) -> WeightedTable {
match self {
UserPreferences::V0(pref) => pref,
}.select_pronouns_on_date(date, name)
}.into_weighted_table()
}
fn from_prefstring_bytes(bytes: &[u8], settings: &'a InstanceSettings) -> Self {

View File

@ -1,11 +1,14 @@
//! Version 0 Prefstrings
use crate::{InstanceSettings, Pronoun, user_preferences::Preference};
use crate::{
InstanceSettings,
Pronoun,
user_preferences::Preference,
WeightedTable,
};
use std::collections::HashMap;
use chrono::{NaiveDate};
/// A parsed version of the V0 prefstring
///
/// See the [prefstring specification][1] for more information about how this is interpretted.
@ -18,7 +21,7 @@ pub struct UserPreferencesV0<'a> {
}
impl<'a> Preference<'a> for UserPreferencesV0<'a> {
fn select_pronouns_on_date(&self, date: NaiveDate, name: Option<&str>) -> &'a Pronoun {
fn into_weighted_table(&self) -> WeightedTable {
todo!()
}

63
src/weighted_table.rs Normal file
View File

@ -0,0 +1,63 @@
use crate::Pronoun;
use time::{Date, Month, OffsetDateTime};
/// The start of the COVID-19 lockdowns
///
/// This is used as an epoch in order to convert from a given date to an integer seed. This is
/// specified as part of the algorithm for randomly selecting from a weighted list.
pub const COVID_EPOCH: Date = match Date::from_calendar_date(2020, Month::January, 26) {
Ok(d) => d,
Err(_) => Date::MIN, // This never runs, but we can't unwrap, so this is what we're stuck with
};
/// 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.
pub struct WeightedTable<'a>(pub Vec<(&'a Pronoun, u8)>);
impl<'a> WeightedTable<'a> {
/// 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]) -> &Pronoun {
self.select_on_date(
seed,
OffsetDateTime::now_local()
.unwrap_or_else(|_| OffsetDateTime::now_utc())
.date()
)
}
/// 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) -> &Pronoun {
let mut new_seed: Vec<u8> = Vec::with_capacity(seed.len() + 4);
new_seed.extend(
(
(date - COVID_EPOCH)
.whole_days()
as u32
).to_le_bytes()
);
new_seed.extend(seed);
self.select(seed.as_ref())
}
/// Randomly select a pronoun set for a given seed
///
/// 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, seed: &[u8]) -> &Pronoun {
todo!()
}
}