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}