word.rs
6.5 kB · rust · 179 lines
1use super::{kind, Bang, Named};2use mrlycore::errors::{value_error, Result};3use serde::{Deserialize, Serialize};45kind!("word");67/// A magic word: an ordered list of design letters, first letter outermost, each at its own side.8///9/// ```10/// use mrlymath::name::{Named, Word};11/// let word = Word::new(2, &[(7, 3), (14, 7), (9, 5)]).unwrap();12/// assert_eq!(word.to_json(), r#"{"kind":"word","dim":2,"magic":[7,14,9],"side":[3,7,5]}"#);13/// assert_eq!(Word::from_json(&word.to_json()).unwrap(), word);14/// ```15#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]16#[serde(deny_unknown_fields)]17pub struct Word {18 /// The kind word.19 pub kind: Kind,20 /// The number of axes every letter shares.21 pub dim: usize,22 /// The codes of the letters in order.23 pub magic: Vec<u128>,24 /// The side each letter renders at.25 pub side: Vec<usize>,26 /// The base of each letter, absent when every letter is base 2.27 #[serde(default, skip_serializing_if = "Option::is_none")]28 pub base: Option<Vec<usize>>,29}3031impl Word {32 /// Pins an ordered letter list at base 2, or an error below two letters or outside the code range.33 pub fn new(dim: usize, letters: &[(u128, usize)]) -> Result<Word> {34 Word {35 kind: Kind,36 dim,37 magic: letters.iter().map(|&(code, _)| code).collect(),38 side: letters.iter().map(|&(_, side)| side).collect(),39 base: None,40 }41 .checked()42 }43 /// Returns the base of every letter, 2 where the name says nothing.44 pub fn bases(&self) -> Vec<usize> {45 self.base46 .clone()47 .unwrap_or_else(|| vec![2; self.magic.len()])48 }49 /// Returns every letter as a design pinned to the word's dimension and its own base.50 pub fn letters(&self) -> Vec<Bang> {51 self.magic52 .iter()53 .zip(self.bases())54 .map(|(&code, base)| Bang::new(code, self.dim, base))55 .collect()56 }57}5859impl Named for Word {60 const KIND: &'static str = "word";61 const LISTS: &'static [&'static str] = &["magic", "side", "base"];62 fn checked(mut self) -> Result<Word> {63 if self.magic.len() < 2 {64 return value_error("a word needs at least two letters.");65 }66 if self.side.len() != self.magic.len() {67 return value_error(format!(68 "a word of {} letters wants {} sides, not {}.",69 self.magic.len(),70 self.magic.len(),71 self.side.len()72 ));73 }74 if let Some(base) = &self.base {75 if base.len() != self.magic.len() {76 return value_error(format!(77 "a word of {} letters wants {} bases, not {}.",78 self.magic.len(),79 self.magic.len(),80 base.len()81 ));82 }83 if base.iter().all(|&b| b == 2) {84 self.base = None;85 }86 }87 for letter in self.letters() {88 if letter.code == 0 {89 return value_error("a letter code of 0 draws nothing.");90 }91 letter.checked()?;92 }93 if let Some(side) = self.side.iter().find(|&&side| side < 2) {94 return value_error(format!("letter side {side} is below two."));95 }96 Ok(self)97 }98}99100#[cfg(test)]101mod tests {102 use super::*;103104 #[test]105 fn the_word_holds_through_every_view() {106 let word = Word::new(2, &[(7, 3), (14, 7), (9, 5)]).unwrap();107 assert_eq!(108 word.to_json(),109 r#"{"kind":"word","dim":2,"magic":[7,14,9],"side":[3,7,5]}"#110 );111 assert_eq!(word.to_url(), "/word?dim=2&magic=7,14,9&side=3,7,5");112 assert_eq!(word.to_file(), "word_dim=2_magic=[7,14,9]_side=[3,7,5]");113 assert_eq!(word.to_mrly(), "word dim 2, magic [7 14 9], side [3 7 5]");114 assert_eq!(Word::from_json(&word.to_json()).unwrap(), word);115 assert_eq!(Word::from_url(&word.to_url()).unwrap(), word);116 assert_eq!(Word::from_file(&word.to_file()).unwrap(), word);117 assert_eq!(word.to_id().len(), 8);118 }119 #[test]120 fn the_word_name_round_trips() {121 for letters in [vec![(3u128, 2usize), (6, 2)], vec![(7, 3), (14, 7), (9, 5)]] {122 let word = Word::new(2, &letters).unwrap();123 assert_eq!(Word::from_json(&word.to_json()).unwrap(), word);124 }125 }126 #[test]127 fn order_shows_in_the_name() {128 let one = Word::new(2, &[(3, 2), (6, 2)]).unwrap().to_json();129 let other = Word::new(2, &[(6, 2), (3, 2)]).unwrap().to_json();130 assert_ne!(one, other);131 }132 #[test]133 fn a_solid_or_mixed_base_word_has_a_name() {134 let solid = Word::new(3, &[(23, 3), (3, 3)]).unwrap();135 assert_eq!(136 solid.to_json(),137 r#"{"kind":"word","dim":3,"magic":[23,3],"side":[3,3]}"#138 );139 let mixed = Word {140 base: Some(vec![2, 3]),141 ..Word::new(2, &[(7, 3), (14, 3)]).unwrap()142 };143 let mixed = Word {144 magic: vec![7, 98],145 ..mixed146 }147 .checked()148 .unwrap();149 assert_eq!(150 mixed.to_json(),151 r#"{"kind":"word","dim":2,"magic":[7,98],"side":[3,3],"base":[2,3]}"#152 );153 assert_eq!(Word::from_json(&mixed.to_json()).unwrap(), mixed);154 assert_eq!(mixed.letters()[1], Bang::new(98, 2, 3));155 let mut plain = Word::new(2, &[(7, 3), (14, 3)]).unwrap();156 plain.base = Some(vec![2, 2]);157 assert_eq!(plain.checked().unwrap().base, None);158 }159 #[test]160 fn the_grammar_refuses_what_it_cannot_draw() {161 assert!(Word::new(2, &[(7, 3)]).is_err());162 assert!(Word::new(2, &[(273, 3), (9, 2)]).is_err());163 assert!(Word::new(2, &[(0, 3), (9, 2)]).is_err());164 assert!(Word::new(2, &[(7, 1), (9, 2)]).is_err());165 assert!(Word::new(0, &[(1, 2), (1, 2)]).is_err());166 for bad in [167 r#"{"kind":"word","dim":2,"magic":[7,14],"side":[3]}"#,168 r#"{"kind":"word","dim":2,"magic":[7,14],"side":[3,3],"base":[3]}"#,169 r#"{"kind":"word","dim":2,"magic":[7,98],"side":[3,3]}"#,170 r#"{"kind":"word","magic":[7,14],"side":[3,3]}"#,171 r#"{"kind":"word","dim":2,"magic":[7,14],"side":[3,3],"level":2}"#,172 r#"{"kind":"bang","dim":2,"code":7}"#,173 "word dim 2, magic [7 9], side [3 5]",174 "word_dim=2_magic=[7,9]_side=[3,5]",175 ] {176 assert!(Word::from_json(bad).is_err(), "{bad}");177 }178 }179}