Skip to main content

latex_rust/
dim.rs

1//! Layout dimension: exact rational (`num / den`).
2//!
3//! TeX layout is ratios of integers (font units, mu = 1/18 em, style scales).
4//! Hardware `f32` / `f64` never appear as calculation terminals.
5
6use core::cmp::Ordering;
7use core::fmt;
8use core::ops::{Add, Div, Mul, Neg, Sub};
9
10use crate::error::Error;
11
12/// Kept in the public API. Layout values are exact rationals, not rounded bits.
13pub const DIM_PREC: usize = 256;
14
15fn gcd_u(mut a: u128, mut b: u128) -> u128 {
16    while b != 0 {
17        let t = a % b;
18        a = b;
19        b = t;
20    }
21    a
22}
23
24/// TeX-style dimension: width, height, depth, italic correction, mu.
25///
26/// One unit is one em at the current math style unless a method says otherwise.
27///
28/// # Examples
29///
30/// ```
31/// use latex_rust::Dim;
32///
33/// let half = Dim::ratio(1, 2);
34/// assert!(half.eq_dim(&(&Dim::one() / &Dim::from_i64(2))));
35/// assert!(!Dim::zero().eq_dim(&Dim::one()));
36/// ```
37#[derive(Clone, Debug)]
38pub struct Dim {
39    num: i128,
40    den: i128,
41    nan: bool,
42}
43
44impl Dim {
45    fn raw(num: i128, den: i128) -> Self {
46        if den == 0 {
47            return Self::nan();
48        }
49        if num == 0 {
50            return Self {
51                num: 0,
52                den: 1,
53                nan: false,
54            };
55        }
56        let g = gcd_u(num.unsigned_abs(), den.unsigned_abs());
57        let g = i128::try_from(g).unwrap_or(1);
58        let mut n = num / g;
59        let mut d = den / g;
60        if d < 0 {
61            n = -n;
62            d = -d;
63        }
64        Self {
65            num: n,
66            den: d,
67            nan: false,
68        }
69    }
70
71    fn nan() -> Self {
72        Self {
73            num: 0,
74            den: 1,
75            nan: true,
76        }
77    }
78
79    fn binop(
80        a: &Self,
81        b: &Self,
82        f: impl Fn(i128, i128, i128, i128) -> Option<(i128, i128)>,
83    ) -> Self {
84        if a.nan || b.nan {
85            return Self::nan();
86        }
87        match f(a.num, a.den, b.num, b.den) {
88            Some((n, d)) => Self::raw(n, d),
89            None => Self::nan(),
90        }
91    }
92
93    /// Zero em.
94    #[must_use]
95    pub fn zero() -> Self {
96        Self::raw(0, 1)
97    }
98
99    /// One em.
100    #[must_use]
101    pub fn one() -> Self {
102        Self::raw(1, 1)
103    }
104
105    /// Integer em count.
106    #[must_use]
107    pub fn from_i64(v: i64) -> Self {
108        Self::raw(i128::from(v), 1)
109    }
110
111    /// Exact rational `num / den` em. `den == 0` yields NaN.
112    #[must_use]
113    pub fn ratio(num: i64, den: i64) -> Self {
114        Self::raw(i128::from(num), i128::from(den))
115    }
116
117    /// Parse a decimal string (optional sign, fraction, `e` exponent).
118    #[must_use]
119    pub fn parse(s: &str) -> Self {
120        let t = s.trim();
121        if t.is_empty() || t.eq_ignore_ascii_case("nan") {
122            return Self::nan();
123        }
124        let mut rest = t;
125        let neg = if let Some(r) = rest.strip_prefix('-') {
126            rest = r;
127            true
128        } else if let Some(r) = rest.strip_prefix('+') {
129            rest = r;
130            false
131        } else {
132            false
133        };
134        let (mant, exp_s) = match rest.find(['e', 'E']) {
135            Some(i) => (&rest[..i], Some(&rest[i + 1..])),
136            None => (rest, None),
137        };
138        let (ip, fp) = match mant.find('.') {
139            Some(i) => (&mant[..i], &mant[i + 1..]),
140            None => (mant, ""),
141        };
142        if ip.is_empty() && fp.is_empty() {
143            return Self::nan();
144        }
145        if !ip.chars().all(|c| c.is_ascii_digit()) || !fp.chars().all(|c| c.is_ascii_digit()) {
146            return Self::nan();
147        }
148        let mut num: i128 = 0;
149        for c in ip.bytes().chain(fp.bytes()) {
150            let d = i128::from(c - b'0');
151            num = match num.checked_mul(10).and_then(|n| n.checked_add(d)) {
152                Some(n) => n,
153                None => return Self::nan(),
154            };
155        }
156        let mut den = 1i128;
157        for _ in 0..fp.len() {
158            den = match den.checked_mul(10) {
159                Some(d) => d,
160                None => return Self::nan(),
161            };
162        }
163        if let Some(es) = exp_s {
164            let e: i32 = match es.parse() {
165                Ok(v) => v,
166                Err(_) => return Self::nan(),
167            };
168            if e > 0 {
169                for _ in 0..e {
170                    num = match num.checked_mul(10) {
171                        Some(n) => n,
172                        None => return Self::nan(),
173                    };
174                }
175            } else {
176                for _ in 0..(-e) {
177                    den = match den.checked_mul(10) {
178                        Some(d) => d,
179                        None => return Self::nan(),
180                    };
181                }
182            }
183        }
184        if neg {
185            num = -num;
186        }
187        Self::raw(num, den)
188    }
189
190    /// Convert integer font units to em: `units / units_per_em`.
191    #[must_use]
192    pub fn from_font_units(units: i64, units_per_em: u16) -> Self {
193        Self::raw(i128::from(units), i128::from(units_per_em))
194    }
195
196    /// One math unit (mu). TeX: `18 mu = 1 em`.
197    #[must_use]
198    pub fn mu() -> Self {
199        Self::ratio(1, 18)
200    }
201
202    /// Convert this em value to mu (`* 18`).
203    #[must_use]
204    pub fn to_mu(&self) -> Self {
205        self.clone() * Self::from_i64(18)
206    }
207
208    /// Convert a mu value to em (`/ 18`).
209    #[must_use]
210    pub fn from_mu(mu: &Self) -> Self {
211        mu.clone() / Self::from_i64(18)
212    }
213
214    /// Absolute value.
215    #[must_use]
216    pub fn abs(&self) -> Self {
217        if self.nan {
218            return Self::nan();
219        }
220        if self.num == i128::MIN {
221            return Self::nan();
222        }
223        Self::raw(self.num.abs(), self.den)
224    }
225
226    /// Maximum of two dimensions.
227    #[must_use]
228    pub fn max(&self, other: &Self) -> Self {
229        match self.cmp(other) {
230            Some(Ordering::Less) => other.clone(),
231            _ => self.clone(),
232        }
233    }
234
235    /// Minimum of two dimensions.
236    #[must_use]
237    pub fn min(&self, other: &Self) -> Self {
238        match self.cmp(other) {
239            Some(Ordering::Greater) => other.clone(),
240            _ => self.clone(),
241        }
242    }
243
244    /// `max(self, 0)`.
245    #[must_use]
246    pub fn clamp_nonneg(&self) -> Self {
247        self.max(&Self::zero())
248    }
249
250    /// True when the value is NaN.
251    #[must_use]
252    pub fn is_nan(&self) -> bool {
253        self.nan
254    }
255
256    /// True when the value compares equal to zero.
257    #[must_use]
258    pub fn is_zero(&self) -> bool {
259        !self.nan && self.num == 0
260    }
261
262    /// Decimal string (exact terminating expansion, or scientific).
263    #[must_use]
264    pub fn to_dec_string(&self) -> String {
265        if self.nan {
266            return "NaN".into();
267        }
268        format_rational(self.num, self.den)
269    }
270
271    /// Compact decimal for SVG attributes.
272    #[must_use]
273    pub fn to_svg_string(&self) -> String {
274        self.to_dec_string()
275    }
276
277    /// Layout dimension from an IEEE-754 binary32 bit pattern (outline boundary).
278    #[must_use]
279    pub fn from_ieee32_bits(bits: u32) -> Self {
280        let sign = (bits >> 31) != 0;
281        let exp = (bits >> 23) & 0xff;
282        let frac = bits & 0x7f_ffff;
283        if exp == 255 {
284            return Self::nan();
285        }
286        let (num, den, sh) = if exp == 0 {
287            if frac == 0 {
288                return Self::zero();
289            }
290            (i128::from(frac), 1i128, 149i32)
291        } else {
292            (i128::from(frac) + (1i128 << 23), 1i128, 150i32 - exp as i32)
293        };
294        let mut n = num;
295        let mut d = den;
296        if sh >= 0 {
297            for _ in 0..sh {
298                d = match d.checked_mul(2) {
299                    Some(v) => v,
300                    None => return Self::nan(),
301                };
302            }
303        } else {
304            for _ in 0..(-sh) {
305                n = match n.checked_mul(2) {
306                    Some(v) => v,
307                    None => return Self::nan(),
308                };
309            }
310        }
311        if sign {
312            n = -n;
313        }
314        Self::raw(n, d)
315    }
316
317    /// Round to IEEE-754 binary32 bits for PNG / raster emission only.
318    #[must_use]
319    pub fn to_ieee32_bits(&self) -> u32 {
320        if self.nan {
321            return 0x7fc0_0000;
322        }
323        if self.num == 0 {
324            return 0;
325        }
326        let sign = if self.num < 0 { 1u32 << 31 } else { 0 };
327        let n = self.num.unsigned_abs();
328        let d = self.den.unsigned_abs();
329        // Find e such that 2^e <= n/d < 2^{e+1}.
330        let mut e: i32 = 0;
331        // Compare n/d vs 1: n ? d
332        if n < d {
333            let mut t = n;
334            while t < d {
335                t = match t.checked_mul(2) {
336                    Some(v) => v,
337                    None => break,
338                };
339                e -= 1;
340                if e < -149 {
341                    return sign;
342                }
343            }
344        } else {
345            let mut t = d;
346            while t <= n / 2 {
347                t = match t.checked_mul(2) {
348                    Some(v) => v,
349                    None => break,
350                };
351                e += 1;
352                if e > 127 {
353                    return sign | 0x7f80_0000;
354                }
355            }
356        }
357        // mantissa: floor((n/d) / 2^(e-23)) = floor(n * 2^(23-e) / d)
358        let mut shift = 23i32 - e;
359        let mut num = n;
360        let den = d;
361        while shift > 0 {
362            num = match num.checked_mul(2) {
363                Some(v) => v,
364                None => return sign | 0x7f80_0000,
365            };
366            shift -= 1;
367        }
368        while shift < 0 {
369            num /= 2;
370            shift += 1;
371        }
372        let mut mant = num / den;
373        let rem = num % den;
374        // round to nearest even
375        if rem * 2 > den || (rem * 2 == den && mant & 1 == 1) {
376            mant += 1;
377        }
378        if mant >= (1u128 << 24) {
379            mant >>= 1;
380            e += 1;
381        }
382        if e > 127 {
383            return sign | 0x7f80_0000;
384        }
385        if e < -126 {
386            // subnormal
387            let sub_shift = -126 - e;
388            if sub_shift >= 24 {
389                return sign;
390            }
391            mant >>= sub_shift as u32;
392            let frac = (mant as u32) & 0x7f_ffff;
393            return sign | frac;
394        }
395        let biased = (e + 127) as u32;
396        let frac = (mant as u32) & 0x7f_ffff;
397        sign | (biased << 23) | frac
398    }
399
400    /// Largest `u32` that is not greater than `self`. Negative and NaN fail.
401    pub fn floor_to_u32(&self) -> Result<u32, Error> {
402        if self.is_nan() {
403            return Err(Error::InvalidOption {
404                what: "dimension is NaN".into(),
405            });
406        }
407        if matches!(self.cmp(&Self::zero()), Some(Ordering::Less)) {
408            return Err(Error::InvalidOption {
409                what: "negative dimension".into(),
410            });
411        }
412        const MAX: u32 = 1 << 20;
413        if matches!(
414            self.cmp(&Self::from_i64(i64::from(MAX))),
415            Some(Ordering::Greater) | Some(Ordering::Equal)
416        ) {
417            return Err(Error::InvalidOption {
418                what: "dimension exceeds raster limit".into(),
419            });
420        }
421        let q = (self.num / self.den) as u32;
422        Ok(q)
423    }
424
425    /// Smallest `u32` that is not less than `self`.
426    pub fn ceil_to_u32(&self) -> Result<u32, Error> {
427        let floor = self.floor_to_u32()?;
428        if self.eq_dim(&Self::from_i64(i64::from(floor))) {
429            Ok(floor)
430        } else {
431            floor.checked_add(1).ok_or_else(|| Error::InvalidOption {
432                what: "dimension overflow".into(),
433            })
434        }
435    }
436
437    /// Compare two dimensions. `None` if either is NaN.
438    #[must_use]
439    #[allow(clippy::should_implement_trait)]
440    pub fn cmp(&self, other: &Self) -> Option<Ordering> {
441        if self.nan || other.nan {
442            return None;
443        }
444        // n1/d1 vs n2/d2 => n1*d2 vs n2*d1
445        let left = self.num.checked_mul(other.den)?;
446        let right = other.num.checked_mul(self.den)?;
447        Some(left.cmp(&right))
448    }
449
450    /// True when `self` and `other` compare equal.
451    #[must_use]
452    pub fn eq_dim(&self, other: &Self) -> bool {
453        matches!(self.cmp(other), Some(Ordering::Equal))
454    }
455}
456
457fn format_rational(num: i128, den: i128) -> String {
458    if num == 0 {
459        return "0.0".into();
460    }
461    let neg = num < 0;
462    let mut n = num.unsigned_abs();
463    let d = den.unsigned_abs();
464    let ip = n / d;
465    n %= d;
466    let mut s = String::new();
467    if neg {
468        s.push('-');
469    }
470    if n == 0 {
471        s.push_str(&ip.to_string());
472        s.push_str(".0");
473        return s;
474    }
475    s.push_str(&ip.to_string());
476    s.push('.');
477    // Long division, cap 24 digits (plenty for font-unit rationals).
478    for _ in 0..24 {
479        if n == 0 {
480            break;
481        }
482        n *= 10;
483        let digit = n / d;
484        s.push(char::from(b'0' + digit as u8));
485        n %= d;
486    }
487    // Trim trailing zeros but keep one fractional digit.
488    while s.ends_with('0') && !s.ends_with(".0") {
489        s.pop();
490    }
491    s
492}
493
494impl PartialEq for Dim {
495    fn eq(&self, other: &Self) -> bool {
496        self.eq_dim(other)
497    }
498}
499
500impl Eq for Dim {}
501
502impl PartialOrd for Dim {
503    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
504        self.cmp(other)
505    }
506}
507
508impl fmt::Display for Dim {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        f.write_str(&self.to_dec_string())
511    }
512}
513
514impl Neg for Dim {
515    type Output = Self;
516
517    fn neg(self) -> Self {
518        if self.nan {
519            return Self::nan();
520        }
521        Self::raw(-self.num, self.den)
522    }
523}
524
525fn add_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
526    let ad = a.checked_mul(d)?;
527    let cb = c.checked_mul(b)?;
528    let n = ad.checked_add(cb)?;
529    let den = b.checked_mul(d)?;
530    Some((n, den))
531}
532
533fn sub_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
534    let ad = a.checked_mul(d)?;
535    let cb = c.checked_mul(b)?;
536    let n = ad.checked_sub(cb)?;
537    let den = b.checked_mul(d)?;
538    Some((n, den))
539}
540
541fn mul_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
542    let n = a.checked_mul(c)?;
543    let den = b.checked_mul(d)?;
544    Some((n, den))
545}
546
547fn div_r(a: i128, b: i128, c: i128, d: i128) -> Option<(i128, i128)> {
548    if c == 0 {
549        return None;
550    }
551    let n = a.checked_mul(d)?;
552    let den = b.checked_mul(c)?;
553    Some((n, den))
554}
555
556macro_rules! impl_dim_op {
557    ($Trait:ident, $method:ident, $helper:ident) => {
558        impl $Trait for Dim {
559            type Output = Dim;
560            fn $method(self, rhs: Dim) -> Dim {
561                Dim::binop(&self, &rhs, $helper)
562            }
563        }
564        impl $Trait<&Dim> for Dim {
565            type Output = Dim;
566            fn $method(self, rhs: &Dim) -> Dim {
567                Dim::binop(&self, rhs, $helper)
568            }
569        }
570        impl $Trait<Dim> for &Dim {
571            type Output = Dim;
572            fn $method(self, rhs: Dim) -> Dim {
573                Dim::binop(self, &rhs, $helper)
574            }
575        }
576        impl $Trait for &Dim {
577            type Output = Dim;
578            fn $method(self, rhs: &Dim) -> Dim {
579                Dim::binop(self, rhs, $helper)
580            }
581        }
582    };
583}
584
585impl_dim_op!(Add, add, add_r);
586impl_dim_op!(Sub, sub, sub_r);
587impl_dim_op!(Mul, mul, mul_r);
588impl_dim_op!(Div, div, div_r);
589
590#[cfg(test)]
591mod tests {
592    use super::Dim;
593
594    #[test]
595    fn half_and_font_units() {
596        assert!(Dim::ratio(1, 2).eq_dim(&(&Dim::one() / &Dim::from_i64(2))));
597        let w = Dim::from_font_units(479, 1000);
598        assert_eq!(w.to_dec_string(), "0.479");
599        assert_eq!((Dim::from_i64(12) * w).to_dec_string(), "5.748");
600    }
601
602    #[test]
603    fn ieee32_one() {
604        let one = Dim::from_ieee32_bits(0x3f80_0000);
605        assert!(one.eq_dim(&Dim::one()));
606        assert_eq!(Dim::one().to_ieee32_bits(), 0x3f80_0000);
607    }
608}