bang.rs

8.8 kB · rust · 269 lines

1use super::{kind, Named};2use mrlycore::errors::{value_error, Result};3use serde::{Deserialize, Serialize};45kind!("bang");67/// The lattice the cells sit on.8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]9#[serde(rename_all = "lowercase")]10pub enum Lattice {11    /// The square lattice, the default the name elides.12    #[default]13    Square,14    /// The hexagonal lattice.15    Hex,16}1718impl Lattice {19    /// Returns whether this is the square lattice.20    pub fn is_square(&self) -> bool {21        matches!(self, Lattice::Square)22    }23    /// Returns the number of unit directions a twist may pick from.24    pub fn units(self) -> usize {25        match self {26            Lattice::Square => 4,27            Lattice::Hex => 6,28        }29    }30}3132fn two() -> usize {33    234}3536fn is_two(base: &usize) -> bool {37    *base == 238}3940/// A design code pinned to its dimension, lattice and base, with one unit index per filled digit when it twists.41///42/// ```43/// use mrlymath::name::{Bang, Named};44/// let carpet = Bang::new(7, 2, 2);45/// assert_eq!(carpet.to_json(), r#"{"kind":"bang","dim":2,"code":7}"#);46/// assert_eq!(Bang::from_json(&carpet.to_json()).unwrap(), carpet);47/// ```48#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]49#[serde(deny_unknown_fields)]50pub struct Bang {51    /// The kind word.52    pub kind: Kind,53    /// The number of axes.54    pub dim: usize,55    /// The lattice, square unless said.56    #[serde(default, skip_serializing_if = "Lattice::is_square")]57    pub lattice: Lattice,58    /// The digits per axis, 2 unless said.59    #[serde(default = "two", skip_serializing_if = "is_two")]60    pub base: usize,61    /// The design as a number.62    pub code: u128,63    /// One unit index per filled digit, absent when nothing turns.64    #[serde(default, skip_serializing_if = "Option::is_none")]65    pub twist: Option<Vec<usize>>,66}6768impl Bang {69    /// Pins a code to its dimension and base on the square lattice.70    pub fn new(code: u128, dim: usize, base: usize) -> Bang {71        Bang {72            kind: Kind,73            dim,74            lattice: Lattice::Square,75            base,76            code,77            twist: None,78        }79    }80    /// Returns the number of digits the code addresses, or an error past the u128 code space.81    pub fn cells(&self) -> Result<u32> {82        if self.dim < 1 {83            return value_error("dim must be at least 1.");84        }85        if self.base < 2 {86            return value_error("base must be at least 2.");87        }88        match u32::try_from(self.base)89            .ok()90            .zip(u32::try_from(self.dim).ok())91            .and_then(|(b, d)| b.checked_pow(d))92        {93            Some(cells) if cells < 128 => Ok(cells),94            _ => value_error(format!(95                "dim {} base {} exceeds the u128 code space.",96                self.dim, self.base97            )),98        }99    }100}101102impl Named for Bang {103    const KIND: &'static str = "bang";104    const LISTS: &'static [&'static str] = &["twist"];105    const BARE: &'static [&'static str] = &["lattice"];106    fn checked(mut self) -> Result<Bang> {107        let cells = self.cells()?;108        if self.code >> cells != 0 {109            return value_error(format!(110                "code {} out of range for dim {} base {} (0..{}).",111                self.code,112                self.dim,113                self.base,114                (1u128 << cells) - 1115            ));116        }117        if let Some(twist) = &self.twist {118            if twist.iter().all(|&unit| unit == 0) {119                self.twist = None;120            } else if twist.len() != self.code.count_ones() as usize {121                return value_error(format!(122                    "twist holds {} units for {} filled digits.",123                    twist.len(),124                    self.code.count_ones()125                ));126            } else if let Some(unit) = twist.iter().find(|&&unit| unit >= self.lattice.units()) {127                return value_error(format!(128                    "twist unit {unit} is not below the {} units of the lattice.",129                    self.lattice.units()130                ));131            }132        }133        Ok(self)134    }135}136137#[cfg(test)]138mod tests {139    use super::*;140    use mrlycore::rng::Rng;141142    const KOCH: &str =143        r#"{"kind":"bang","dim":2,"lattice":"hex","base":3,"code":39,"twist":[0,1,5,0]}"#;144145    fn koch() -> Bang {146        Bang {147            lattice: Lattice::Hex,148            twist: Some(vec![0, 1, 5, 0]),149            ..Bang::new(39, 2, 3)150        }151    }152153    #[test]154    fn defaults_elide() {155        assert_eq!(156            Bang::new(7, 2, 2).to_json(),157            r#"{"kind":"bang","dim":2,"code":7}"#158        );159        assert_eq!(160            Bang::new(23, 3, 2).to_json(),161            r#"{"kind":"bang","dim":3,"code":23}"#162        );163        assert_eq!(164            Bang::new(0, 2, 3).to_json(),165            r#"{"kind":"bang","dim":2,"base":3,"code":0}"#166        );167        assert_eq!(koch().to_json(), KOCH);168    }169    #[test]170    fn canonical_names_parse() {171        assert_eq!(172            Bang::from_json(r#"{"kind":"bang","dim":2,"code":7}"#).unwrap(),173            Bang::new(7, 2, 2)174        );175        assert_eq!(176            Bang::from_json(r#"{"kind":"bang","dim":2,"base":3,"code":511}"#).unwrap(),177            Bang::new(511, 2, 3)178        );179        assert_eq!(Bang::from_json(KOCH).unwrap(), koch());180    }181    #[test]182    fn a_spelt_default_folds_to_the_canonical_string() {183        let spelt =184            r#"{"code":7, "base":2, "lattice":"square", "dim":2, "kind":"bang", "twist":[0,0,0]}"#;185        assert_eq!(Bang::from_json(spelt).unwrap(), Bang::new(7, 2, 2));186        assert_eq!(187            Bang::from_json(spelt).unwrap().to_json(),188            r#"{"kind":"bang","dim":2,"code":7}"#189        );190    }191    #[test]192    fn only_a_fitting_bang_parses() {193        for bad in [194            r#"{"kind":"rule","dim":2,"code":7}"#,195            r#"{"dim":2,"code":7}"#,196            r#"{"kind":"bang","code":7}"#,197            r#"{"kind":"bang","dim":2}"#,198            r#"{"kind":"bang","dim":2,"code":16}"#,199            r#"{"kind":"bang","dim":0,"code":1}"#,200            r#"{"kind":"bang","dim":2,"base":1,"code":1}"#,201            r#"{"kind":"bang","dim":7,"code":1}"#,202            r#"{"kind":"bang","dim":2,"code":-1}"#,203            r#"{"kind":"bang","dim":2,"code":"7"}"#,204            r#"{"kind":"bang","dim":2,"code":7,"level":3}"#,205            r#"{"kind":"bang","dim":2,"code":7,"lattice":"cubic"}"#,206            r#"{"kind":"bang","dim":2,"code":7,"twist":[1,2]}"#,207            r#"{"kind":"bang","dim":2,"code":7,"twist":[1,2,4]}"#,208            r#"{"kind":"bang","dim":2,"lattice":"hex","code":7,"twist":[1,2,6]}"#,209            "bang dim 2, code 7",210            "bang_dim=2_code=7",211            "7",212        ] {213            assert!(Bang::from_json(bad).is_err(), "{bad}");214        }215    }216    #[test]217    fn a_code_past_u64_survives() {218        let wide = Bang::new(1u128 << 99, 1, 100);219        let text = wide.to_json();220        assert_eq!(221            text,222            format!(223                r#"{{"kind":"bang","dim":1,"base":100,"code":{}}}"#,224                1u128 << 99225            )226        );227        assert_eq!(Bang::from_json(&text).unwrap(), wide);228        assert_eq!(Bang::from_file(&wide.to_file()).unwrap(), wide);229    }230    #[test]231    fn the_koch_row_holds_through_every_view() {232        let koch = koch();233        assert_eq!(234            koch.to_url(),235            "/bang?dim=2&lattice=hex&base=3&code=39&twist=0,1,5,0"236        );237        assert_eq!(238            koch.to_file(),239            "bang_dim=2_lattice=hex_base=3_code=39_twist=[0,1,5,0]"240        );241        assert_eq!(242            koch.to_mrly(),243            "bang dim 2, hex, base 3, code 39, twist [0 1 5 0]"244        );245        assert_eq!(Bang::from_url(&koch.to_url()).unwrap(), koch);246        assert_eq!(Bang::from_file(&koch.to_file()).unwrap(), koch);247        assert_eq!(koch.to_id().len(), 8);248        assert_ne!(koch.to_id(), Bang::new(39, 2, 3).to_id());249        assert_eq!(Bang::new(7, 2, 2).to_mrly(), "bang dim 2, code 7");250        assert_eq!(Bang::new(7, 2, 2).to_file(), "bang_dim=2_code=7");251    }252    #[test]253    fn seeded_values_round_trip() {254        let mut rng = Rng::new(11);255        for _ in 0..500 {256            let base = *rng.choice(&[2usize, 3]);257            let top = if base == 2 { 4 } else { 3 };258            let dim = rng.range(1, top) as usize;259            let cells = (base as u32).pow(dim as u32);260            let code = rng.below(1usize << cells) as u128;261            let bang = Bang::new(code, dim, base);262            let text = bang.to_json();263            assert_eq!(Bang::from_json(&text).unwrap(), bang);264            assert_eq!(Bang::from_json(&text).unwrap().to_json(), text);265            assert_eq!(Bang::from_url(&bang.to_url()).unwrap(), bang);266            assert_eq!(Bang::from_file(&bang.to_file()).unwrap(), bang);267        }268    }269}