Skip to main content

catallaxy_runtime/
fmtspec.rs

1//! Format specifications: the mini-language after the colon in `"#{x:>8.2f}"`.
2//!
3//! ```text
4//! spec      := [[fill] align] [sign] ["#"] ["0"] [width] [grouping] ["." precision] [type]
5//! align     := "<" | ">" | "^" | "="
6//! sign      := "+" | "-" | " "
7//! grouping  := "," | "_"
8//! type      := "s" | "d" | "x" | "X" | "o" | "b" | "e" | "E" | "f" | "F" | "%"
9//! ```
10//!
11//! The compiler parses a spec once, checks it against the interpolated expression's static type
12//! and encodes it into two instruction data words ([`FormatSpec::encode`]); the VM decodes those
13//! with a few shifts ([`FormatSpec::decode`]) and calls [`format_value`]. Nothing is reparsed at
14//! run time. `String.format` parses its template at run time with the same [`parse`].
15
16use 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    /// Numbers right, text left.
25    Default,
26    Left,
27    Right,
28    Center,
29    /// Padding goes between the sign (and `0x`-style prefix) and the digits.
30    AfterSign,
31}
32
33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub enum Sign {
35    /// `-`: only negative numbers get a sign (the default).
36    Minus,
37    /// `+`: always write a sign.
38    Plus,
39    /// ` `: a space in front of non-negative numbers.
40    Space,
41}
42
43#[derive(Copy, Clone, Debug, PartialEq, Eq)]
44pub enum Grouping {
45    None,
46    /// `,` every three decimal digits.
47    Comma,
48    /// `_` every three decimal digits, or every four hex/octal/binary digits.
49    Underscore,
50}
51
52#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub enum FmtType {
54    /// No type letter: `d` for Int, the shortest form (or `f` with a precision) for Float, text
55    /// for everything else.
56    Default,
57    /// `s`: the value's `to_s`.
58    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
134/// The largest width; precision is limited to one less (it is stored plus one).
135pub 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    /// `#`: `0x`/`0o`/`0b` prefix.
143    pub alt: bool,
144    /// `0`: pad with zeros after the sign.
145    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/// A malformed spec: the message and the byte range in the spec text it refers to.
169#[derive(Clone, Debug, PartialEq, Eq)]
170pub struct SpecError {
171    pub msg: String,
172    pub start: usize,
173    pub end: usize,
174}
175
176/// What a value formats as.
177#[derive(Copy, Clone, Debug, PartialEq, Eq)]
178pub enum ValueKind {
179    Int,
180    Float,
181    /// Anything else, formatted through `to_s`.
182    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
195/// Parses a spec (the text after the colon).
196pub 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    /// Packs the spec into two instruction data words.
279    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    /// Unpacks [`FormatSpec::encode`]'s words.
292    #[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    /// Whether the spec can format a value of `kind`; `type_name` names it in the error.
313    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
363/// The kind of a runtime value, for [`FormatSpec::check`].
364pub 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
374/// Formats `v` per `spec`, panicking with a `FormatError` if the spec does not apply to it (the
375/// compiler rejects that statically unless the value is `Dyn` or generic).
376pub 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
394/// Inserts `sep` every `every` digits from the right of `digits`. With `min_len`, pads `digits`
395/// with leading zeros (regrouping as it goes) until the result is at least that long.
396fn 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    /// Pads `sign prefix body` to the width.
425    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    /// With the `0` flag and grouping, the zero padding is grouped too (`0,001,234`).
465    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
556/// Formats a runtime template: `{}` takes the next argument, `{:spec}` formats it with a spec,
557/// `{{` and `}}` are literal braces. Errors (a bad spec, too few or too many arguments) panic.
558pub 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}