Skip to main content

latex_rust/parser/
ast.rs

1//! Typed math AST produced by the parser.
2
3use crate::color::Color;
4use crate::dim::Dim;
5
6/// TeX math atom class (Appendix G).
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum AtomKind {
9    /// Ordinary (`x`, `1`, `\alpha` as a letter-like glyph).
10    Ord,
11    /// Large operator class.
12    Op,
13    /// Binary operator (`+`, `\times`).
14    Bin,
15    /// Relation (`=`, `\leq`).
16    Rel,
17    /// Opening delimiter.
18    Open,
19    /// Closing delimiter.
20    Close,
21    /// Punctuation (`,`).
22    Punct,
23    /// Inner (fraction-like).
24    Inner,
25}
26
27impl AtomKind {
28    fn gold(self) -> &'static str {
29        match self {
30            Self::Ord => "Ord",
31            Self::Op => "Op",
32            Self::Bin => "Bin",
33            Self::Rel => "Rel",
34            Self::Open => "Open",
35            Self::Close => "Close",
36            Self::Punct => "Punct",
37            Self::Inner => "Inner",
38        }
39    }
40}
41
42/// Accent or decoration applied to a nucleus.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum AccentKind {
45    /// `\hat`
46    Hat,
47    /// `\check`
48    Check,
49    /// `\breve`
50    Breve,
51    /// `\acute`
52    Acute,
53    /// `\grave`
54    Grave,
55    /// `\tilde`
56    Tilde,
57    /// `\bar`
58    Bar,
59    /// `\vec`
60    Vec,
61    /// `\dot`
62    Dot,
63    /// `\ddot`
64    Ddot,
65    /// `\dddot`
66    Dddot,
67    /// `\ddddot`
68    Ddddot,
69    /// `\widehat`
70    WideHat,
71    /// `\widetilde`
72    WideTilde,
73    /// `\overline`
74    Overline,
75    /// `\underline`
76    Underline,
77    /// `\overbrace`
78    Overbrace,
79    /// `\underbrace`
80    Underbrace,
81    /// `\overleftarrow`
82    Overleftarrow,
83    /// `\overrightarrow`
84    Overrightarrow,
85    /// `\overleftrightarrow`
86    Overleftrightarrow,
87    /// `\underleftarrow`
88    Underleftarrow,
89    /// `\underrightarrow`
90    Underrightarrow,
91    /// `\underleftrightarrow`
92    Underleftrightarrow,
93    /// `\cancel`
94    Cancel,
95    /// `\bcancel`
96    BCancel,
97    /// `\xcancel`
98    XCancel,
99    /// `\boxed`
100    Boxed,
101    /// `\mathring`
102    Ring,
103    /// `\not`
104    Not,
105}
106
107impl AccentKind {
108    pub(crate) fn gold(self) -> &'static str {
109        match self {
110            Self::Hat => "hat",
111            Self::Check => "check",
112            Self::Breve => "breve",
113            Self::Acute => "acute",
114            Self::Grave => "grave",
115            Self::Tilde => "tilde",
116            Self::Bar => "bar",
117            Self::Vec => "vec",
118            Self::Dot => "dot",
119            Self::Ddot => "ddot",
120            Self::Dddot => "dddot",
121            Self::Ddddot => "ddddot",
122            Self::WideHat => "widehat",
123            Self::WideTilde => "widetilde",
124            Self::Overline => "overline",
125            Self::Underline => "underline",
126            Self::Overbrace => "overbrace",
127            Self::Underbrace => "underbrace",
128            Self::Overleftarrow => "overleftarrow",
129            Self::Overrightarrow => "overrightarrow",
130            Self::Overleftrightarrow => "overleftrightarrow",
131            Self::Underleftarrow => "underleftarrow",
132            Self::Underrightarrow => "underrightarrow",
133            Self::Underleftrightarrow => "underleftrightarrow",
134            Self::Cancel => "cancel",
135            Self::BCancel => "bcancel",
136            Self::XCancel => "xcancel",
137            Self::Boxed => "boxed",
138            Self::Ring => "mathring",
139            Self::Not => "not",
140        }
141    }
142}
143
144/// Font / text style for a run of characters.
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub enum TextStyle {
147    /// `\mathrm`
148    Rm,
149    /// `\mathbf`
150    Bf,
151    /// `\mathit`
152    It,
153    /// `\mathsf`
154    Sf,
155    /// `\mathtt`
156    Tt,
157    /// `\mathbb`
158    Bb,
159    /// `\mathcal`
160    Cal,
161    /// `\mathfrak`
162    Frak,
163    /// `\mathscr`
164    Scr,
165    /// `\boldsymbol`
166    Boldsymbol,
167    /// `\pmb`
168    Pmb,
169    /// `\text`
170    Text,
171}
172
173impl TextStyle {
174    fn gold(self) -> &'static str {
175        match self {
176            Self::Rm => "rm",
177            Self::Bf => "bf",
178            Self::It => "it",
179            Self::Sf => "sf",
180            Self::Tt => "tt",
181            Self::Bb => "bb",
182            Self::Cal => "cal",
183            Self::Frak => "frak",
184            Self::Scr => "scr",
185            Self::Boldsymbol => "boldsymbol",
186            Self::Pmb => "pmb",
187            Self::Text => "text",
188        }
189    }
190}
191
192/// Horizontal skip.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub enum SpaceKind {
195    /// `\,`
196    Thin,
197    /// `\:` or `\>`
198    Medium,
199    /// `\;`
200    Thick,
201    /// `\!`
202    NegThin,
203    /// `\quad`
204    Quad,
205    /// `\qquad`
206    Qquad,
207    /// `\ ` (control space)
208    ControlSpace,
209    /// `\hspace{...}` in em.
210    Hspace(Dim),
211}
212
213/// Matrix / alignment environment.
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215pub enum MatrixStyle {
216    /// `{matrix}`
217    Matrix,
218    /// `{pmatrix}`
219    Pmatrix,
220    /// `{bmatrix}`
221    Bmatrix,
222    /// `{vmatrix}`
223    Vmatrix,
224    /// `{Vmatrix}`
225    VVmatrix,
226    /// `{Bmatrix}`
227    BBmatrix,
228    /// `{cases}`
229    Cases,
230    /// `{array}`
231    Array,
232    /// `{aligned}`
233    Aligned,
234    /// `{align}`
235    Align,
236    /// `{gather}`
237    Gather,
238    /// `{multline}`
239    Multline,
240    /// `{equation}`
241    Equation,
242    /// `{split}`
243    Split,
244}
245
246impl MatrixStyle {
247    fn gold(self) -> &'static str {
248        match self {
249            Self::Matrix => "matrix",
250            Self::Pmatrix => "pmatrix",
251            Self::Bmatrix => "bmatrix",
252            Self::Vmatrix => "vmatrix",
253            Self::VVmatrix => "Vmatrix",
254            Self::BBmatrix => "Bmatrix",
255            Self::Cases => "cases",
256            Self::Array => "array",
257            Self::Aligned => "aligned",
258            Self::Align => "align",
259            Self::Gather => "gather",
260            Self::Multline => "multline",
261            Self::Equation => "equation",
262            Self::Split => "split",
263        }
264    }
265
266    /// `align` / `gather` / `multline` / `equation` take display style and numbers.
267    #[must_use]
268    pub fn is_display_env(self) -> bool {
269        matches!(
270            self,
271            Self::Align | Self::Gather | Self::Multline | Self::Equation
272        )
273    }
274
275    /// Rows are numbered unless `\nonumber` / `\notag` / `\tag` says otherwise.
276    #[must_use]
277    pub fn numbers_rows(self) -> bool {
278        matches!(self, Self::Align | Self::Gather)
279    }
280
281    /// One equation number for the whole environment (last line for `multline`).
282    #[must_use]
283    pub fn numbers_once(self) -> bool {
284        matches!(self, Self::Equation | Self::Multline)
285    }
286}
287
288/// One column of an `{array}` preamble (`l`, `c`, `r`, `|`).
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub enum ColSpec {
291    /// `l`
292    Left,
293    /// `c`
294    Center,
295    /// `r`
296    Right,
297    /// `|`
298    VRule,
299}
300
301impl ColSpec {
302    fn gold(self) -> char {
303        match self {
304            Self::Left => 'l',
305            Self::Center => 'c',
306            Self::Right => 'r',
307            Self::VRule => '|',
308        }
309    }
310
311    /// True for `|`.
312    #[must_use]
313    pub fn is_rule(self) -> bool {
314        matches!(self, Self::VRule)
315    }
316}
317
318/// Per-row equation number in numbered environments.
319#[derive(Clone, Debug, PartialEq, Eq)]
320pub enum EqNumber {
321    /// Auto number when the environment numbers rows; none otherwise.
322    Default,
323    /// `\nonumber` / `\notag`
324    Suppress,
325    /// `\tag{...}` or `\tag*{...}`
326    Tag {
327        /// `\tag*` — no parentheses around the tag.
328        star: bool,
329        /// Tag body (text style at layout).
330        body: Box<MathNode>,
331    },
332}
333
334/// One row of a matrix / alignment environment.
335#[derive(Clone, Debug, PartialEq, Eq)]
336pub enum EnvRow {
337    /// Alignment cells (`&`-separated).
338    Cells {
339        /// Column entries.
340        cells: Vec<MathNode>,
341        /// Number / tag for this row.
342        number: EqNumber,
343        /// `\label{...}` keys bound to this row's number.
344        labels: Vec<String>,
345    },
346    /// `\hline`
347    Hline,
348    /// `\intertext{...}`
349    Intertext(Box<MathNode>),
350}
351
352impl EnvRow {
353    /// A data row with default numbering and no labels.
354    #[must_use]
355    pub fn cells(cells: Vec<MathNode>) -> Self {
356        Self::Cells {
357            cells,
358            number: EqNumber::Default,
359            labels: Vec::new(),
360        }
361    }
362
363    fn gold(&self) -> String {
364        match self {
365            Self::Hline => "(hline)".into(),
366            Self::Intertext(n) => format!("(intertext {})", n.gold()),
367            Self::Cells {
368                cells,
369                number,
370                labels,
371            } => {
372                let mut s = String::from("(");
373                for (i, c) in cells.iter().enumerate() {
374                    if i > 0 {
375                        s.push(' ');
376                    }
377                    s.push_str(&c.gold());
378                }
379                match number {
380                    EqNumber::Default => {}
381                    EqNumber::Suppress => s.push_str(" (nonumber)"),
382                    EqNumber::Tag { star: false, body } => {
383                        s.push_str(&format!(" (tag {})", body.gold()));
384                    }
385                    EqNumber::Tag { star: true, body } => {
386                        s.push_str(&format!(" (tagstar {})", body.gold()));
387                    }
388                }
389                for lab in labels {
390                    s.push_str(&format!(" (label {lab})"));
391                }
392                s.push(')');
393                s
394            }
395        }
396    }
397}
398
399/// Which integral glyph.
400#[derive(Clone, Copy, Debug, PartialEq, Eq)]
401pub enum IntegralKind {
402    /// `\int`
403    Int,
404    /// `\iint`
405    Iint,
406    /// `\iiint`
407    Iiint,
408    /// `\oint`
409    Oint,
410    /// `\oiint`
411    Oiint,
412}
413
414impl IntegralKind {
415    fn gold(self) -> &'static str {
416        match self {
417            Self::Int => "int",
418            Self::Iint => "iint",
419            Self::Iiint => "iiint",
420            Self::Oint => "oint",
421            Self::Oiint => "oiint",
422        }
423    }
424}
425
426/// `\phantom` / `\vphantom` / `\hphantom`.
427#[derive(Clone, Copy, Debug, PartialEq, Eq)]
428pub enum PhantomKind {
429    /// `\phantom`
430    Full,
431    /// `\vphantom`
432    Vertical,
433    /// `\hphantom`
434    Horizontal,
435}
436
437impl PhantomKind {
438    fn gold(self) -> &'static str {
439        match self {
440            Self::Full => "phantom",
441            Self::Vertical => "vphantom",
442            Self::Horizontal => "hphantom",
443        }
444    }
445}
446
447/// A `\left` / `\right` delimiter (or `.` for empty).
448#[derive(Clone, Debug, PartialEq, Eq)]
449pub enum Delimiter {
450    /// `\left.` / `\right.`
451    Empty,
452    /// Literal character (`(`, `[`, `|`).
453    Char(char),
454    /// Named delimiter (`langle`, `{` from `\{`).
455    Named(String),
456}
457
458impl Delimiter {
459    fn gold(&self) -> String {
460        match self {
461            Self::Empty => ".".into(),
462            Self::Char(c) => c.to_string(),
463            Self::Named(n) => {
464                if n == "{" || n == "}" || n == "|" {
465                    format!("\\{n}")
466                } else {
467                    n.clone()
468                }
469            }
470        }
471    }
472}
473
474/// `\big` / `\Big` / `\bigg` / `\Bigg` (and `l`/`r`/`m` siblings).
475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
476pub enum DelimSize {
477    /// `\big` — about 1.2 em.
478    Big,
479    /// `\Big` — about 1.8 em.
480    Big2,
481    /// `\bigg` — about 2.4 em.
482    Bigg,
483    /// `\Bigg` — about 3.0 em.
484    Bigg2,
485}
486
487impl DelimSize {
488    fn gold(self) -> &'static str {
489        match self {
490            Self::Big => "big",
491            Self::Big2 => "Big",
492            Self::Bigg => "bigg",
493            Self::Bigg2 => "Bigg",
494        }
495    }
496
497    /// Map a control sequence to a size. `None` if it is not a `\big` family command.
498    #[must_use]
499    pub fn from_command(name: &str) -> Option<Self> {
500        match name {
501            "big" | "bigl" | "bigr" | "bigm" => Some(Self::Big),
502            "Big" | "Bigl" | "Bigr" | "Bigm" => Some(Self::Big2),
503            "bigg" | "biggl" | "biggr" | "biggm" => Some(Self::Bigg),
504            "Bigg" | "Biggl" | "Biggr" | "Biggm" => Some(Self::Bigg2),
505            _ => None,
506        }
507    }
508
509    /// Open / Close / Rel from the `l` / `r` / `m` suffix; `None` for unsuffixed `\big`.
510    #[must_use]
511    pub fn class_from_command(name: &str) -> Option<AtomKind> {
512        if name.ends_with('l') {
513            Some(AtomKind::Open)
514        } else if name.ends_with('r') {
515            Some(AtomKind::Close)
516        } else if name.ends_with('m') {
517            Some(AtomKind::Rel)
518        } else {
519            None
520        }
521    }
522}
523
524/// Typed math-mode syntax tree.
525///
526/// Produced by [`crate::parse()`]. [`MathNode::gold`] is the gold-stable debug form.
527///
528/// # Examples
529///
530/// ```
531/// use latex_rust::parse;
532///
533/// let n = parse("x^2").unwrap();
534/// assert!(n.gold().contains("sup"));
535/// ```
536#[derive(Clone, Debug, PartialEq, Eq)]
537pub enum MathNode {
538    /// Single character with a TeX atom class.
539    Atom(char, AtomKind),
540    /// `\frac` / `\dfrac` / `\tfrac` / `\cfrac` / `{a \over b}`.
541    Fraction(Box<MathNode>, Box<MathNode>),
542    /// `\sqrt` / `\sqrt[n]`. Index `None` is a square root.
543    Radical(Option<Box<MathNode>>, Box<MathNode>),
544    /// `x^{}`
545    Superscript(Box<MathNode>, Box<MathNode>),
546    /// `x_{}`
547    Subscript(Box<MathNode>, Box<MathNode>),
548    /// `x_{}^{}`
549    SubSup(Box<MathNode>, Box<MathNode>, Box<MathNode>),
550    /// `\left ... \right`
551    Delimited(Delimiter, Box<MathNode>, Delimiter),
552    /// `\big` / `\Big` / `\bigg` / `\Bigg` (and `l`/`r`/`m` forms).
553    SizedDelim(Delimiter, DelimSize, AtomKind),
554    /// Horizontal list of nodes.
555    Row(Vec<MathNode>),
556    /// Matrix or multiline environment. `colspec` is the `{array}` preamble (empty otherwise).
557    Matrix(MatrixStyle, Vec<ColSpec>, Vec<EnvRow>),
558    /// `\substack{...}` — stacked script-style lines.
559    Substack(Vec<MathNode>),
560    /// `\ref{key}`
561    Ref(String),
562    /// `\tag{...}` / `\tag*{...}` (peeled into [`EnvRow`] when inside an environment).
563    Tag {
564        /// `\tag*`
565        star: bool,
566        /// Tag body.
567        body: Box<MathNode>,
568    },
569    /// `\label{key}`
570    Label(String),
571    /// `\nonumber` / `\notag`
572    NoNumber,
573    /// `\hline` (peeled into [`EnvRow::Hline`] in environments).
574    Hline,
575    /// `\intertext{...}`
576    Intertext(Box<MathNode>),
577    /// `\sum` with optional lower / upper limits.
578    Sum(Option<Box<MathNode>>, Option<Box<MathNode>>),
579    /// `\int` family with optional limits.
580    Integral(IntegralKind, Option<Box<MathNode>>, Option<Box<MathNode>>),
581    /// `\prod` with optional limits.
582    Product(Option<Box<MathNode>>, Option<Box<MathNode>>),
583    /// `\lim` with optional subscript.
584    Limit(Option<Box<MathNode>>),
585    /// `\overset` / `\underset` / `\stackrel` (base, over, under).
586    OverUnder(Box<MathNode>, Option<Box<MathNode>>, Option<Box<MathNode>>),
587    /// Accent or decoration on a nucleus.
588    Accent(Box<MathNode>, AccentKind),
589    /// `\cancelto{value}{expr}`
590    CancelTo(Box<MathNode>, Box<MathNode>),
591    /// Styled character run (`\mathrm`, `\text`, …).
592    Text(String, TextStyle),
593    /// Explicit math skip.
594    Space(SpaceKind),
595    /// Named operator (`\sin`). The flag is `\limits` vs `\nolimits`.
596    Operator(String, bool),
597    /// Named glyph (`\alpha`, `\times`). The string is the control-sequence name.
598    Symbol(String),
599    /// `\color` applied to a following math list.
600    Color(Color, Box<MathNode>),
601    /// `\textcolor{...}{...}`
602    TextColor(Color, Box<MathNode>),
603    /// `\colorbox{...}{...}`
604    ColorBox(Color, Box<MathNode>),
605    /// `\fcolorbox{border}{fill}{body}`
606    FColorBox(Color, Color, Box<MathNode>),
607    /// Vertical strut (height, depth) in em.
608    Strut(Dim, Dim),
609    /// `\phantom` family.
610    Phantom(PhantomKind, Box<MathNode>),
611}
612
613impl MathNode {
614    /// Gold-stable S-expression. One space between items; no trailing space.
615    #[must_use]
616    pub fn gold(&self) -> String {
617        match self {
618            Self::Atom(c, k) => format!("(atom {} {})", k.gold(), quote_atom(*c)),
619            Self::Fraction(n, d) => format!("(frac {} {})", n.gold(), d.gold()),
620            Self::Radical(None, r) => format!("(sqrt {})", r.gold()),
621            Self::Radical(Some(i), r) => format!("(sqrtn {} {})", i.gold(), r.gold()),
622            Self::Superscript(b, e) => format!("(sup {} {})", b.gold(), e.gold()),
623            Self::Subscript(b, s) => format!("(sub {} {})", b.gold(), s.gold()),
624            Self::SubSup(b, s, e) => format!("(subsup {} {} {})", b.gold(), s.gold(), e.gold()),
625            Self::Delimited(l, b, r) => {
626                format!("(delim {} {} {})", l.gold(), b.gold(), r.gold())
627            }
628            Self::SizedDelim(d, sz, k) => {
629                format!("(big {} {} {})", sz.gold(), k.gold(), quote_delim(d))
630            }
631            Self::Row(items) => {
632                if items.is_empty() {
633                    "(row)".into()
634                } else {
635                    let mut s = String::from("(row");
636                    for it in items {
637                        s.push(' ');
638                        s.push_str(&it.gold());
639                    }
640                    s.push(')');
641                    s
642                }
643            }
644            Self::Matrix(style, spec, rows) => {
645                let mut s = format!("(matrix {}", style.gold());
646                if !spec.is_empty() {
647                    s.push(' ');
648                    for c in spec {
649                        s.push(c.gold());
650                    }
651                }
652                for row in rows {
653                    s.push(' ');
654                    s.push_str(&row.gold());
655                }
656                s.push(')');
657                s
658            }
659            Self::Substack(lines) => {
660                let mut s = String::from("(substack");
661                for ln in lines {
662                    s.push(' ');
663                    s.push_str(&ln.gold());
664                }
665                s.push(')');
666                s
667            }
668            Self::Ref(k) => format!("(ref {k})"),
669            Self::Tag { star: false, body } => format!("(tag {})", body.gold()),
670            Self::Tag { star: true, body } => format!("(tagstar {})", body.gold()),
671            Self::Label(k) => format!("(label {k})"),
672            Self::NoNumber => "(nonumber)".into(),
673            Self::Hline => "(hline)".into(),
674            Self::Intertext(n) => format!("(intertext {})", n.gold()),
675            Self::Sum(lo, hi) => format!("(sum {} {})", opt(lo), opt(hi)),
676            Self::Integral(k, lo, hi) => {
677                format!("({} {} {})", k.gold(), opt(lo), opt(hi))
678            }
679            Self::Product(lo, hi) => format!("(prod {} {})", opt(lo), opt(hi)),
680            Self::Limit(lo) => format!("(lim {})", opt(lo)),
681            Self::OverUnder(b, over, under) => {
682                format!("(overunder {} {} {})", b.gold(), opt(over), opt(under))
683            }
684            Self::Accent(b, a) => format!("(accent {} {})", a.gold(), b.gold()),
685            Self::CancelTo(v, e) => format!("(cancelto {} {})", v.gold(), e.gold()),
686            Self::Text(t, st) => format!("(text {} {})", st.gold(), quote_text(t)),
687            Self::Space(SpaceKind::Thin) => "(space thin)".into(),
688            Self::Space(SpaceKind::Medium) => "(space medium)".into(),
689            Self::Space(SpaceKind::Thick) => "(space thick)".into(),
690            Self::Space(SpaceKind::NegThin) => "(space negthin)".into(),
691            Self::Space(SpaceKind::Quad) => "(space quad)".into(),
692            Self::Space(SpaceKind::Qquad) => "(space qquad)".into(),
693            Self::Space(SpaceKind::ControlSpace) => "(space control)".into(),
694            Self::Space(SpaceKind::Hspace(d)) => format!("(space hspace {})", dim_gold(d)),
695            Self::Operator(name, false) => format!("(op {name})"),
696            Self::Operator(name, true) => format!("(op {name} limits)"),
697            Self::Symbol(name) => format!("(symbol {name})"),
698            Self::Color(c, b) => format!("(color {} {})", c.css_hex(), b.gold()),
699            Self::TextColor(c, b) => format!("(textcolor {} {})", c.css_hex(), b.gold()),
700            Self::ColorBox(c, b) => format!("(colorbox {} {})", c.css_hex(), b.gold()),
701            Self::FColorBox(border, fill, b) => {
702                format!(
703                    "(fcolorbox {} {} {})",
704                    border.css_hex(),
705                    fill.css_hex(),
706                    b.gold()
707                )
708            }
709            Self::Strut(h, d) => format!("(strut {} {})", dim_gold(h), dim_gold(d)),
710            Self::Phantom(k, b) => format!("({} {})", k.gold(), b.gold()),
711        }
712    }
713}
714
715fn opt(n: &Option<Box<MathNode>>) -> String {
716    match n {
717        None => "_".into(),
718        Some(x) => x.gold(),
719    }
720}
721
722fn quote_delim(d: &Delimiter) -> String {
723    match d {
724        Delimiter::Empty => ".".into(),
725        Delimiter::Char(c) => quote_atom(*c),
726        Delimiter::Named(n) => n.clone(),
727    }
728}
729
730fn quote_atom(c: char) -> String {
731    match c {
732        '"' => "'\"'".into(),
733        '\'' => "\"'\"".into(),
734        _ => format!("\"{c}\""),
735    }
736}
737
738fn quote_text(t: &str) -> String {
739    format!("\"{}\"", t.replace('\\', "\\\\").replace('"', "\\\""))
740}
741
742fn dim_gold(d: &Dim) -> String {
743    const RATIOS: [(i64, i64); 10] = [
744        (0, 1),
745        (1, 1),
746        (2, 1),
747        (1, 2),
748        (1, 18),
749        (2, 18),
750        (3, 18),
751        (7, 10),
752        (3, 10),
753        (1, 10),
754    ];
755    for (n, den) in RATIOS {
756        if d.eq_dim(&Dim::ratio(n, den)) {
757            if den == 1 {
758                return n.to_string();
759            }
760            return format!("{n}/{den}");
761        }
762    }
763    for i in -64i64..65 {
764        if d.eq_dim(&Dim::from_i64(i)) {
765            return i.to_string();
766        }
767    }
768    d.to_dec_string()
769}