Compare commits
3 Commits
aeef51f1e4
...
eaad02b4e7
Author | SHA1 | Date |
---|---|---|
Emi Simpson | eaad02b4e7 | |
Emi Simpson | b04d8ed8bc | |
Emi Simpson | 4ce602c14a |
|
@ -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"]
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
97
src/lib.rs
97
src/lib.rs
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue