Add coversion to a weighted table to v0

This commit is contained in:
Emi Simpson 2021-10-21 21:48:13 -04:00
parent 01ef419505
commit 8a486323df
Signed by: Emi
GPG key ID: A12F2C2FFDC3D847
4 changed files with 274 additions and 6 deletions

View file

@ -12,7 +12,7 @@ use std::{
use data_encoding::DecodeError;
/// An error occured while trying to parse a user's prefstring
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ParseError {
@ -58,6 +58,18 @@ pub enum ParseError {
/// was zero bytes long. Often, this means that the developer should simply use the
/// instance default preferences.
ZeroLengthPrefstring,
/// The prefstring assumes the existance of more pronouns than exist
///
/// Often a symptom of a malformed prefstring, this error occurs when the prefstring
/// is correctly formatted, but when a [`pronouns_today::WeightedTable`] is formed
/// from it, the prefstring refers to a pronoun with an index greater than is in the
/// list. For example, setting the weight of pronoun set number 20 when there's only
/// 15 pronoun sets known by the instance.
///
/// This can be caused by reducing the number of available pronouns after a prefstring
/// has been generated, or by trying to parse a lucky malformed prefstring.
PrefstringExceedsPronounCount,
}
impl fmt::Display for ParseError {
@ -92,6 +104,9 @@ impl fmt::Display for ParseError {
},
ParseError::ZeroLengthPrefstring => {
write!(f, "Tried to parse a zero-length prefstring")
},
ParseError::PrefstringExceedsPronounCount => {
write!(f, "The user preferences refers to pronouns beyond those that are known")
}
}
}

View file

@ -39,7 +39,7 @@ pub trait Preference {
/// 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<'a>(&self, settings: &'a InstanceSettings) -> WeightedTable<'a>;
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<'a>, ParseError>;
/// Parse a given prefstring, after it's extraction from base64
///
@ -78,7 +78,7 @@ pub trait Preference {
}
impl Preference for UserPreferences {
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> WeightedTable<'a> {
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<'a>, ParseError> {
match self {
UserPreferences::V0(pref) => pref,
}.into_weighted_table(settings)

View file

@ -6,6 +6,11 @@ use crate::{
WeightedTable,
};
use std::{
collections::HashMap,
iter::{Extend, self},
};
/// A parsed version of the V0 prefstring
///
/// See the [prefstring specification][1] for more information about how this is interpretted.
@ -18,8 +23,58 @@ pub struct UserPreferencesV0 {
}
impl Preference for UserPreferencesV0 {
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> WeightedTable<'a> {
todo!()
fn into_weighted_table<'a>(&self, settings: &'a InstanceSettings) -> Result<WeightedTable<'a>, ParseError> {
let mut remaining_pronouns = settings.pronoun_list.iter();
let mut weighted_pronouns = HashMap::with_capacity(settings.pronoun_list.len());
for command in &self.commands {
match command {
Command::SetWeight(weight) => {
if *weight > 0 {
weighted_pronouns.insert(
remaining_pronouns.next()
.ok_or(ParseError::PrefstringExceedsPronounCount)?,
*weight
);
}
},
Command::Move { toggle_enabled, distance } => {
let skipped_pronouns = iter::repeat_with(|| remaining_pronouns.next())
.take(*distance as usize)
.collect::<Option<Vec<_>>>()
.ok_or(ParseError::PrefstringExceedsPronounCount)?;
if self.default_enabled {
weighted_pronouns.extend(
skipped_pronouns
.into_iter()
.map(|pronoun| (pronoun, self.default_weight))
)
}
let last_enabled = *toggle_enabled != self.default_enabled;
let last_pronoun = remaining_pronouns.next()
.ok_or(ParseError::PrefstringExceedsPronounCount)?;
if last_enabled {
weighted_pronouns.insert(last_pronoun, self.default_weight);
}
}
}
}
if self.default_enabled {
weighted_pronouns.extend(
remaining_pronouns
.into_iter()
.map(|pronoun| (pronoun, self.default_weight))
);
}
Ok(WeightedTable(weighted_pronouns))
}
/// ```
@ -109,3 +164,198 @@ impl From<u8> for Command {
}
}
}
#[cfg(test)]
mod tests {
use crate::{InstanceSettings, WeightedTable};
use crate::user_preferences::{Preference, ParseError};
use crate::user_preferences::v0::{UserPreferencesV0, Command};
use std::collections::HashMap;
fn testing_instance_settings() -> InstanceSettings {
InstanceSettings {
pronoun_list: 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(),
],
}
}
fn check_table(actual: WeightedTable, expected: Vec<(&str, u8)>) {
let actual_simplified = actual.0.into_iter()
.filter(|(pronoun, weight)| *weight > 0)
.map(|(pronoun, weight)| (pronoun.subject_pronoun.as_str(), weight))
.collect::<HashMap<&str, u8>>();
let expected_owned = expected.into_iter()
.map(|(pronoun, weight)| (pronoun, weight))
.collect::<HashMap<&str, u8>>();
assert_eq!(actual_simplified, expected_owned);
}
#[test]
fn test_into_weighted_table_no_commands() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: true,
default_weight: 2,
commands: Vec::new(),
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("she", 2),
("he", 2),
("they", 2),
("it", 2),
("xe", 2),
];
check_table(table, expected_table);
}
#[test]
fn test_into_weighted_table_move_commands_select_deactivation() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: true,
default_weight: 3,
commands: vec![
Command::Move {
toggle_enabled: false,
distance: 1
},
Command::Move {
toggle_enabled: true,
distance: 2
},
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("she", 3),
("he", 3),
("they", 3),
("it", 3),
];
}
#[test]
fn test_into_weighted_table_weight_commands() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 1,
commands: vec![
Command::SetWeight(5),
Command::SetWeight(4),
Command::SetWeight(3),
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("she", 5),
("he", 4),
("they", 3),
];
check_table(table, expected_table);
}
#[test]
fn test_into_weighted_table_combined_commands() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 9,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 1
},
Command::SetWeight(5),
Command::Move {
toggle_enabled: false,
distance: 0
},
Command::SetWeight(4),
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("he", 9),
("they", 5),
("xe", 4),
];
check_table(table, expected_table);
}
#[test]
fn test_into_weighted_table_move_past_boundry() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 1,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 5
},
],
};
let err = p.into_weighted_table(&s).unwrap_err();
assert_eq!(err, ParseError::PrefstringExceedsPronounCount);
}
#[test]
fn test_into_weighted_table_set_weight_past_boundry() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: false,
default_weight: 1,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 4
},
Command::SetWeight(9),
],
};
let err = p.into_weighted_table(&s).unwrap_err();
assert_eq!(err, ParseError::PrefstringExceedsPronounCount);
}
#[test]
fn test_into_weighted_table_zero_default_weight() {
let s = testing_instance_settings();
let p = UserPreferencesV0 {
default_enabled: true,
default_weight: 0,
commands: vec![
Command::Move {
toggle_enabled: true,
distance: 2
},
Command::SetWeight(3),
],
};
let table = p.into_weighted_table(&s).unwrap();
let expected_table = vec![
("it", 3),
];
check_table(table, expected_table);
}
}

View file

@ -1,5 +1,7 @@
use crate::Pronoun;
use std::collections::HashMap;
use time::{Date, Month, OffsetDateTime};
/// The start of the COVID-19 lockdowns
@ -19,7 +21,8 @@ pub const COVID_EPOCH: Date = match Date::from_calendar_date(2020, Month::Januar
/// 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)>);
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct WeightedTable<'a>(pub HashMap<&'a Pronoun, u8>);
impl<'a> WeightedTable<'a> {