diff --git a/src/user_preferences/error.rs b/src/user_preferences/error.rs index 10b0e38..cd01c2f 100644 --- a/src/user_preferences/error.rs +++ b/src/user_preferences/error.rs @@ -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") } } } diff --git a/src/user_preferences/mod.rs b/src/user_preferences/mod.rs index e81cbfc..d20c45c 100644 --- a/src/user_preferences/mod.rs +++ b/src/user_preferences/mod.rs @@ -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, 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, ParseError> { match self { UserPreferences::V0(pref) => pref, }.into_weighted_table(settings) diff --git a/src/user_preferences/v0.rs b/src/user_preferences/v0.rs index f71be40..d2751b7 100644 --- a/src/user_preferences/v0.rs +++ b/src/user_preferences/v0.rs @@ -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, 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::>>() + .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 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::>(); + let expected_owned = expected.into_iter() + .map(|(pronoun, weight)| (pronoun, weight)) + .collect::>(); + + 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); + } +} diff --git a/src/weighted_table.rs b/src/weighted_table.rs index bbb0179..a1b0353 100644 --- a/src/weighted_table.rs +++ b/src/weighted_table.rs @@ -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> {