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}