Skip to main content

latexml_core/common/
mudimension.rs

1use std::fmt;
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use super::dimension::fixedformat;
7use crate::{
8  Object,
9  common::numeric_ops::{NumericOps, UNITY_F64, fixpoint, kround},
10  definition::register::RegisterType,
11};
12
13static MUDIM_SPEC_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(-?\d*\.?\d*)mu$").unwrap());
14
15#[derive(Debug, Copy, Clone, PartialEq, Default, Eq)]
16pub struct MuDimension(pub i64);
17
18impl NumericOps for MuDimension {
19  fn new(number: i64) -> Self { MuDimension(number) }
20  fn new_f64(number: f64) -> Self { MuDimension(kround(number)) }
21  fn value_of(self) -> i64 { self.0 }
22  fn register_type(&self) -> RegisterType { RegisterType::MuDimension }
23  fn unit(&self) -> Option<&'static str> { Some("mu") }
24  // XML attribute output is pt-typed by convention. Convert mu→pt via
25  // Perl `MuGlue::ptValue` two-step truncation so XMHint width attrs
26  // emit `1.66663pt` not `3.0mu` (and downstream lpadding/rpadding
27  // transferred from the XMHint width keep the pt unit).
28  fn to_attribute(&self) -> String {
29    let fs = crate::state::lookup_font()
30      .and_then(|f| f.get_size())
31      .unwrap_or(10.0);
32    let muwidth = (fs * UNITY_F64 / 18.0) as i64;
33    let pt_scaled = ((self.0 as f64 * muwidth as f64 / UNITY_F64).trunc()) as i64;
34    super::dimension::attribute_format(pt_scaled, Some("pt"))
35  }
36}
37
38impl fmt::Display for MuDimension {
39  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40    write!(f, "{}", fixedformat(self.0, self.unit()))
41  }
42}
43impl Object for MuDimension {}
44
45impl MuDimension {
46  pub fn new_spec(spec: &str) -> Self {
47    if let Some(cap) = MUDIM_SPEC_RE.captures(spec) {
48      // The numeric capture `-?\d*\.?\d*` can match empty (e.g. input
49      // "mu"); Perl's fixpoint coerces "" → 0 via numeric context, so
50      // unwrap_or(0.0) keeps parity.
51      let num: f64 = cap
52        .get(1)
53        .map_or("", |m| m.as_str())
54        .parse::<f64>()
55        .unwrap_or(0.0);
56      MuDimension(fixpoint(num, Some(UNITY_F64)))
57    } else {
58      // Perl parity: bad input coerces to 0.
59      MuDimension(kround(spec.parse::<f64>().unwrap_or(0.0)))
60    }
61  }
62}
63
64#[cfg(test)]
65mod tests {
66  use super::*;
67
68  #[test]
69  fn mudim_default_is_zero() {
70    assert_eq!(MuDimension::default().value_of(), 0);
71  }
72
73  #[test]
74  fn mudim_new_builds_value() {
75    assert_eq!(MuDimension::new(65536).value_of(), 65536);
76    assert_eq!(MuDimension::new(0).value_of(), 0);
77    assert_eq!(MuDimension::new(-100).value_of(), -100);
78  }
79
80  #[test]
81  fn mudim_new_f64_rounds_knuthian() {
82    assert_eq!(MuDimension::new_f64(0.0).value_of(), 0);
83    assert_eq!(MuDimension::new_f64(1.0).value_of(), 1);
84    assert_eq!(MuDimension::new_f64(-1.0).value_of(), -1);
85  }
86
87  #[test]
88  fn mudim_register_type_is_mudimension() {
89    assert_eq!(
90      MuDimension::default().register_type(),
91      RegisterType::MuDimension
92    );
93  }
94
95  #[test]
96  fn mudim_unit_is_mu() {
97    assert_eq!(MuDimension::default().unit(), Some("mu"));
98  }
99
100  #[test]
101  fn mudim_display_includes_mu_unit() {
102    let m = MuDimension::new(65536); // 1mu in scaled units
103    let out = format!("{m}");
104    assert!(out.ends_with("mu"), "got {out:?}");
105  }
106
107  #[test]
108  fn new_spec_parses_numeric_with_mu() {
109    // "1mu" parses as MuDimension(fixpoint(1.0, UNITY_F64)).
110    let m = MuDimension::new_spec("1mu");
111    assert_ne!(m.value_of(), 0, "1mu should not be zero");
112    // "0mu" is zero.
113    let m0 = MuDimension::new_spec("0mu");
114    assert_eq!(m0.value_of(), 0);
115  }
116
117  #[test]
118  fn new_spec_empty_numeric_part_is_zero() {
119    // "mu" (bare unit, no number) coerces to 0 via Perl-parity.
120    let m = MuDimension::new_spec("mu");
121    assert_eq!(m.value_of(), 0);
122  }
123
124  #[test]
125  fn new_spec_bad_input_is_zero() {
126    // Non-matching spec falls through to bare-number parse → 0 if that
127    // also fails.
128    let m = MuDimension::new_spec("not a number");
129    assert_eq!(m.value_of(), 0);
130  }
131
132  #[test]
133  fn mudim_equality() {
134    assert_eq!(MuDimension::new(100), MuDimension::new(100));
135    assert_ne!(MuDimension::new(100), MuDimension::new(101));
136  }
137}