Skip to main content

yui_core/util/
format.rs

1//! Math-flavored string formatting helpers.
2
3use std::fmt::Display;
4use itertools::Itertools;
5use num_traits::ToPrimitive;
6use crate::ext::IntoDigits;
7
8/// Wrap `s` in parentheses if its `Display` form contains a space; otherwise return as-is.
9pub fn paren_expr<S>(s: S) -> String
10where S: Display {
11    let s = s.to_string();
12    if s.contains(' ') {
13        format!("({s})")
14    } else {
15        s
16    }
17}
18
19/// Format a sequence of `(generator, coefficient)` pairs as a sum
20/// `r₁·x₁ + r₂·x₂ + …`, with sign flips for negative coefficients and
21/// `1`/`-1` coefficients elided. Returns `"0"` if the iterator is empty.
22pub fn lc<X, R, S>(mut terms: S) -> String
23where
24    X: Display,
25    R: Display,
26    S: Iterator<Item = (X, R)>
27{
28    let mut res: Vec<String> = vec![];
29
30    if let Some((x, r)) = terms.next() {
31        let r = paren_expr(r);
32        let x = x.to_string();
33
34        let term = if r == "1" {
35            x
36        } else if r == "-1" {
37            format!("-{x}")
38        } else if x == "1" {
39            format!("{r}")
40        } else {
41            format!("{r}{x}")
42        };
43
44        res.push(term)
45    };
46
47    for (x, r) in terms {
48        let r = paren_expr(r);
49        let x = x.to_string();
50
51        let (op, r) = if let Some(r) = r.strip_prefix('-') {
52            ("-", r.to_owned())
53        } else {
54            ("+", r.to_owned())
55        };
56
57        let term = if r == "1" {
58            x
59        } else if x == "1" {
60            r.to_string()
61        } else {
62            format!("{r}{x}")
63        };
64
65        res.push(op.to_string());
66        res.push(term);
67    }
68
69    if res.is_empty() {
70        "0".to_string()
71    } else {
72        res.join(" ")
73    }
74}
75
76/// Render an integer as Unicode subscript digits (`₀..₉`, with `₋` for negatives).
77pub fn subscript<I>(i: I) -> String
78where I: ToPrimitive {
79    let i = i.to_isize().unwrap();
80
81    if i == 0 {
82        return '\u{2080}'.into()
83    }
84
85    let (init, i) = if i > 0 {
86        (String::new(), i as usize)
87    } else {
88        ('\u{208B}'.into(), -i as usize)
89    };
90
91    i.into_digits().fold(init, |mut res, d| {
92        let c = char::from_u32( ('\u{2080}' as u32) + (d as u32) ).unwrap();
93        res.push(c);
94        res
95    })
96}
97
98/// Render an integer as Unicode superscript digits (`⁰..⁹`, with `⁻` for negatives).
99pub fn superscript<I>(i: I) -> String
100where I: ToPrimitive {
101    let i = i.to_isize().unwrap();
102
103    if i == 0 {
104        return '\u{2070}'.into()
105    }
106
107    let (init, i) = if i > 0 {
108        (String::new(), i as usize)
109    } else {
110        ('\u{207B}'.into(), -i as usize)
111    };
112
113    i.into_digits().fold(init, |mut res, d| {
114        let c = match d {
115            1 => '\u{00B9}',
116            2 => '\u{00B2}',
117            3 => '\u{00B3}',
118            _ => char::from_u32(('\u{2070}' as u32) + (d as u32)).unwrap()
119        };
120        res.push(c);
121        res
122    })
123}
124
125/// Render a 2D table with the given header label, row keys, column keys,
126/// and a function producing each cell entry. Backed by `prettytable`.
127pub fn table<S, I, J, I1, I2, D, F>(head: S, rows: I1, cols: I2, entry: F) -> String
128where
129    S: Display,
130    I: Display,
131    J: Display,
132    I1: IntoIterator<Item = I>,
133    I2: IntoIterator<Item = J>,
134    D: Display,
135    F: Fn(&I, &J) -> D
136{
137    use prettytable::*;
138
139    let rows = rows.into_iter().collect_vec();
140    let cols = cols.into_iter().collect_vec();
141
142    fn row<I>(head: String, cols: I) -> Row
143    where I: Iterator<Item = String> {
144        let mut cells = vec![Cell::new(head.as_str())];
145        cells.extend(cols.map(|str| Cell::new(str.as_str())));
146        Row::new(cells)
147    }
148
149    let mut table = Table::new();
150
151    table.set_format(*format::consts::FORMAT_CLEAN);
152    table.set_titles(row(
153        head.to_string(),
154        cols.iter().map(|j| j.to_string() )
155    ));
156
157    for i in rows.iter() {
158        table.add_row(row(
159            i.to_string(),
160            cols.iter().map(|j| format!("{}", entry(i, j)))
161        ));
162    }
163
164    table.to_string()
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_subscript() {
173        assert_eq!(subscript(0), "₀");
174        assert_eq!(subscript(1234567890), "₁₂₃₄₅₆₇₈₉₀");
175        assert_eq!(subscript(-1234567890), "₋₁₂₃₄₅₆₇₈₉₀");
176    }
177
178    #[test]
179    fn test_superscript() {
180        assert_eq!(superscript(0), "⁰");
181        assert_eq!(superscript(1234567890), "¹²³⁴⁵⁶⁷⁸⁹⁰");
182        assert_eq!(superscript(-1234567890), "⁻¹²³⁴⁵⁶⁷⁸⁹⁰");
183    }
184
185    #[test]
186    fn test_table() {
187        let table = table("", 1..=3, 4..=6, |i, j| i * 10 + j);
188        let a = "    4   5   6 \n 1  14  15  16 \n 2  24  25  26 \n 3  34  35  36 \n";
189        assert_eq!(table, a.to_string());
190    }
191}