1pub fn round_to(x: f64, ndigits: usize) -> f64 {
8 if !x.is_finite() {
9 return x;
10 }
11 format!("{x:.ndigits$}").parse().unwrap_or(x)
12}
13
14pub fn fixed(x: f64, prec: usize) -> String {
16 if x.is_nan() {
17 "nan".to_string()
18 } else if x.is_infinite() {
19 (if x > 0.0 { "inf" } else { "-inf" }).to_string()
20 } else {
21 format!("{x:.prec$}")
22 }
23}
24
25pub fn pad_right(s: &str, width: usize) -> String {
27 let n = s.chars().count();
28 if n >= width {
29 s.to_string()
30 } else {
31 format!("{s}{}", " ".repeat(width - n))
32 }
33}
34
35pub fn pad_left(s: &str, width: usize) -> String {
37 let n = s.chars().count();
38 if n >= width {
39 s.to_string()
40 } else {
41 format!("{}{s}", " ".repeat(width - n))
42 }
43}
44
45pub fn head(s: &str, n: usize) -> String {
47 s.chars().take(n).collect()
48}
49
50pub fn commas(n: i64) -> String {
52 let digits = n.unsigned_abs().to_string();
53 let mut out = String::new();
54 for (i, c) in digits.chars().enumerate() {
55 if i > 0 && (digits.len() - i) % 3 == 0 {
56 out.push(',');
57 }
58 out.push(c);
59 }
60 if n < 0 {
61 format!("-{out}")
62 } else {
63 out
64 }
65}
66
67pub fn html_escape(s: &str) -> String {
69 let mut out = String::with_capacity(s.len());
70 for c in s.chars() {
71 match c {
72 '&' => out.push_str("&"),
73 '<' => out.push_str("<"),
74 '>' => out.push_str(">"),
75 '"' => out.push_str("""),
76 '\'' => out.push_str("'"),
77 _ => out.push(c),
78 }
79 }
80 out
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
89 fn rounding_and_fixed_match_cpython() {
90 assert_eq!(round_to(2.675, 2), 2.67);
91 assert_eq!(round_to(0.15, 1), 0.1);
92 assert_eq!(round_to(1.25, 1), 1.2);
93 assert_eq!(round_to(2.35, 1), 2.4);
94 assert_eq!(round_to(100.0 / 3.0, 1), 33.3);
95 assert_eq!(fixed(2.5, 0), "2");
96 assert_eq!(fixed(3.5, 0), "4");
97 assert_eq!(fixed(0.25, 1), "0.2");
98 assert_eq!(fixed(0.35, 1), "0.3");
99 assert_eq!(fixed(f64::NEG_INFINITY, 1), "-inf");
100 assert_eq!(pad_left(&fixed(12345.678, 1), 6), "12345.7");
101 }
102
103 #[test]
104 fn padding_and_slicing_count_characters() {
105 assert_eq!(pad_right("ééé", 6), "ééé ");
106 assert_eq!(pad_left("ab", 4), " ab");
107 assert_eq!(head("日本語テキスト", 3), "日本語");
108 assert_eq!(head("ab", 5), "ab");
109 }
110
111 #[test]
112 fn commas_like_format_spec() {
113 assert_eq!(commas(0), "0");
114 assert_eq!(commas(1234567), "1,234,567");
115 assert_eq!(commas(-1234), "-1,234");
116 assert_eq!(commas(-123), "-123");
117 }
118
119 #[test]
120 fn escape_matches_html_module() {
121 assert_eq!(
122 html_escape("a & b < c > d \" e ' f"),
123 "a & b < c > d " e ' f"
124 );
125 }
126}