//! Version 0 Prefstrings use crate::{ InstanceSettings, Pronoun, user_preferences::{Preference, ParseError}, 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. /// /// [1]: https://fem.mint.lgbt/Emi/PronounsToday/raw/branch/main/doc/User-Preference-String-Spec.txt #[derive(Debug, PartialEq, Eq)] pub struct UserPreferencesV0 { pub default_weight: u8, pub default_enabled: bool, pub commands: Vec, } impl Preference for UserPreferencesV0 { fn create_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)) ); } WeightedTable::new(weighted_pronouns) } /// ``` /// # use pronouns_today::user_preferences::{Preference, v0::{Command, UserPreferencesV0}}; /// let pbytes = &[ 0x00, 0x81, 0x08, 0xcf, 0x80 ][..]; /// let prefs = UserPreferencesV0::from_prefstring_bytes(&pbytes).unwrap(); /// assert_eq!(prefs.default_weight, 1); /// assert!(prefs.default_enabled); /// assert_eq!(prefs.commands, vec![ /// Command::SetWeight(8), /// Command::Move { toggle_enabled: true, distance: 15 }, /// Command::Move { toggle_enabled: false, distance: 0 }, /// ]); /// ``` fn from_prefstring_bytes(pbytes: &[u8]) -> Result { // Some simple error checks if pbytes.len() == 0 { return Err(ParseError::ZeroLengthPrefstring); } else if pbytes[0] != 00 { return Err(ParseError::VersionMismatch { expected_version: 0..1, expected_variant: 0..1, actual_version_byte: pbytes[0], }) } else if pbytes.len() == 1 { return Err( ParseError::MalformedContent( "Version 0 prefstrings must be at least two bytes long, but this one was just one byte long".into() ) ) } // Extract the defaults from byte 1 Ok(UserPreferencesV0 { default_enabled: pbytes[1] & 0b10000000 > 0, default_weight: pbytes[1] & 0b01111111, commands: pbytes[2..].iter().map(|b| Command::from(*b)).collect(), }) } fn as_prefstring_bytes(&self) -> Vec { let mut defaults_byte = 0u8; if self.default_enabled { defaults_byte |= 0b10000000; } defaults_byte |= self.default_weight & 0b01111111; vec![0, defaults_byte] .into_iter() .chain(self.commands.iter().map(|cmd| cmd.into())) .collect() } fn from_preferences(prefs: &[u8]) -> Self where Self: Sized { if prefs.is_empty() { return Self::default(); } let mut weight_counts = vec![0u8; *prefs.iter().max().unwrap() as usize + 1]; let mut num_zeros = 0; for w in prefs { if *w == 0 { num_zeros += 1; } else { weight_counts[*w as usize] += 1; } } let default_weight = weight_counts .iter() .enumerate() .max_by(|(_, v1), (_, v2)| v1.cmp(v2)) .map(|(w, _)| w as u8) .unwrap(); let default_enabled = num_zeros < prefs.len() / 2; let mut commands = Vec::new(); if default_enabled { let mut last_default = -1; for (i, w) in prefs.iter().enumerate() { let i = i as isize; if *w == default_weight { continue; } else { if i - last_default > 1 { let toggle_enabled = *w == 0; let distance = if toggle_enabled { (i - last_default) as u8 - 1 } else { (i - last_default) as u8 - 2 }; commands.push(Command::Move { toggle_enabled, distance }); if !toggle_enabled { commands.push(Command::SetWeight(*w)); } } else { commands.push(Command::SetWeight(*w)); } last_default = i; } } } else { let mut last_enabled = -1; for (i, w) in prefs.iter().enumerate() { let i = i as isize; if *w == 0 { continue; } else { if i - last_enabled > 1 { let toggle_enabled = *w == default_weight; let distance = if toggle_enabled { (i - last_enabled) as u8 - 1 } else { (i - last_enabled) as u8 - 2 }; commands.push(Command::Move { toggle_enabled, distance }); if !toggle_enabled { commands.push(Command::SetWeight(*w)); } } else { commands.push(Command::SetWeight(*w)); } last_enabled = i; } } } Self { default_weight, default_enabled, commands, } } } /// Default to all pronouns on with equal weight /// TODO make this configurable impl Default for UserPreferencesV0 { fn default() -> Self { Self { default_enabled: true, default_weight: 1, commands: vec![], } } } #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Command { SetWeight(u8), Move { toggle_enabled: bool, distance: u8, } } impl From for Command { /// ``` /// use pronouns_today::user_preferences::v0::Command; /// assert_eq!( /// Command::from(0b01001000), /// Command::SetWeight(0b01001000) /// ); /// assert_eq!( /// Command::from(0b11000000), /// Command::Move { /// toggle_enabled: true, /// distance: 0b00000000 /// } /// ); /// assert_eq!( /// Command::from(0b10101000), /// Command::Move { /// toggle_enabled: false, /// distance: 0b00101000 /// } /// ); /// ``` fn from(b: u8) -> Self { if b & 0b10000000 > 0 { Command::Move { toggle_enabled: b & 0b01000000 > 0, distance: b & 0b00111111, } } else { Command::SetWeight(b) } } } impl From<&Command> for u8 { fn from(command: &Command) -> Self { match command { // Some these ands shouldn't be needed if the command is valid Command::SetWeight(weight) => weight & 0b01111111, Command::Move { toggle_enabled, distance } => { 0b10000000 | ((*toggle_enabled as u8) << 6) | (distance & 0b00111111) } } } } #[cfg(test)] mod tests { use crate::{InstanceSettings, Pronoun, 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<&Pronoun>, expected: Vec<(&str, u8)>) { let actual_simplified = actual.weights() .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.create_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.create_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.create_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.create_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.create_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.create_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.create_weighted_table(&s).unwrap(); let expected_table = vec![ ("it", 3), ]; check_table(table, expected_table); } #[test] fn test_from_prefs_most_disabled1() { let pref_vals = vec![0,1,2,0,0]; let expected_prefs = UserPreferencesV0 { default_enabled: false, default_weight: 2, commands: vec![ Command::Move { toggle_enabled: false, distance: 0, }, Command::SetWeight(1), Command::SetWeight(2), ], }; let prefs = UserPreferencesV0::from_preferences(&pref_vals); assert_eq!(prefs, expected_prefs); } #[test] fn test_from_prefs_most_disabled2() { let pref_vals = vec![0,1,1,0,0,0,2]; let expected_prefs = UserPreferencesV0 { default_enabled: false, default_weight: 1, commands: vec![ Command::Move { toggle_enabled: true, distance: 1, }, Command::SetWeight(1), Command::Move { toggle_enabled: false, distance: 2, }, Command::SetWeight(2), ], }; let prefs = UserPreferencesV0::from_preferences(&pref_vals); assert_eq!(prefs, expected_prefs); } #[test] fn test_from_prefs_most_disabled3() { let pref_vals = vec![0,0,0,0,1,0,0,0,2]; let expected_prefs = UserPreferencesV0 { default_enabled: false, default_weight: 2, commands: vec![ Command::Move { toggle_enabled: false, distance: 3, }, Command::SetWeight(1), Command::Move { toggle_enabled: true, distance: 3, }, ], }; let prefs = UserPreferencesV0::from_preferences(&pref_vals); assert_eq!(prefs, expected_prefs); } #[test] fn test_from_prefs_most_enabled1() { let pref_vals = vec![0,1,2,1,1]; let expected_prefs = UserPreferencesV0 { default_enabled: true, default_weight: 1, commands: vec![ Command::SetWeight(0), Command::Move { toggle_enabled: false, distance: 0, }, Command::SetWeight(2), ], }; let prefs = UserPreferencesV0::from_preferences(&pref_vals); assert_eq!(prefs, expected_prefs); } #[test] fn test_from_prefs_most_enabled2() { let pref_vals = vec![1,0,0,1,1,1,2]; let expected_prefs = UserPreferencesV0 { default_enabled: true, default_weight: 1, commands: vec![ Command::Move { toggle_enabled: true, distance: 1, }, Command::SetWeight(0), Command::Move { toggle_enabled: false, distance: 2, }, Command::SetWeight(2), ], }; let prefs = UserPreferencesV0::from_preferences(&pref_vals); assert_eq!(prefs, expected_prefs); } #[test] fn test_from_prefs_most_enabled3() { let pref_vals = vec![1,1,1,1,0,1,1,1,2]; let expected_prefs = UserPreferencesV0 { default_enabled: true, default_weight: 1, commands: vec![ Command::Move { toggle_enabled: true, distance: 4, }, Command::Move { toggle_enabled: false, distance: 2, }, Command::SetWeight(2), ], }; let prefs = UserPreferencesV0::from_preferences(&pref_vals); assert_eq!(prefs, expected_prefs); } #[test] fn test_as_prefstring_bytes() { let prefs = UserPreferencesV0::default(); let expected_bytes = vec![0 , 0b10000001]; assert_eq!(expected_bytes, prefs.as_prefstring_bytes()); } #[test] fn test_from_prefstring_bytes() { let bytes = vec![0 , 0b10000001]; let prefs = UserPreferencesV0::from_prefstring_bytes(&bytes).unwrap(); let expected_prefs = UserPreferencesV0::default(); assert_eq!(expected_prefs, prefs); } }