1#[derive(Debug, Clone, PartialEq)]
18pub enum FormatValue {
19 None,
21 Bool(bool),
22 Int(i64),
23 Float(f64),
24 Str(String),
25}
26
27impl From<&str> for FormatValue {
28 fn from(value: &str) -> Self {
29 FormatValue::Str(value.to_string())
30 }
31}
32
33impl From<String> for FormatValue {
34 fn from(value: String) -> Self {
35 FormatValue::Str(value)
36 }
37}
38
39impl From<i64> for FormatValue {
40 fn from(value: i64) -> Self {
41 FormatValue::Int(value)
42 }
43}
44
45impl From<i32> for FormatValue {
46 fn from(value: i32) -> Self {
47 FormatValue::Int(i64::from(value))
48 }
49}
50
51impl From<usize> for FormatValue {
52 fn from(value: usize) -> Self {
53 FormatValue::Int(value as i64)
54 }
55}
56
57impl From<f64> for FormatValue {
58 fn from(value: f64) -> Self {
59 FormatValue::Float(value)
60 }
61}
62
63impl From<bool> for FormatValue {
64 fn from(value: bool) -> Self {
65 FormatValue::Bool(value)
66 }
67}
68
69impl<T: Into<FormatValue>> From<Option<T>> for FormatValue {
70 fn from(value: Option<T>) -> Self {
71 value.map_or(FormatValue::None, Into::into)
72 }
73}
74
75pub fn format(template: &str, lookup: impl Fn(&str) -> Option<FormatValue>) -> String {
81 let mut out = String::with_capacity(template.len());
82 let mut chars = template.char_indices().peekable();
83 while let Some((index, c)) = chars.next() {
84 match c {
85 '{' if chars.peek().map(|&(_, n)| n) == Some('{') => {
86 chars.next();
87 out.push('{');
88 }
89 '}' if chars.peek().map(|&(_, n)| n) == Some('}') => {
90 chars.next();
91 out.push('}');
92 }
93 '{' => {
94 let Some(close) = template[index..].find('}') else {
95 out.push_str(&template[index..]);
96 break;
97 };
98 let field = &template[index + 1..index + close];
99 while let Some(&(at, _)) = chars.peek() {
101 if at > index + close {
102 break;
103 }
104 chars.next();
105 }
106 let (name, spec) = match field.split_once(':') {
107 Some((name, spec)) => (name, spec),
108 None => (field, ""),
109 };
110 let name = name.split_once('!').map_or(name, |(name, _)| name);
112 match lookup(name).and_then(|value| format_value(&value, spec)) {
113 Some(text) => out.push_str(&text),
114 None => out.push_str(&template[index..=index + close]),
115 }
116 }
117 other => out.push(other),
118 }
119 }
120 out
121}
122
123pub fn float_repr(value: f64) -> String {
126 if value.is_nan() {
127 return "nan".to_string();
128 }
129 if value.is_infinite() {
130 return if value > 0.0 { "inf" } else { "-inf" }.to_string();
131 }
132 let sci = format!("{value:e}");
133 let (mantissa, exponent) = sci.split_once('e').expect("LowerExp has an exponent");
134 let exponent: i32 = exponent.parse().expect("integer exponent");
135 let negative = mantissa.starts_with('-');
136 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
137 let sign = if negative { "-" } else { "" };
138 if !(-4..16).contains(&exponent) {
139 let (head, tail) = digits.split_at(1);
140 let fraction = if tail.is_empty() {
141 String::new()
142 } else {
143 format!(".{tail}")
144 };
145 return format!(
146 "{sign}{head}{fraction}e{}{:02}",
147 exp_sign(exponent),
148 exponent.abs()
149 );
150 }
151 let point = exponent + 1;
152 let text = if point <= 0 {
153 format!("0.{}{digits}", "0".repeat((-point) as usize))
154 } else if point as usize >= digits.len() {
155 format!("{digits}{}.0", "0".repeat(point as usize - digits.len()))
156 } else {
157 let (integer, fraction) = digits.split_at(point as usize);
158 format!("{integer}.{fraction}")
159 };
160 format!("{sign}{text}")
161}
162
163fn exp_sign(exponent: i32) -> char {
164 if exponent < 0 { '-' } else { '+' }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169enum Thousands {
170 None,
171 Comma,
173 Underscore,
175 UnderscoreFour,
177}
178
179#[derive(Debug)]
182struct Spec {
183 fill: char,
184 align: char,
185 sign: Option<char>,
186 no_neg_0: bool,
187 alternate: bool,
188 width: Option<usize>,
189 thousands: Thousands,
190 precision: Option<usize>,
191 kind: char,
193}
194
195fn parse_spec(spec: &str, default_type: char, default_align: char) -> Option<Spec> {
198 let chars: Vec<char> = spec.chars().collect();
199 let mut at = 0;
200 let mut parsed = Spec {
201 fill: ' ',
202 align: default_align,
203 sign: None,
204 no_neg_0: false,
205 alternate: false,
206 width: None,
207 thousands: Thousands::None,
208 precision: None,
209 kind: default_type,
210 };
211 let is_align = |c: char| matches!(c, '<' | '>' | '^' | '=');
212 let mut fill_specified = false;
213 let mut align_specified = false;
214 if chars.len() >= 2 && is_align(chars[1]) {
215 parsed.fill = chars[0];
216 parsed.align = chars[1];
217 fill_specified = true;
218 align_specified = true;
219 at = 2;
220 } else if !chars.is_empty() && is_align(chars[0]) {
221 parsed.align = chars[0];
222 align_specified = true;
223 at = 1;
224 }
225 if let Some(&c) = chars.get(at).filter(|c| matches!(c, '+' | '-' | ' ')) {
226 parsed.sign = Some(c);
227 at += 1;
228 }
229 if chars.get(at) == Some(&'z') {
230 parsed.no_neg_0 = true;
231 at += 1;
232 }
233 if chars.get(at) == Some(&'#') {
234 parsed.alternate = true;
235 at += 1;
236 }
237 if !fill_specified && chars.get(at) == Some(&'0') {
239 parsed.fill = '0';
240 if !align_specified && default_align == '>' {
241 parsed.align = '=';
242 }
243 at += 1;
244 }
245 let start = at;
246 while chars.get(at).is_some_and(char::is_ascii_digit) {
247 at += 1;
248 }
249 if at > start {
250 parsed.width = Some(chars[start..at].iter().collect::<String>().parse().ok()?);
251 }
252 if chars.get(at) == Some(&',') {
253 parsed.thousands = Thousands::Comma;
254 at += 1;
255 }
256 if chars.get(at) == Some(&'_') {
257 if parsed.thousands != Thousands::None {
258 return None; }
260 parsed.thousands = Thousands::Underscore;
261 at += 1;
262 }
263 if chars.get(at) == Some(&',') {
264 return None; }
266 if chars.get(at) == Some(&'.') {
267 at += 1;
268 let start = at;
269 while chars.get(at).is_some_and(char::is_ascii_digit) {
270 at += 1;
271 }
272 if at == start {
273 return None; }
275 parsed.precision = Some(chars[start..at].iter().collect::<String>().parse().ok()?);
276 }
277 match chars.len() - at {
278 0 => {}
279 1 => parsed.kind = chars[at],
280 _ => return None, }
282 if parsed.thousands != Thousands::None {
283 match parsed.kind {
284 'd' | 'e' | 'f' | 'g' | 'E' | 'G' | '%' | 'F' | '\0' => {}
285 'b' | 'o' | 'x' | 'X' if parsed.thousands == Thousands::Underscore => {
286 parsed.thousands = Thousands::UnderscoreFour;
287 }
288 _ => return None, }
290 }
291 Some(parsed)
292}
293
294pub fn format_value(value: &FormatValue, spec: &str) -> Option<String> {
296 if spec.is_empty() {
298 return Some(match value {
299 FormatValue::None => "None".to_string(),
300 FormatValue::Bool(flag) => if *flag { "True" } else { "False" }.to_string(),
301 FormatValue::Int(number) => number.to_string(),
302 FormatValue::Float(number) => float_repr(*number),
303 FormatValue::Str(text) => text.clone(),
304 });
305 }
306 match value {
307 FormatValue::None => None,
309 FormatValue::Str(text) => format_str(text, &parse_spec(spec, 's', '<')?),
310 FormatValue::Bool(flag) => format_int(i64::from(*flag), &parse_spec(spec, 'd', '>')?),
312 FormatValue::Int(number) => format_int(*number, &parse_spec(spec, 'd', '>')?),
313 FormatValue::Float(number) => format_float(*number, &parse_spec(spec, '\0', '>')?),
314 }
315}
316
317fn format_str(text: &str, spec: &Spec) -> Option<String> {
319 if spec.kind != 's'
320 || spec.sign.is_some()
321 || spec.no_neg_0
322 || spec.alternate
323 || spec.align == '='
324 {
325 return None;
326 }
327 let body: String = match spec.precision {
328 Some(precision) => text.chars().take(precision).collect(),
329 None => text.to_string(),
330 };
331 let length = body.chars().count();
332 let gap = spec.width.map_or(0, |width| width.saturating_sub(length));
333 let left = match spec.align {
334 '>' => gap,
335 '^' => gap / 2,
336 _ => 0,
337 };
338 let fill = |count: usize| spec.fill.to_string().repeat(count);
339 Some(format!("{}{body}{}", fill(left), fill(gap - left)))
340}
341
342fn format_int(number: i64, spec: &Spec) -> Option<String> {
345 if matches!(spec.kind, 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | '%') {
346 return format_float(number as f64, spec);
347 }
348 if spec.precision.is_some() || spec.no_neg_0 {
349 return None;
350 }
351 let sign_char = number < 0;
352 let magnitude = number.unsigned_abs();
353 let (prefix, digits, remainder) = match spec.kind {
354 'c' => {
355 if spec.sign.is_some() || spec.alternate {
356 return None;
357 }
358 let c = u32::try_from(number).ok().and_then(char::from_u32)?;
361 return Some(fill_number(
364 false,
365 "",
366 "",
367 false,
368 &c.to_string(),
369 spec,
370 None,
371 ));
372 }
373 'd' | 'n' => ("", magnitude.to_string(), ""),
374 'b' => ("0b", format!("{magnitude:b}"), ""),
375 'o' => ("0o", format!("{magnitude:o}"), ""),
376 'x' => ("0x", format!("{magnitude:x}"), ""),
377 'X' => ("0X", format!("{magnitude:X}"), ""),
378 _ => return None,
379 };
380 let prefix = if spec.alternate { prefix } else { "" };
381 Some(fill_number(
382 sign_char,
383 prefix,
384 &digits,
385 false,
386 remainder,
387 spec,
388 grouping(spec),
389 ))
390}
391
392fn grouping(spec: &Spec) -> Option<(char, usize)> {
396 if spec.kind == 'n' {
397 return None;
398 }
399 match spec.thousands {
400 Thousands::None => None,
401 Thousands::Comma => Some((',', 3)),
402 Thousands::Underscore => Some(('_', 3)),
403 Thousands::UnderscoreFour => Some(('_', 4)),
404 }
405}
406
407fn format_float(number: f64, spec: &Spec) -> Option<String> {
409 let mut kind = spec.kind;
410 let mut add_dot_0 = false;
411 let mut default_precision = 6;
412 if kind == '\0' {
413 add_dot_0 = true;
416 kind = 'r';
417 default_precision = 0;
418 }
419 if kind == 'n' {
420 kind = 'g';
421 }
422 if !matches!(kind, 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | '%' | 'r') {
423 return None; }
425 let mut value = number;
426 let mut add_pct = false;
427 if kind == '%' {
428 kind = 'f';
429 value *= 100.0;
430 add_pct = true;
431 }
432 let precision = match spec.precision {
433 None => default_precision,
434 Some(precision) => {
435 if kind == 'r' {
436 kind = 'g';
437 }
438 precision
439 }
440 };
441 let mut buffer = double_to_string(
442 value,
443 kind,
444 precision,
445 spec.alternate,
446 add_dot_0,
447 spec.no_neg_0,
448 );
449 if add_pct {
450 buffer.push('%');
451 }
452 let (negative, body) = match buffer.strip_prefix('-') {
453 Some(body) => (true, body),
454 None => (false, buffer.as_str()),
455 };
456 let digit_count = body.bytes().take_while(u8::is_ascii_digit).count();
459 let (digits, rest) = body.split_at(digit_count);
460 let (has_decimal, remainder) = match rest.strip_prefix('.') {
461 Some(remainder) => (true, remainder),
462 None => (false, rest),
463 };
464 Some(fill_number(
465 negative,
466 "",
467 digits,
468 has_decimal,
469 remainder,
470 spec,
471 grouping(spec),
472 ))
473}
474
475fn double_to_string(
478 value: f64,
479 code: char,
480 precision: usize,
481 alternate: bool,
482 add_dot_0: bool,
483 no_neg_0: bool,
484) -> String {
485 let upper = code.is_ascii_uppercase();
486 let code = code.to_ascii_lowercase();
487 let case = |text: String| if upper { text.to_uppercase() } else { text };
488 if value.is_nan() {
489 return case("nan".to_string());
491 }
492 if value.is_infinite() {
493 return case(if value < 0.0 { "-inf" } else { "inf" }.to_string());
494 }
495 let magnitude = value.abs();
498 let (digits, mut decpt) = match code {
499 'e' => dtoa_significant(magnitude, precision + 1),
500 'g' => dtoa_significant(magnitude, precision.max(1)),
501 'f' => dtoa_fixed(magnitude, precision),
502 _ => dtoa_shortest(magnitude),
503 };
504 let precision = match code {
505 'e' => precision + 1,
506 'g' => precision.max(1),
507 _ => precision,
508 } as i64;
509 let digits_len = digits.len() as i64;
510 let mut use_exp = false;
511 let mut vdigits_end = digits_len;
512 match code {
513 'e' => {
514 use_exp = true;
515 vdigits_end = precision;
516 }
517 'f' => vdigits_end = decpt + precision,
518 'g' => {
519 let limit = if add_dot_0 { precision - 1 } else { precision };
520 if decpt <= -4 || decpt > limit {
521 use_exp = true;
522 }
523 if alternate {
524 vdigits_end = precision;
525 }
526 }
527 _ => {
528 if decpt <= -4 || decpt > 16 {
529 use_exp = true;
530 }
531 }
532 }
533 let mut exponent = 0;
534 if use_exp {
535 exponent = decpt - 1;
536 decpt = 1;
537 }
538 let vdigits_start = if decpt <= 0 { decpt - 1 } else { 0 };
539 vdigits_end = if !use_exp && add_dot_0 {
540 vdigits_end.max(decpt + 1)
541 } else {
542 vdigits_end.max(decpt)
543 };
544
545 let zero_value = digits.bytes().all(|b| b == b'0');
546 let negative = value.is_sign_negative() && !(no_neg_0 && zero_value);
547 let zeros = |count: i64| "0".repeat(count.max(0) as usize);
548 let mut out = String::new();
549 if negative {
550 out.push('-');
551 }
552 if decpt <= 0 {
553 out.push_str(&zeros(decpt - vdigits_start));
554 out.push('.');
555 out.push_str(&zeros(-decpt));
556 } else {
557 out.push_str(&zeros(-vdigits_start));
558 }
559 if 0 < decpt && decpt <= digits_len {
560 out.push_str(&digits[..decpt as usize]);
561 out.push('.');
562 out.push_str(&digits[decpt as usize..]);
563 } else {
564 out.push_str(&digits);
565 }
566 if digits_len < decpt {
567 out.push_str(&zeros(decpt - digits_len));
568 out.push('.');
569 out.push_str(&zeros(vdigits_end - decpt));
570 } else {
571 out.push_str(&zeros(vdigits_end - digits_len));
572 }
573 if out.ends_with('.') && !alternate {
574 out.pop();
575 }
576 if use_exp {
577 out.push_str(&format!(
578 "e{}{:02}",
579 exp_sign(exponent as i32),
580 exponent.abs()
581 ));
582 }
583 case(out)
584}
585
586fn split_scientific(text: &str) -> (String, i64) {
589 let (mantissa, exponent) = text.split_once('e').expect("LowerExp has an exponent");
590 let exponent: i64 = exponent.parse().expect("integer exponent");
591 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
592 let digits = digits.trim_end_matches('0');
593 if digits.is_empty() {
594 return ("0".to_string(), 1);
595 }
596 (digits.to_string(), exponent + 1)
597}
598
599fn dtoa_shortest(value: f64) -> (String, i64) {
601 split_scientific(&format!("{value:e}"))
602}
603
604fn dtoa_significant(value: f64, count: usize) -> (String, i64) {
606 split_scientific(&format!("{value:.*e}", count - 1))
607}
608
609fn dtoa_fixed(value: f64, precision: usize) -> (String, i64) {
612 if value == 0.0 {
613 return ("0".to_string(), 1);
614 }
615 let text = format!("{value:.precision$}");
616 let (integer, fraction) = text.split_once('.').unwrap_or((&text, ""));
617 let all = format!("{integer}{fraction}");
618 let leading = all.bytes().take_while(|b| *b == b'0').count();
619 let digits = all[leading..].trim_end_matches('0');
620 if digits.is_empty() {
621 return (String::new(), -(precision as i64));
622 }
623 (digits.to_string(), integer.len() as i64 - leading as i64)
624}
625
626fn fill_number(
629 negative: bool,
630 prefix: &str,
631 digits: &str,
632 has_decimal: bool,
633 remainder: &str,
634 spec: &Spec,
635 grouping: Option<(char, usize)>,
636) -> String {
637 let sign = match (spec.sign, negative) {
638 (_, true) => Some('-'),
639 (Some('+'), false) => Some('+'),
640 (Some(' '), false) => Some(' '),
641 _ => None,
642 };
643 let width = spec.width.map_or(-1, |width| width as i64);
644 let non_digit = i64::from(sign.is_some())
645 + prefix.chars().count() as i64
646 + i64::from(has_decimal)
647 + remainder.chars().count() as i64;
648 let min_width = if spec.fill == '0' && spec.align == '=' {
649 width - non_digit
650 } else {
651 0
652 };
653 let grouped = if digits.is_empty() {
654 String::new()
655 } else {
656 insert_thousands_grouping(digits, min_width, grouping)
657 };
658 let padding = width - (non_digit + grouped.chars().count() as i64);
659 let (mut left, mut middle, mut right) = (0, 0, 0);
660 if padding > 0 {
661 match spec.align {
662 '<' => right = padding,
663 '^' => {
664 left = padding / 2;
665 right = padding - left;
666 }
667 '=' => middle = padding,
668 _ => left = padding,
669 }
670 }
671 let fill = |count: i64| spec.fill.to_string().repeat(count as usize);
672 let mut out = fill(left);
673 out.extend(sign);
674 out.push_str(prefix);
675 out.push_str(&fill(middle));
676 out.push_str(&grouped);
677 if has_decimal {
678 out.push('.');
679 }
680 out.push_str(remainder);
681 out.push_str(&fill(right));
682 out
683}
684
685fn insert_thousands_grouping(
688 digits: &str,
689 min_width: i64,
690 grouping: Option<(char, usize)>,
691) -> String {
692 let chars: Vec<char> = digits.chars().collect();
693 let mut remaining = chars.len() as i64;
694 let mut min_width = min_width;
695 let mut pieces: Vec<String> = Vec::new();
697 let mut use_separator = false;
698 let mut piece = |length: i64, remaining: i64, use_separator: bool| {
699 let zeros = (length - remaining).max(0);
700 let count = remaining.min(length).max(0);
701 let mut text = "0".repeat(zeros as usize);
702 text.extend(&chars[(remaining - count) as usize..remaining as usize]);
703 if use_separator {
704 text.push(grouping.map_or(',', |(separator, _)| separator));
705 }
706 pieces.push(text);
707 count
708 };
709 let mut finished = false;
710 if let Some((_, size)) = grouping {
711 loop {
712 let length = (size as i64).min(remaining.max(min_width).max(1));
713 remaining -= piece(length, remaining, use_separator);
714 use_separator = true;
715 min_width -= length;
716 if remaining <= 0 && min_width <= 0 {
717 finished = true;
718 break;
719 }
720 min_width -= 1;
721 }
722 }
723 if !finished {
724 let length = remaining.max(min_width).max(1);
725 piece(length, remaining, use_separator);
726 }
727 pieces.iter().rev().map(String::as_str).collect()
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733
734 fn fmt(value: impl Into<FormatValue>, spec: &str) -> String {
735 format_value(&value.into(), spec).expect("valid spec")
736 }
737
738 #[test]
739 fn floats_print_like_python_str() {
740 for (value, expected) in [
742 (10.0, "10.0"),
743 (0.1, "0.1"),
744 (1e16, "1e+16"),
745 (1.5e16, "1.5e+16"),
746 (1e15, "1000000000000000.0"),
747 (0.0001, "0.0001"),
748 (0.00001, "1e-05"),
749 (-2.5, "-2.5"),
750 (123456.789, "123456.789"),
751 (f64::INFINITY, "inf"),
752 ] {
753 assert_eq!(float_repr(value), expected, "{value}");
754 }
755 }
756
757 #[test]
758 fn specs_match_python_format() {
759 assert_eq!(fmt(42.0, ">3.0f"), " 42");
761 assert_eq!(fmt(99.95, ">3.0f"), "100");
762 assert_eq!(fmt(2.5, ".0f"), "2");
763 assert_eq!(fmt(3.5, ".0f"), "4");
764 assert_eq!(fmt(0.425, ".1%"), "42.5%");
765 assert_eq!(fmt(1234567.891, ",.2f"), "1,234,567.89");
766 assert_eq!(fmt(1234567i64, ","), "1,234,567");
767 assert_eq!(fmt(42i64, "05d"), "00042");
768 assert_eq!(fmt(-42i64, "05d"), "-0042");
769 assert_eq!(fmt(42i64, "+d"), "+42");
770 assert_eq!(fmt(255i64, "#x"), "0xff");
771 assert_eq!(fmt("ab", "*^6"), "**ab**");
772 assert_eq!(fmt("abcdef", ".3"), "abc");
773 assert_eq!(fmt("ab", "5"), "ab ");
774 assert_eq!(fmt(7i64, "5"), " 7");
775 assert_eq!(fmt(1234.5, ".2"), "1.2e+03");
776 assert_eq!(fmt(10.0, ".3"), "10.0");
777 assert_eq!(fmt(10.0, ".3g"), "10");
778 assert_eq!(fmt(0.000012345, "g"), "1.2345e-05");
779 assert_eq!(fmt(1500.0, "e"), "1.500000e+03");
780 assert_eq!(fmt(true, ""), "True");
781 assert_eq!(fmt(true, "d"), "1");
782 assert_eq!(fmt(FormatValue::None, ""), "None");
783 assert_eq!(fmt(3i64, ".1f"), "3.0");
784 assert_eq!(fmt(12.5, ".2"), "1.2e+01");
786 assert_eq!(fmt(2.5, ".0"), "2e+00");
787 assert_eq!(fmt(1e16, ","), "1e+16");
789 assert_eq!(fmt(f64::INFINITY, "%"), "inf%");
790 assert_eq!(fmt(123.0, "#g"), "123.000");
791 assert_eq!(fmt(123.0, "#.3g"), "123.");
792 assert_eq!(fmt(-0.0001, "z.2f"), "0.00");
793 assert_eq!(fmt(1234.5, "n"), "1234.5");
794 assert_eq!(fmt("h\u{e9}llo", "08"), "h\u{e9}llo000");
795 assert_eq!(fmt(1234i64, "010,"), "00,001,234");
797 assert_eq!(fmt(65i64, "c"), "A");
798 assert_eq!(format_value(&FormatValue::from("ab"), "=5"), None);
799 assert_eq!(format_value(&FormatValue::Int(1234), ",n"), None);
800 }
801
802 #[test]
803 fn templates_expand_fields_and_escapes() {
804 let lookup = |name: &str| match name {
805 "task.completed" => Some(FormatValue::Float(3.0)),
806 "task.fields[name]" => Some(FormatValue::from("disk")),
807 _ => None,
808 };
809 assert_eq!(
810 format("{{x}} {task.fields[name]}: {task.completed:>5.1f}", lookup),
811 "{x} disk: 3.0"
812 );
813 assert_eq!(
815 format("{task.nope} {task.completed:q}", lookup),
816 "{task.nope} {task.completed:q}"
817 );
818 assert_eq!(format("{task.completed!s}", lookup), "3.0");
819 assert_eq!(format("open {", lookup), "open {");
820 }
821}