1use num_bigint::Sign as BigSign;
17
18use crate::mutator::{panic, RtResult};
19use crate::ops::{self, Interop};
20use crate::{class_of, Value};
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23pub enum Align {
24 Default,
26 Left,
27 Right,
28 Center,
29 AfterSign,
31}
32
33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub enum Sign {
35 Minus,
37 Plus,
39 Space,
41}
42
43#[derive(Copy, Clone, Debug, PartialEq, Eq)]
44pub enum Grouping {
45 None,
46 Comma,
48 Underscore,
50}
51
52#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub enum FmtType {
54 Default,
57 Str,
59 Dec,
60 Hex,
61 HexUpper,
62 Oct,
63 Bin,
64 Exp,
65 ExpUpper,
66 Fixed,
67 FixedUpper,
68 Percent,
69}
70
71impl FmtType {
72 const ALL: [FmtType; 12] = [
73 FmtType::Default,
74 FmtType::Str,
75 FmtType::Dec,
76 FmtType::Hex,
77 FmtType::HexUpper,
78 FmtType::Oct,
79 FmtType::Bin,
80 FmtType::Exp,
81 FmtType::ExpUpper,
82 FmtType::Fixed,
83 FmtType::FixedUpper,
84 FmtType::Percent,
85 ];
86
87 fn from_char(c: char) -> Option<FmtType> {
88 Some(match c {
89 's' => FmtType::Str,
90 'd' => FmtType::Dec,
91 'x' => FmtType::Hex,
92 'X' => FmtType::HexUpper,
93 'o' => FmtType::Oct,
94 'b' => FmtType::Bin,
95 'e' => FmtType::Exp,
96 'E' => FmtType::ExpUpper,
97 'f' => FmtType::Fixed,
98 'F' => FmtType::FixedUpper,
99 '%' => FmtType::Percent,
100 _ => return None,
101 })
102 }
103
104 fn letter(self) -> &'static str {
105 match self {
106 FmtType::Default => "",
107 FmtType::Str => "s",
108 FmtType::Dec => "d",
109 FmtType::Hex => "x",
110 FmtType::HexUpper => "X",
111 FmtType::Oct => "o",
112 FmtType::Bin => "b",
113 FmtType::Exp => "e",
114 FmtType::ExpUpper => "E",
115 FmtType::Fixed => "f",
116 FmtType::FixedUpper => "F",
117 FmtType::Percent => "%",
118 }
119 }
120
121 fn is_radix(self) -> bool {
122 matches!(self, FmtType::Hex | FmtType::HexUpper | FmtType::Oct | FmtType::Bin)
123 }
124
125 fn is_int_only(self) -> bool {
126 self == FmtType::Dec || self.is_radix()
127 }
128
129 fn is_float(self) -> bool {
130 matches!(self, FmtType::Exp | FmtType::ExpUpper | FmtType::Fixed | FmtType::FixedUpper | FmtType::Percent)
131 }
132}
133
134pub const MAX_WIDTH: u16 = 0x3fff;
136
137#[derive(Copy, Clone, Debug, PartialEq, Eq)]
138pub struct FormatSpec {
139 pub fill: char,
140 pub align: Align,
141 pub sign: Sign,
142 pub alt: bool,
144 pub zero: bool,
146 pub width: u16,
147 pub grouping: Grouping,
148 pub precision: Option<u16>,
149 pub ty: FmtType,
150}
151
152impl Default for FormatSpec {
153 fn default() -> Self {
154 FormatSpec {
155 fill: ' ',
156 align: Align::Default,
157 sign: Sign::Minus,
158 alt: false,
159 zero: false,
160 width: 0,
161 grouping: Grouping::None,
162 precision: None,
163 ty: FmtType::Default,
164 }
165 }
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
170pub struct SpecError {
171 pub msg: String,
172 pub start: usize,
173 pub end: usize,
174}
175
176#[derive(Copy, Clone, Debug, PartialEq, Eq)]
178pub enum ValueKind {
179 Int,
180 Float,
181 Text,
183}
184
185fn align_of(c: char) -> Option<Align> {
186 Some(match c {
187 '<' => Align::Left,
188 '>' => Align::Right,
189 '^' => Align::Center,
190 '=' => Align::AfterSign,
191 _ => return None,
192 })
193}
194
195pub fn parse(spec: &str) -> Result<FormatSpec, SpecError> {
197 let err = |msg: String, start: usize, end: usize| Err(SpecError { msg, start, end });
198 if spec.is_empty() {
199 return err("empty format spec".into(), 0, 0);
200 }
201 let cs: Vec<(usize, char)> = spec.char_indices().collect();
202 let at = |i: usize| cs.get(i).map(|&(_, c)| c);
203 let pos = |i: usize| cs.get(i).map_or(spec.len(), |&(p, _)| p);
204 let mut f = FormatSpec::default();
205 let mut i = 0;
206 if let Some(a) = at(1).and_then(align_of) {
207 f.fill = cs[0].1;
208 f.align = a;
209 i = 2;
210 } else if let Some(a) = at(0).and_then(align_of) {
211 f.align = a;
212 i = 1;
213 }
214 if let Some(c @ ('+' | ' ' | '-')) = at(i) {
215 f.sign = match c {
216 '+' => Sign::Plus,
217 ' ' => Sign::Space,
218 _ => Sign::Minus,
219 };
220 i += 1;
221 }
222 if at(i) == Some('#') {
223 f.alt = true;
224 i += 1;
225 }
226 if at(i) == Some('0') {
227 f.zero = true;
228 i += 1;
229 }
230 let ws = i;
231 while at(i).is_some_and(|c| c.is_ascii_digit()) {
232 i += 1;
233 }
234 if i > ws {
235 match spec[pos(ws)..pos(i)].parse::<u32>() {
236 Ok(w) if w <= MAX_WIDTH as u32 => f.width = w as u16,
237 _ => return err(format!("width is larger than {MAX_WIDTH}"), pos(ws), pos(i)),
238 }
239 }
240 if let Some(c @ (',' | '_')) = at(i) {
241 f.grouping = if c == ',' { Grouping::Comma } else { Grouping::Underscore };
242 i += 1;
243 }
244 if at(i) == Some('.') {
245 let dot = i;
246 i += 1;
247 let ps = i;
248 while at(i).is_some_and(|c| c.is_ascii_digit()) {
249 i += 1;
250 }
251 if i == ps {
252 return err("expected digits after `.`".into(), pos(dot), pos(i));
253 }
254 match spec[pos(ps)..pos(i)].parse::<u32>() {
255 Ok(p) if p < MAX_WIDTH as u32 => f.precision = Some(p as u16),
256 _ => return err(format!("precision is larger than {}", MAX_WIDTH - 1), pos(ps), pos(i)),
257 }
258 }
259 if let Some(c) = at(i) {
260 if let Some(t) = FmtType::from_char(c) {
261 f.ty = t;
262 i += 1;
263 } else if c.is_ascii_alphabetic() && i + 1 == cs.len() {
264 return err(
265 format!("unknown format type `{c}` (expected one of s d x X o b e E f F %)"),
266 pos(i),
267 pos(i + 1),
268 );
269 }
270 }
271 if i < cs.len() {
272 return err(format!("unexpected `{}` in format spec", &spec[pos(i)..]), pos(i), spec.len());
273 }
274 Ok(f)
275}
276
277impl FormatSpec {
278 pub fn encode(&self) -> [u32; 2] {
280 let w0 = self.fill as u32
281 | (self.align as u32) << 21
282 | (self.sign as u32) << 24
283 | (self.alt as u32) << 26
284 | (self.zero as u32) << 27
285 | (self.grouping as u32) << 28;
286 let prec = self.precision.map_or(0, |p| p as u32 + 1);
287 let w1 = self.ty as u32 | (self.width as u32 & 0x3fff) << 4 | (prec & 0x3fff) << 18;
288 [w0, w1]
289 }
290
291 #[inline]
293 pub fn decode(w: [u32; 2]) -> FormatSpec {
294 const ALIGNS: [Align; 5] = [Align::Default, Align::Left, Align::Right, Align::Center, Align::AfterSign];
295 const SIGNS: [Sign; 3] = [Sign::Minus, Sign::Plus, Sign::Space];
296 const GROUPS: [Grouping; 3] = [Grouping::None, Grouping::Comma, Grouping::Underscore];
297 let [w0, w1] = w;
298 let prec = (w1 >> 18) & 0x3fff;
299 FormatSpec {
300 fill: char::from_u32(w0 & 0x1f_ffff).unwrap_or(' '),
301 align: ALIGNS[((w0 >> 21) & 7) as usize % 5],
302 sign: SIGNS[((w0 >> 24) & 3) as usize % 3],
303 alt: (w0 >> 26) & 1 != 0,
304 zero: (w0 >> 27) & 1 != 0,
305 grouping: GROUPS[((w0 >> 28) & 3) as usize % 3],
306 width: ((w1 >> 4) & 0x3fff) as u16,
307 precision: if prec == 0 { None } else { Some(prec as u16 - 1) },
308 ty: FmtType::ALL[(w1 & 0xf) as usize % 12],
309 }
310 }
311
312 pub fn check(&self, kind: ValueKind, type_name: &str) -> Result<(), String> {
314 let t = self.ty;
315 let numeric = match kind {
316 ValueKind::Int => t != FmtType::Str,
317 ValueKind::Float => {
318 if t.is_int_only() {
319 return Err(format!("format type `{}` needs an Int, found {type_name}", t.letter()));
320 }
321 t != FmtType::Str
322 }
323 ValueKind::Text => {
324 if t.is_int_only() || t.is_float() {
325 let what = if t.is_float() { "a number" } else { "an Int" };
326 return Err(format!("format type `{}` needs {what}, found {type_name}", t.letter()));
327 }
328 false
329 }
330 };
331 if !numeric {
332 let flag = if self.sign != Sign::Minus {
333 Some(if self.sign == Sign::Plus { "+" } else { " " })
334 } else if self.alt {
335 Some("#")
336 } else if self.zero {
337 Some("0")
338 } else if self.grouping != Grouping::None {
339 Some(if self.grouping == Grouping::Comma { "," } else { "_" })
340 } else if self.align == Align::AfterSign {
341 Some("=")
342 } else {
343 None
344 };
345 if let Some(flag) = flag {
346 return Err(format!("`{flag}` applies only to numbers, found {type_name}"));
347 }
348 return Ok(());
349 }
350 if self.alt && !t.is_radix() {
351 return Err("`#` needs one of the types x X o b".into());
352 }
353 if self.grouping == Grouping::Comma && t.is_radix() {
354 return Err(format!("`,` grouping does not apply to `{}`; use `_`", t.letter()));
355 }
356 if kind == ValueKind::Int && self.precision.is_some() && (t == FmtType::Default || t.is_int_only()) {
357 return Err("an Int format takes no precision; use `f` or `e` to format it as a Float".into());
358 }
359 Ok(())
360 }
361}
362
363pub fn kind_of_value(v: Value) -> ValueKind {
365 if ops::is_int(v) {
366 ValueKind::Int
367 } else if v.is_float() {
368 ValueKind::Float
369 } else {
370 ValueKind::Text
371 }
372}
373
374pub fn format_value(ix: &mut dyn Interop, v: Value, spec: &FormatSpec) -> RtResult<String> {
377 let kind = kind_of_value(v);
378 if let Err(msg) = spec.check(kind, &class_of(v).name) {
379 return panic(format!("FormatError: {msg}"));
380 }
381 let mut out = String::new();
382 match (kind, spec.ty) {
383 (_, FmtType::Str) | (ValueKind::Text, _) => {
384 let text = ops::to_s(ix, v)?;
385 spec.write_text(&mut out, &text);
386 }
387 (ValueKind::Int, t) if !t.is_float() => spec.write_int(&mut out, v),
388 (ValueKind::Int, _) => spec.write_float(&mut out, ops::int_to_f64(v)),
389 (ValueKind::Float, _) => spec.write_float(&mut out, v.as_f64()),
390 }
391 Ok(out)
392}
393
394fn group(digits: &str, sep: char, every: usize, min_len: usize) -> String {
397 let mut digits = digits.to_string();
398 loop {
399 let n = digits.len();
400 let mut out = String::with_capacity(n + n / every);
401 for (i, c) in digits.chars().enumerate() {
402 if i > 0 && (n - i) % every == 0 {
403 out.push(sep);
404 }
405 out.push(c);
406 }
407 if out.chars().count() >= min_len {
408 return out;
409 }
410 digits.insert(0, '0');
411 }
412}
413
414impl FormatSpec {
415 fn sign_str(&self, negative: bool) -> &'static str {
416 match (negative, self.sign) {
417 (true, _) => "-",
418 (false, Sign::Plus) => "+",
419 (false, Sign::Space) => " ",
420 (false, Sign::Minus) => "",
421 }
422 }
423
424 fn pad(&self, out: &mut String, sign: &str, prefix: &str, body: &str, numeric: bool) {
426 let len = sign.chars().count() + prefix.chars().count() + body.chars().count();
427 let pad = (self.width as usize).saturating_sub(len);
428 let (fill, align) = match self.align {
429 Align::Default if self.zero => ('0', Align::AfterSign),
430 Align::Default if numeric => (self.fill, Align::Right),
431 Align::Default => (self.fill, Align::Left),
432 a => (self.fill, a),
433 };
434 let fills = |out: &mut String, n: usize| out.extend(std::iter::repeat_n(fill, n));
435 match align {
436 Align::Left => {
437 out.push_str(sign);
438 out.push_str(prefix);
439 out.push_str(body);
440 fills(out, pad);
441 }
442 Align::Center => {
443 fills(out, pad / 2);
444 out.push_str(sign);
445 out.push_str(prefix);
446 out.push_str(body);
447 fills(out, pad - pad / 2);
448 }
449 Align::AfterSign => {
450 out.push_str(sign);
451 out.push_str(prefix);
452 fills(out, pad);
453 out.push_str(body);
454 }
455 Align::Right | Align::Default => {
456 fills(out, pad);
457 out.push_str(sign);
458 out.push_str(prefix);
459 out.push_str(body);
460 }
461 }
462 }
463
464 fn grouped(&self, digits: &str, every: usize, others: usize) -> String {
466 let sep = match self.grouping {
467 Grouping::None => return digits.to_string(),
468 Grouping::Comma => ',',
469 Grouping::Underscore => '_',
470 };
471 let min =
472 if self.zero && self.align == Align::Default { (self.width as usize).saturating_sub(others) } else { 0 };
473 group(digits, sep, every, min)
474 }
475
476 fn write_text(&self, out: &mut String, text: &str) {
477 let text = match self.precision {
478 Some(p) => match text.char_indices().nth(p as usize) {
479 Some((i, _)) => &text[..i],
480 None => text,
481 },
482 None => text,
483 };
484 self.pad(out, "", "", text, false);
485 }
486
487 fn write_int(&self, out: &mut String, v: Value) {
488 let (radix, upper, prefix) = match self.ty {
489 FmtType::Hex => (16, false, "0x"),
490 FmtType::HexUpper => (16, true, "0X"),
491 FmtType::Oct => (8, false, "0o"),
492 FmtType::Bin => (2, false, "0b"),
493 _ => (10, false, ""),
494 };
495 let (negative, mut digits) = if v.is_small_int() {
496 let n = v.as_small_int();
497 let m = n.unsigned_abs();
498 let d = match radix {
499 16 => format!("{m:x}"),
500 8 => format!("{m:o}"),
501 2 => format!("{m:b}"),
502 _ => m.to_string(),
503 };
504 (n < 0, d)
505 } else {
506 let b = ops::to_bigint(v);
507 (b.sign() == BigSign::Minus, b.magnitude().to_str_radix(radix))
508 };
509 if upper {
510 digits.make_ascii_uppercase();
511 }
512 let sign = self.sign_str(negative);
513 let prefix = if self.alt { prefix } else { "" };
514 let every = if radix == 10 { 3 } else { 4 };
515 let body = self.grouped(&digits, every, sign.len() + prefix.len());
516 self.pad(out, sign, prefix, &body, true);
517 }
518
519 fn write_float(&self, out: &mut String, x: f64) {
520 let upper = matches!(self.ty, FmtType::ExpUpper | FmtType::FixedUpper);
521 let negative = x.is_sign_negative() && !x.is_nan();
522 let sign = self.sign_str(negative);
523 let a = x.abs();
524 let mut body = if !a.is_finite() {
525 if a.is_nan() { "NaN" } else { "Infinity" }.to_string()
526 } else {
527 let p = self.precision.unwrap_or(6) as usize;
528 match self.ty {
529 FmtType::Exp | FmtType::ExpUpper => {
530 let s = format!("{a:.p$e}");
531 let (mant, exp) = s.split_once('e').unwrap_or((&s, "0"));
532 let (esign, edigits) = match exp.strip_prefix('-') {
533 Some(d) => ('-', d),
534 None => ('+', exp),
535 };
536 format!("{mant}e{esign}{edigits:0>2}")
537 }
538 FmtType::Percent => format!("{:.p$}%", a * 100.0),
539 FmtType::Default if self.precision.is_none() => ops::format_float(a),
540 _ => format!("{a:.p$}"),
541 }
542 };
543 if upper {
544 body.make_ascii_uppercase();
545 }
546 if self.grouping != Grouping::None && a.is_finite() {
547 let end = body.find(|c: char| !c.is_ascii_digit()).unwrap_or(body.len());
548 let rest_len = body.len() - end;
549 let int = self.grouped(&body[..end], 3, sign.len() + rest_len);
550 body = format!("{int}{}", &body[end..]);
551 }
552 self.pad(out, sign, "", &body, true);
553 }
554}
555
556pub fn format_template(ix: &mut dyn Interop, template: &str, args: &[Value]) -> RtResult<String> {
559 let mut out = String::new();
560 let mut next = 0;
561 let mut rest = template;
562 while let Some(i) = rest.find(['{', '}']) {
563 out.push_str(&rest[..i]);
564 let c = rest.as_bytes()[i];
565 rest = &rest[i + 1..];
566 if rest.as_bytes().first() == Some(&c) {
567 out.push(c as char);
568 rest = &rest[1..];
569 continue;
570 }
571 if c == b'}' {
572 return panic("FormatError: unmatched `}` in format template; write `}}` for a literal brace");
573 }
574 let Some(close) = rest.find('}') else {
575 return panic("FormatError: unterminated `{` in format template; write `{{` for a literal brace");
576 };
577 let inner = &rest[..close];
578 rest = &rest[close + 1..];
579 let spec = match inner.strip_prefix(':') {
580 Some(s) => match parse(s) {
581 Ok(f) => f,
582 Err(e) => return panic(format!("FormatError: {} in `{{{inner}}}`", e.msg)),
583 },
584 None if inner.is_empty() => FormatSpec::default(),
585 None => return panic(format!("FormatError: expected `{{}}` or `{{:spec}}`, found `{{{inner}}}`")),
586 };
587 let Some(&v) = args.get(next) else {
588 return panic(format!("FormatError: the template needs more than {} argument(s)", args.len()));
589 };
590 next += 1;
591 if spec == FormatSpec::default() {
592 out.push_str(&ops::to_s(ix, v)?);
593 } else {
594 out.push_str(&format_value(ix, v, &spec)?);
595 }
596 }
597 out.push_str(rest);
598 if next < args.len() {
599 return panic(format!("FormatError: the template uses {next} argument(s) but {} were given", args.len()));
600 }
601 Ok(out)
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn parses_and_round_trips() {
610 for s in ["d", ">8.2f", "*^10", "+,d", "#x", "#010b", "_X", ".3e", "08,.1f", " 5", "=+9", "é<4s", ".0%"] {
611 let f = parse(s).unwrap_or_else(|e| panic!("{s}: {e:?}"));
612 assert_eq!(FormatSpec::decode(f.encode()), f, "{s}");
613 }
614 assert_eq!(parse(">8").unwrap().width, 8);
615 assert_eq!(parse("*^10").unwrap().fill, '*');
616 }
617
618 #[test]
619 fn rejects_bad_specs() {
620 assert!(parse("").is_err());
621 assert!(parse("8.").is_err());
622 assert!(parse("q").unwrap_err().msg.contains("unknown format type"));
623 assert!(parse("8.2fx").is_err());
624 assert!(parse("99999").is_err());
625 }
626
627 #[test]
628 fn checks_kinds() {
629 let f = parse(".2f").unwrap();
630 assert!(f.check(ValueKind::Float, "Float").is_ok());
631 assert!(f.check(ValueKind::Int, "Int").is_ok());
632 assert!(f.check(ValueKind::Text, "String").is_err());
633 assert!(parse("x").unwrap().check(ValueKind::Float, "Float").is_err());
634 assert!(parse(".2").unwrap().check(ValueKind::Int, "Int").is_err());
635 assert!(parse(".2").unwrap().check(ValueKind::Text, "String").is_ok());
636 assert!(parse("+").unwrap().check(ValueKind::Text, "String").is_err());
637 assert!(parse("#d").unwrap().check(ValueKind::Int, "Int").is_err());
638 assert!(parse(",x").unwrap().check(ValueKind::Int, "Int").is_err());
639 }
640
641 #[test]
642 fn groups_with_zero_padding() {
643 assert_eq!(group("1234", ',', 3, 0), "1,234");
644 assert_eq!(group("1234", ',', 3, 8), "0,001,234");
645 assert_eq!(group("123", ',', 3, 0), "123");
646 }
647
648 fn fmt_f(spec: &str, x: f64) -> String {
649 let mut out = String::new();
650 parse(spec).unwrap().write_float(&mut out, x);
651 out
652 }
653
654 fn fmt_t(spec: &str, s: &str) -> String {
655 let mut out = String::new();
656 parse(spec).unwrap().write_text(&mut out, s);
657 out
658 }
659
660 #[test]
661 fn formats_floats_and_text() {
662 assert_eq!(fmt_f(".2f", 2.34567), "2.35");
663 assert_eq!(fmt_f(">8.2f", -2.34567), " -2.35");
664 assert_eq!(fmt_f("08.2f", -2.34567), "-0002.35");
665 assert_eq!(fmt_f(",.2f", 1234567.891), "1,234,567.89");
666 assert_eq!(fmt_f(".3e", 1234.5), "1.234e+03");
667 assert_eq!(fmt_f("E", 0.00012), "1.200000E-04");
668 assert_eq!(fmt_f(".1%", 0.256), "25.6%");
669 assert_eq!(fmt_f("+", 2.5), "+2.5");
670 assert_eq!(fmt_f("f", f64::NAN), "NaN");
671 assert_eq!(fmt_t("*^7", "abc"), "**abc**");
672 assert_eq!(fmt_t("6", "ab"), "ab ");
673 assert_eq!(fmt_t(".2", "héllo"), "hé");
674 }
675}