Skip to main content

ferroui_base/data/converters/
composite_format.rs

1//! Composite formatting: the `string.Format` of the managed runtime.
2//! [`format`] and [`format_arg`] use the invariant culture, the `_with`
3//! forms the number and date formats of a [`CultureInfo`] and
4//! [`format_values`] those of the current culture.
5//!
6//! A composite format string is literal text with format items
7//! `{index[,alignment][:formatString]}`. `{{` and `}}` are literal braces.
8//! The results match the managed runtime (version 10) character for
9//! character for the supported argument types.
10//!
11//! # Supported
12//!
13//! - Format items: index, repeated and out-of-order indices, alignment
14//!   (`{0,10}` right-aligns, `{0,-10}` left-aligns; the width counts UTF-16
15//!   code units), format strings, `{{` / `}}` escapes, spaces after the index
16//!   and around the alignment. A `}` ends the format string of an item and a
17//!   `{` inside it is an error, as in the managed runtime.
18//! - Arguments: null (empty text), strings, booleans (`True` / `False`),
19//!   characters, signed and unsigned integers and floating point numbers.
20//!   Strings, booleans and characters ignore the format string.
21//! - Default number formatting (no format string): integers as decimal
22//!   digits; floating point numbers as the shortest text that round-trips,
23//!   switching to exponent notation (`1E+17`, `1E-05`) when the value is
24//!   below `0.0001` or its integer part has more digits than both the text
25//!   and the round-trip precision (17 for `f64`, 9 for `f32`); `NaN`,
26//!   `Infinity`, `-Infinity`, `-0`.
27//! - Standard numeric format strings, upper and lower case, with an optional
28//!   precision of up to 999,999,999: `C` (currency symbol `¤`, negative in
29//!   parentheses), `D` (integers only), `E`, `F`, `G`, `N`, `P` (`12.50 %`),
30//!   `R`, `X` (integers only; negative values in two's complement of the
31//!   integer's width), `B` (integers only).
32//! - Custom numeric format strings: `0`, `#`, `.`, `,` as group separator and
33//!   as number scaling, `%`, `‰`, `;` sections (positive; negative; zero),
34//!   `E0` / `E+0` / `E-0` / `e0` / `e+0` / `e-0` exponents, quoted literals
35//!   (`'..'`, `".."`), `\` escapes; every other character is copied.
36//! - Rounding: standard format strings round floating point numbers
37//!   correctly from the exact binary value, exact midpoints going to the even
38//!   digit (`0.125` with `F2` is `0.12`, `2.5` with `F0` is `2`); custom
39//!   format strings first take 15 (`f64`) or 7 (`f32`) significant digits and
40//!   then round midpoints away from zero (`2.5` with `0` is `3`); integers
41//!   round midpoints away from zero (`12350` with `E2` is `1.24E+004`).
42//!
43//! - Decimals (standard and custom numeric format strings), dates and
44//!   times (`DateTime`, `DateTimeOffset`: standard and custom date and time
45//!   format strings) and time spans, formatted by their own types with the
46//!   format string of the item.
47//! - Cultures: decimal and group separators, group sizes, the negative and
48//!   positive signs, the currency and percent symbols and patterns, the
49//!   default numbers of decimal digits and the symbols of the values that
50//!   are not numbers come from the `NumberFormatInfo` of the culture; dates
51//!   use its `DateTimeFormatInfo`.
52//!
53//! # Not supported
54//!
55//! - Custom format providers.
56//! - GUID and enumeration format strings and arbitrary precision numbers.
57//!   Values of such types are formatted through their registered display
58//!   form and ignore the format string of the item. The `g` / `G` formats
59//!   of a time span use `.` as the decimal separator in every culture.
60//! - 128-bit integers and half precision floats.
61
62use crate::data::core::ValueTypes;
63use crate::data::BindingNotification;
64use crate::utilities::number_format::{
65    pattern, NEGATIVE_CURRENCY_FORMATS, NEGATIVE_NUMBER_FORMATS, NEGATIVE_PERCENT_FORMATS, POSITIVE_CURRENCY_FORMATS,
66    POSITIVE_PERCENT_FORMATS,
67};
68use crate::animation::TimeSpan;
69use crate::utilities::{CultureInfo, DateTime, DateTimeOffset, Decimal, NumberFormatInfo};
70use crate::{BoxedValue, DoNothingType, UnsetValueType};
71use std::fmt;
72
73/// An argument of a composite format operation.
74#[derive(Clone, Copy, Debug, PartialEq)]
75pub enum FormatArg<'a> {
76    /// A null reference: formats as empty text.
77    Null,
78    /// A string: ignores the format string of the item.
79    Str(&'a str),
80    /// A boolean: `True` or `False`.
81    Bool(bool),
82    /// A character.
83    Char(char),
84    /// An 8-bit signed integer (the width matters for `X` and `B`).
85    I8(i8),
86    /// A 16-bit signed integer (the width matters for `X` and `B`).
87    I16(i16),
88    /// A 32-bit signed integer (the width matters for `X` and `B`).
89    I32(i32),
90    /// A 64-bit signed integer.
91    I64(i64),
92    /// An unsigned integer of any width.
93    U64(u64),
94    /// A double precision floating point number.
95    F64(f64),
96    /// A single precision floating point number.
97    F32(f32),
98    /// A decimal (128-bit) number.
99    Decimal(Decimal),
100    /// A date and time: formatted with date and time format strings.
101    DateTime(DateTime),
102    /// A date and time with an offset from UTC.
103    DateTimeOffset(DateTimeOffset),
104    /// A time interval: formatted with time span format strings.
105    TimeSpan(TimeSpan),
106}
107
108/// The error of a composite format operation: the equivalent of the format
109/// exception of the managed runtime.
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
111pub enum FormatError {
112    /// The composite format string is malformed: unbalanced braces or a
113    /// malformed format item.
114    InvalidFormat,
115    /// A format item refers to an argument that does not exist.
116    IndexOutOfRange,
117    /// A numeric format string is not valid for the argument.
118    InvalidFormatSpecifier,
119}
120
121impl fmt::Display for FormatError {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.write_str(match self {
124            FormatError::InvalidFormat => "Input string was not in a correct format.",
125            FormatError::IndexOutOfRange => {
126                "Index (zero based) must be greater than or equal to zero and less than the size of the argument list."
127            }
128            FormatError::InvalidFormatSpecifier => "Format specifier was invalid.",
129        })
130    }
131}
132
133impl std::error::Error for FormatError {}
134
135const INDEX_LIMIT: usize = 1_000_000;
136const WIDTH_LIMIT: usize = 1_000_000;
137
138/// Replaces the format items of `format` with the text of the matching
139/// arguments.
140pub fn format(format: &str, args: &[FormatArg<'_>]) -> Result<String, FormatError> {
141    format_with(format, args, &CultureInfo::invariant_culture())
142}
143
144/// Replaces the format items of `format` with the text of the matching
145/// arguments, formatted with the conventions of `culture`: the
146/// `string.Format(IFormatProvider, ..)` of the managed runtime.
147pub fn format_with(format: &str, args: &[FormatArg<'_>], culture: &CultureInfo) -> Result<String, FormatError> {
148    fn next(bytes: &[u8], pos: &mut usize) -> Result<u8, FormatError> {
149        *pos += 1;
150        bytes.get(*pos).copied().ok_or(FormatError::InvalidFormat)
151    }
152
153    let bytes = format.as_bytes();
154    let len = bytes.len();
155    let mut out = String::with_capacity(len + args.len() * 8);
156    let mut pos = 0;
157    loop {
158        // Literal text up to the next brace. Braces are ASCII, so every
159        // position the scan stops at is a character boundary.
160        let start = pos;
161        while pos < len && bytes[pos] != b'{' && bytes[pos] != b'}' {
162            pos += 1;
163        }
164        out.push_str(&format[start..pos]);
165        if pos >= len {
166            return Ok(out);
167        }
168
169        let brace = bytes[pos];
170        let mut ch = next(bytes, &mut pos)?;
171        if ch == brace {
172            out.push(brace as char);
173            pos += 1;
174            continue;
175        }
176        if brace != b'{' {
177            return Err(FormatError::InvalidFormat);
178        }
179
180        // The argument index.
181        if !ch.is_ascii_digit() {
182            return Err(FormatError::InvalidFormat);
183        }
184        let mut index = usize::from(ch - b'0');
185        ch = next(bytes, &mut pos)?;
186        let mut width = 0usize;
187        let mut left_justify = false;
188        let mut item_format = "";
189        if ch != b'}' {
190            while ch.is_ascii_digit() && index < INDEX_LIMIT {
191                index = index * 10 + usize::from(ch - b'0');
192                ch = next(bytes, &mut pos)?;
193            }
194            while ch == b' ' {
195                ch = next(bytes, &mut pos)?;
196            }
197
198            // The alignment.
199            if ch == b',' {
200                loop {
201                    ch = next(bytes, &mut pos)?;
202                    if ch != b' ' {
203                        break;
204                    }
205                }
206                if ch == b'-' {
207                    left_justify = true;
208                    ch = next(bytes, &mut pos)?;
209                }
210                if !ch.is_ascii_digit() {
211                    return Err(FormatError::InvalidFormat);
212                }
213                width = usize::from(ch - b'0');
214                ch = next(bytes, &mut pos)?;
215                while ch.is_ascii_digit() && width < WIDTH_LIMIT {
216                    width = width * 10 + usize::from(ch - b'0');
217                    ch = next(bytes, &mut pos)?;
218                }
219                while ch == b' ' {
220                    ch = next(bytes, &mut pos)?;
221                }
222            }
223
224            // The format string.
225            if ch != b'}' {
226                if ch != b':' {
227                    return Err(FormatError::InvalidFormat);
228                }
229                let format_start = pos + 1;
230                loop {
231                    ch = next(bytes, &mut pos)?;
232                    if ch == b'}' {
233                        break;
234                    }
235                    if ch == b'{' {
236                        return Err(FormatError::InvalidFormat);
237                    }
238                }
239                item_format = &format[format_start..pos];
240            }
241        }
242        pos += 1;
243
244        let arg = args.get(index).ok_or(FormatError::IndexOutOfRange)?;
245        if width == 0 {
246            write_arg(&mut out, arg, item_format, culture)?;
247        } else {
248            let mut text = String::new();
249            write_arg(&mut text, arg, item_format, culture)?;
250            let padding = width.saturating_sub(text.encode_utf16().count());
251            if left_justify {
252                out.push_str(&text);
253                out.extend(std::iter::repeat_n(' ', padding));
254            } else {
255                out.extend(std::iter::repeat_n(' ', padding));
256                out.push_str(&text);
257            }
258        }
259    }
260}
261
262/// Formats one argument with a numeric format string: the equivalent of
263/// `ToString(format)` with the invariant culture.
264pub fn format_arg(arg: &FormatArg<'_>, format: &str) -> Result<String, FormatError> {
265    format_arg_with(arg, format, &CultureInfo::invariant_culture())
266}
267
268/// Formats one argument with a format string and the conventions of
269/// `culture`: the equivalent of `ToString(format, culture)`.
270pub fn format_arg_with(arg: &FormatArg<'_>, format: &str, culture: &CultureInfo) -> Result<String, FormatError> {
271    let mut out = String::new();
272    write_arg(&mut out, arg, format, culture)?;
273    Ok(out)
274}
275
276/// Replaces the format items of `format` with the text of the matching
277/// untyped values.
278///
279/// Nullable (`Option<T>`) values are unwrapped first. Integers, floating
280/// point numbers, booleans, characters and strings are formatted as the
281/// matching [`FormatArg`]; every other value is formatted as its display form
282/// ([`ValueTypes::to_display_string`]) and ignores the format string of the
283/// item.
284///
285/// Numbers, decimals and dates are formatted with the conventions of the
286/// current culture (`string.Format` without a provider); see
287/// [`format_values_with`] for another culture.
288pub fn format_values(format_string: &str, values: &[Option<BoxedValue>]) -> Result<String, FormatError> {
289    format_values_with(format_string, values, &CultureInfo::current_culture())
290}
291
292/// [`format_values`] with the conventions of `culture`.
293pub fn format_values_with(
294    format_string: &str,
295    values: &[Option<BoxedValue>],
296    culture: &CultureInfo,
297) -> Result<String, FormatError> {
298    enum Owned {
299        Arg(FormatArg<'static>),
300        Text(String),
301    }
302
303    fn to_owned(value: &Option<BoxedValue>) -> Owned {
304        let Some(value) = value.clone().and_then(ValueTypes::normalize) else {
305            return Owned::Arg(FormatArg::Null);
306        };
307        macro_rules! primitive {
308            ($($ty:ty => |$v:ident| $arg:expr),* $(,)?) => {
309                $(
310                    if let Some($v) = value.downcast_ref::<$ty>() {
311                        return Owned::Arg($arg);
312                    }
313                )*
314            };
315        }
316        primitive!(
317            i32 => |v| FormatArg::I32(*v),
318            f64 => |v| FormatArg::F64(*v),
319            bool => |v| FormatArg::Bool(*v),
320            i64 => |v| FormatArg::I64(*v),
321            f32 => |v| FormatArg::F32(*v),
322            u8 => |v| FormatArg::U64(u64::from(*v)),
323            u16 => |v| FormatArg::U64(u64::from(*v)),
324            u32 => |v| FormatArg::U64(u64::from(*v)),
325            u64 => |v| FormatArg::U64(*v),
326            usize => |v| FormatArg::U64(*v as u64),
327            i8 => |v| FormatArg::I8(*v),
328            i16 => |v| FormatArg::I16(*v),
329            isize => |v| FormatArg::I64(*v as i64),
330            char => |v| FormatArg::Char(*v),
331            Decimal => |v| FormatArg::Decimal(*v),
332            DateTime => |v| FormatArg::DateTime(*v),
333            DateTimeOffset => |v| FormatArg::DateTimeOffset(*v),
334            TimeSpan => |v| FormatArg::TimeSpan(*v),
335            &'static str => |v| FormatArg::Str(v),
336        );
337        if let Some(s) = value.downcast_ref::<String>() {
338            return Owned::Text(s.clone());
339        }
340        if let Some(v) = value.downcast_ref::<UnsetValueType>() {
341            return Owned::Text(v.to_string());
342        }
343        if let Some(v) = value.downcast_ref::<DoNothingType>() {
344            return Owned::Text(v.to_string());
345        }
346        if let Some(v) = value.downcast_ref::<BindingNotification>() {
347            return Owned::Text(v.to_string());
348        }
349        Owned::Text(ValueTypes::to_display_string(Some(&value)))
350    }
351
352    let owned: Vec<Owned> = values.iter().map(to_owned).collect();
353    let args: Vec<FormatArg<'_>> = owned
354        .iter()
355        .map(|o| match o {
356            Owned::Arg(arg) => *arg,
357            Owned::Text(text) => FormatArg::Str(text),
358        })
359        .collect();
360    format_with(format_string, &args, culture)
361}
362
363fn write_arg(out: &mut String, arg: &FormatArg<'_>, format: &str, culture: &CultureInfo) -> Result<(), FormatError> {
364    let numbers = culture.number_format();
365    let info: &NumberFormatInfo = &numbers;
366    match *arg {
367        FormatArg::Null => {}
368        FormatArg::Str(s) => out.push_str(s),
369        FormatArg::Bool(b) => out.push_str(if b { "True" } else { "False" }),
370        FormatArg::Char(c) => out.push(c),
371        FormatArg::I8(v) => format_integer(out, v < 0, u64::from(v.unsigned_abs()), u64::from(v as u8), format, info)?,
372        FormatArg::I16(v) => format_integer(out, v < 0, u64::from(v.unsigned_abs()), u64::from(v as u16), format, info)?,
373        FormatArg::I32(v) => format_integer(out, v < 0, u64::from(v.unsigned_abs()), u64::from(v as u32), format, info)?,
374        FormatArg::I64(v) => format_integer(out, v < 0, v.unsigned_abs(), v as u64, format, info)?,
375        FormatArg::U64(v) => format_integer(out, false, v, v, format, info)?,
376        FormatArg::F64(v) => format_float(out, Float::F64(v), format, info)?,
377        FormatArg::F32(v) => format_float(out, Float::F32(v), format, info)?,
378        FormatArg::Decimal(v) => out.push_str(&v.to_string_with(format, Some(info))?),
379        // An invalid date and time format string is the format exception.
380        FormatArg::DateTime(v) => {
381            out.push_str(&v.try_to_string_format(format, culture).map_err(|_| FormatError::InvalidFormat)?)
382        }
383        FormatArg::DateTimeOffset(v) => {
384            out.push_str(&v.try_to_string_format(format, culture).map_err(|_| FormatError::InvalidFormat)?)
385        }
386        FormatArg::TimeSpan(v) => {
387            out.push_str(&v.try_to_string_format(format).map_err(|_| FormatError::InvalidFormat)?)
388        }
389    }
390    Ok(())
391}
392
393// ---------------------------------------------------------------------------
394// Numbers
395// ---------------------------------------------------------------------------
396
397/// A number as decimal digits: `0.d1d2d3.. * 10^scale`. Zero has no digits
398/// and scale 0.
399struct Number {
400    /// ASCII digits.
401    digits: Vec<u8>,
402    scale: i32,
403    negative: bool,
404    /// A floating point number: keeps its sign when it is (or rounds to)
405    /// zero, and has been rounded correctly already for standard formats.
406    floating: bool,
407}
408
409impl Number {
410    fn from_integer(negative: bool, abs: u64) -> Self {
411        let digits = if abs == 0 { Vec::new() } else { abs.to_string().into_bytes() };
412        let scale = digits.len() as i32;
413        // Trailing zeros stay: the digit count is the default precision of
414        // the general format.
415        Number { digits, scale, negative, floating: false }
416    }
417
418    /// From the output of exponent formatting: `d.ddd..e[-]x`.
419    fn from_exponent_text(text: &str, negative: bool) -> Self {
420        let (mantissa, exponent) = text.split_once('e').unwrap_or((text, "0"));
421        let exponent: i32 = exponent.parse().unwrap_or(0);
422        let digits: Vec<u8> = mantissa.bytes().filter(u8::is_ascii_digit).collect();
423        let mut number = Number { digits, scale: exponent + 1, negative, floating: true };
424        number.trim();
425        if number.digits.is_empty() {
426            number.scale = 0;
427        }
428        number
429    }
430
431    /// From the output of fixed formatting: `ddd.ddd`.
432    fn from_fixed_text(text: &str, negative: bool) -> Self {
433        let integer_len = text.find('.').unwrap_or(text.len());
434        let all: Vec<u8> = text.bytes().filter(u8::is_ascii_digit).collect();
435        let leading_zeros = all.iter().take_while(|&&d| d == b'0').count();
436        let digits = all[leading_zeros..].to_vec();
437        let mut number =
438            Number { digits, scale: integer_len as i32 - leading_zeros as i32, negative, floating: true };
439        number.trim();
440        if number.digits.is_empty() {
441            number.scale = 0;
442        }
443        number
444    }
445
446    fn trim(&mut self) {
447        while self.digits.last() == Some(&b'0') {
448            self.digits.pop();
449        }
450    }
451
452    /// The digit at `index`, `'0'` after the last digit.
453    #[inline]
454    fn digit(&self, index: usize) -> char {
455        self.digits.get(index).map_or('0', |&d| d as char)
456    }
457
458    #[inline]
459    fn is_zero(&self) -> bool {
460        self.digits.is_empty()
461    }
462}
463
464/// Rounds a number to `pos` digits. Numbers that were rounded correctly from
465/// their exact value are only truncated; others round midpoints up.
466fn round_number(number: &mut Number, pos: i64, is_correctly_rounded: bool) {
467    let len = number.digits.len();
468    let mut i = if pos <= 0 { 0 } else { len.min(usize::try_from(pos).unwrap_or(usize::MAX)) };
469    let at_pos = pos >= 0 && i as i64 == pos;
470    let round_up = at_pos && i < len && !is_correctly_rounded && number.digits[i] >= b'5';
471    if round_up {
472        while i > 0 && number.digits[i - 1] == b'9' {
473            i -= 1;
474        }
475        if i > 0 {
476            number.digits[i - 1] += 1;
477        } else {
478            number.scale += 1;
479            if number.digits.is_empty() {
480                number.digits.push(b'1');
481            } else {
482                number.digits[0] = b'1';
483            }
484            i = 1;
485        }
486    } else {
487        while i > 0 && number.digits[i - 1] == b'0' {
488            i -= 1;
489        }
490    }
491    if i == 0 {
492        if !number.floating {
493            number.negative = false;
494        }
495        number.scale = 0;
496    }
497    number.digits.truncate(i);
498}
499
500enum Specifier {
501    /// A standard format: the specifier and its precision (-1 if absent).
502    Standard(u8, i32),
503    Custom,
504}
505
506fn parse_format_specifier(format: &str) -> Result<Specifier, FormatError> {
507    let bytes = format.as_bytes();
508    let Some(&c) = bytes.first() else {
509        return Ok(Specifier::Standard(b'G', -1));
510    };
511    if c.is_ascii_alphabetic() {
512        let mut n: i32 = 0;
513        let mut i = 1;
514        while i < bytes.len() && bytes[i].is_ascii_digit() {
515            if n >= 100_000_000 {
516                return Err(FormatError::InvalidFormatSpecifier);
517            }
518            n = n * 10 + i32::from(bytes[i] - b'0');
519            i += 1;
520        }
521        if i == bytes.len() || bytes[i] == 0 {
522            return Ok(Specifier::Standard(c, if i == 1 { -1 } else { n }));
523        }
524    }
525    Ok(if c == 0 { Specifier::Standard(b'G', -1) } else { Specifier::Custom })
526}
527
528fn push_zeros(out: &mut String, count: usize) {
529    out.extend(std::iter::repeat_n('0', count));
530}
531
532/// `twos` is the two's complement bit pattern of the value in its own width.
533fn format_integer(
534    out: &mut String,
535    negative: bool,
536    abs: u64,
537    twos: u64,
538    format: &str,
539    info: &NumberFormatInfo,
540) -> Result<(), FormatError> {
541    let (specifier, precision) = match parse_format_specifier(format)? {
542        Specifier::Standard(specifier, precision) => (specifier, precision),
543        Specifier::Custom => {
544            let mut number = Number::from_integer(negative, abs);
545            let format: Vec<char> = format.chars().collect();
546            number_to_string_format(out, &mut number, &format, info);
547            return Ok(());
548        }
549    };
550    let min_digits = usize::try_from(precision).unwrap_or(0);
551    let padded = |out: &mut String, digits: &str| {
552        push_zeros(out, min_digits.saturating_sub(digits.len()));
553        out.push_str(digits);
554    };
555    match specifier.to_ascii_uppercase() {
556        b'G' if precision < 1 => {
557            if negative {
558                out.push_str(info.negative_sign());
559            }
560            out.push_str(&abs.to_string());
561        }
562        b'D' => {
563            if negative {
564                out.push_str(info.negative_sign());
565            }
566            padded(out, &abs.to_string());
567        }
568        b'X' => {
569            let digits = if specifier == b'X' { format!("{twos:X}") } else { format!("{twos:x}") };
570            padded(out, &digits);
571        }
572        b'B' => padded(out, &format!("{twos:b}")),
573        _ => {
574            let mut number = Number::from_integer(negative, abs);
575            number_to_string(out, &mut number, specifier, precision, info)?;
576        }
577    }
578    Ok(())
579}
580
581#[derive(Clone, Copy)]
582enum Float {
583    F64(f64),
584    F32(f32),
585}
586
587impl Float {
588    fn is_nan(self) -> bool {
589        match self {
590            Float::F64(v) => v.is_nan(),
591            Float::F32(v) => v.is_nan(),
592        }
593    }
594
595    fn is_infinite(self) -> bool {
596        match self {
597            Float::F64(v) => v.is_infinite(),
598            Float::F32(v) => v.is_infinite(),
599        }
600    }
601
602    fn is_negative(self) -> bool {
603        match self {
604            Float::F64(v) => v.is_sign_negative(),
605            Float::F32(v) => v.is_sign_negative(),
606        }
607    }
608
609    fn is_zero(self) -> bool {
610        match self {
611            Float::F64(v) => v == 0.0,
612            Float::F32(v) => v == 0.0,
613        }
614    }
615
616    /// The number of significant digits custom formats work with.
617    fn custom_format_precision(self) -> i32 {
618        match self {
619            Float::F64(_) => 15,
620            Float::F32(_) => 7,
621        }
622    }
623
624    /// The number of digits that always round-trip.
625    fn max_round_trip_digits(self) -> i32 {
626        match self {
627            Float::F64(_) => 17,
628            Float::F32(_) => 9,
629        }
630    }
631
632    /// The shortest digits that round-trip. When the exact value lies halfway
633    /// between two candidates of that length the one with the even last digit
634    /// is used, provided it round-trips.
635    fn shortest(self) -> Number {
636        // The managed runtime computes the lower rounding boundary of powers
637        // of two as if it were as far away as the upper one. Of all `f64` and
638        // `f32` powers of two this changes the result of exactly two values,
639        // which get one digit less than needed to round-trip.
640        if let Float::F64(v) = self {
641            let quirk = match v.abs().to_bits() {
642                0x3E60_0000_0000_0000 => Some("2.980232238769531e-8"), // 2^-25
643                0x0410_0000_0000_0000 => Some("4.104536801298376e-289"), // 2^-958
644                _ => None,
645            };
646            if let Some(text) = quirk {
647                return Number::from_exponent_text(text, self.is_negative());
648            }
649        }
650        let text = match self {
651            Float::F64(v) => format!("{:e}", v.abs()),
652            Float::F32(v) => format!("{:e}", v.abs()),
653        };
654        let shortest = Number::from_exponent_text(&text, self.is_negative());
655        let mantissa_len = text.find('e').unwrap_or(text.len());
656        let decimals = mantissa_len.saturating_sub(2);
657        let rounded_text = match self {
658            Float::F64(v) => format!("{:.*e}", decimals, v.abs()),
659            Float::F32(v) => format!("{:.*e}", decimals, v.abs()),
660        };
661        if rounded_text == text {
662            return shortest;
663        }
664        let round_trips = match self {
665            Float::F64(v) => rounded_text.parse::<f64>().is_ok_and(|r| r == v.abs()),
666            Float::F32(v) => rounded_text.parse::<f32>().is_ok_and(|r| r == v.abs()),
667        };
668        if round_trips {
669            Number::from_exponent_text(&rounded_text, self.is_negative())
670        } else {
671            shortest
672        }
673    }
674
675    /// The exact value rounded to `count` significant digits.
676    fn significant_digits(self, count: i32) -> Number {
677        // No value has more significant digits than this: more are zeros.
678        let decimals = (count.clamp(1, 800) - 1) as usize;
679        let text = match self {
680            Float::F64(v) => format!("{:.*e}", decimals, v.abs()),
681            Float::F32(v) => format!("{:.*e}", decimals, v.abs()),
682        };
683        Number::from_exponent_text(&text, self.is_negative())
684    }
685
686    /// The exact value rounded to `count` digits after the decimal point.
687    fn decimal_digits(self, count: i32) -> Number {
688        // No value has more decimal digits than this: more are zeros.
689        let decimals = count.clamp(0, 1100) as usize;
690        let text = match self {
691            Float::F64(v) => format!("{:.*}", decimals, v.abs()),
692            Float::F32(v) => format!("{:.*}", decimals, v.abs()),
693        };
694        Number::from_fixed_text(&text, self.is_negative())
695    }
696}
697
698fn format_float(out: &mut String, value: Float, format: &str, info: &NumberFormatInfo) -> Result<(), FormatError> {
699    if value.is_nan() {
700        out.push_str(info.nan_symbol());
701        return Ok(());
702    }
703    if value.is_infinite() {
704        out.push_str(if value.is_negative() {
705            info.negative_infinity_symbol()
706        } else {
707            info.positive_infinity_symbol()
708        });
709        return Ok(());
710    }
711
712    let (specifier, precision) = match parse_format_specifier(format)? {
713        Specifier::Standard(specifier, precision) => (specifier, precision),
714        Specifier::Custom => {
715            let mut number = if value.is_zero() {
716                Number { digits: Vec::new(), scale: 0, negative: value.is_negative(), floating: true }
717            } else {
718                value.significant_digits(value.custom_format_precision())
719            };
720            let format: Vec<char> = format.chars().collect();
721            number_to_string_format(out, &mut number, &format, info);
722            return Ok(());
723        }
724    };
725
726    enum Digits {
727        Shortest,
728        Significant(i32),
729        Decimals(i32),
730    }
731    let or_default = |default: i32| if precision == -1 { default } else { precision };
732    let digits = match specifier.to_ascii_uppercase() {
733        b'C' => Digits::Decimals(or_default(info.currency_decimal_digits())),
734        b'F' | b'N' => Digits::Decimals(or_default(info.number_decimal_digits())),
735        b'E' => Digits::Significant(or_default(6).saturating_add(1)),
736        b'G' if precision < 1 => Digits::Shortest,
737        b'G' => Digits::Significant(precision),
738        b'P' => Digits::Decimals(or_default(info.percent_decimal_digits()).saturating_add(2)),
739        b'R' => Digits::Shortest,
740        _ => return Err(FormatError::InvalidFormatSpecifier),
741    };
742
743    let mut max_digits = precision;
744    let mut number = if value.is_zero() {
745        Number { digits: Vec::new(), scale: 0, negative: value.is_negative(), floating: true }
746    } else {
747        match digits {
748            Digits::Shortest => value.shortest(),
749            Digits::Significant(count) => value.significant_digits(count),
750            Digits::Decimals(count) => value.decimal_digits(count),
751        }
752    };
753    if matches!(digits, Digits::Shortest) {
754        // Keeps short values "pretty": without this 60 would be 6E+01.
755        max_digits = (number.digits.len() as i32).max(value.max_round_trip_digits());
756    }
757    number_to_string(out, &mut number, specifier, max_digits, info)
758}
759
760// ---------------------------------------------------------------------------
761// Standard numeric format strings
762// ---------------------------------------------------------------------------
763
764const PER_MILLE_SYMBOL: char = '\u{2030}';
765
766/// Writes a number laid out by a pattern of the format information (`($#)`,
767/// `-# %`): `#` is the digits, `-` the negative sign, `$` and `%` the
768/// symbols; everything else is copied.
769fn write_pattern(
770    out: &mut String,
771    pattern: &str,
772    info: &NumberFormatInfo,
773    symbol: &str,
774    mut digits: impl FnMut(&mut String),
775) {
776    for ch in pattern.chars() {
777        match ch {
778            '#' => digits(out),
779            '-' => out.push_str(info.negative_sign()),
780            '$' | '%' => out.push_str(symbol),
781            other => out.push(other),
782        }
783    }
784}
785
786fn number_to_string(
787    out: &mut String,
788    number: &mut Number,
789    format: u8,
790    max_digits: i32,
791    info: &NumberFormatInfo,
792) -> Result<(), FormatError> {
793    let correctly_rounded = number.floating;
794    let mut max_digits = max_digits;
795    match format {
796        b'C' | b'c' => {
797            if max_digits < 0 {
798                max_digits = info.currency_decimal_digits();
799            }
800            round_number(number, i64::from(number.scale) + i64::from(max_digits), correctly_rounded);
801            let layout = if number.negative {
802                pattern(&NEGATIVE_CURRENCY_FORMATS, info.currency_negative_pattern())
803            } else {
804                pattern(&POSITIVE_CURRENCY_FORMATS, info.currency_positive_pattern())
805            };
806            write_pattern(out, layout, info, info.currency_symbol(), |out| {
807                format_fixed(
808                    out,
809                    number,
810                    max_digits,
811                    Some(info.currency_group_sizes()),
812                    info.currency_decimal_separator(),
813                    info.currency_group_separator(),
814                )
815            });
816        }
817        b'F' | b'f' => {
818            if max_digits < 0 {
819                max_digits = info.number_decimal_digits();
820            }
821            round_number(number, i64::from(number.scale) + i64::from(max_digits), correctly_rounded);
822            if number.negative {
823                out.push_str(info.negative_sign());
824            }
825            format_fixed(out, number, max_digits, None, info.number_decimal_separator(), "");
826        }
827        b'N' | b'n' => {
828            if max_digits < 0 {
829                max_digits = info.number_decimal_digits();
830            }
831            round_number(number, i64::from(number.scale) + i64::from(max_digits), correctly_rounded);
832            let layout =
833                if number.negative { pattern(&NEGATIVE_NUMBER_FORMATS, info.number_negative_pattern()) } else { "#" };
834            write_pattern(out, layout, info, "", |out| {
835                format_fixed(
836                    out,
837                    number,
838                    max_digits,
839                    Some(info.number_group_sizes()),
840                    info.number_decimal_separator(),
841                    info.number_group_separator(),
842                )
843            });
844        }
845        b'E' | b'e' => {
846            if max_digits < 0 {
847                max_digits = 6;
848            }
849            max_digits = max_digits.saturating_add(1);
850            round_number(number, i64::from(max_digits), correctly_rounded);
851            if number.negative {
852                out.push_str(info.negative_sign());
853            }
854            format_scientific(out, number, max_digits, format as char, info);
855        }
856        b'G' | b'g' | b'R' | b'r' => {
857            if max_digits < 1 {
858                max_digits = number.digits.len() as i32;
859            }
860            round_number(number, i64::from(max_digits), correctly_rounded);
861            if number.negative {
862                out.push_str(info.negative_sign());
863            }
864            let exponent_char = if format.is_ascii_uppercase() { 'E' } else { 'e' };
865            format_general(out, number, max_digits, exponent_char, info);
866        }
867        b'P' | b'p' => {
868            if max_digits < 0 {
869                max_digits = info.percent_decimal_digits();
870            }
871            number.scale += 2;
872            round_number(number, i64::from(number.scale) + i64::from(max_digits), correctly_rounded);
873            let layout = if number.negative {
874                pattern(&NEGATIVE_PERCENT_FORMATS, info.percent_negative_pattern())
875            } else {
876                pattern(&POSITIVE_PERCENT_FORMATS, info.percent_positive_pattern())
877            };
878            write_pattern(out, layout, info, info.percent_symbol(), |out| {
879                format_fixed(
880                    out,
881                    number,
882                    max_digits,
883                    Some(info.percent_group_sizes()),
884                    info.percent_decimal_separator(),
885                    info.percent_group_separator(),
886                )
887            });
888        }
889        _ => return Err(FormatError::InvalidFormatSpecifier),
890    }
891    Ok(())
892}
893
894/// The digit counts (from the decimal separator leftwards) after which a
895/// group separator is written for `digits` integer digits: the group sizes
896/// in order, the last one repeating; a size of zero ends the grouping.
897fn group_positions(digits: i32, sizes: &[i32]) -> Vec<i32> {
898    let mut positions = Vec::new();
899    let Some(&first) = sizes.first() else { return positions };
900    let mut index = 0;
901    let mut total = first;
902    while digits > total {
903        if sizes[index] == 0 {
904            break;
905        }
906        positions.push(total);
907        if index + 1 < sizes.len() {
908            index += 1;
909        }
910        if sizes[index] == 0 {
911            break;
912        }
913        total += sizes[index];
914    }
915    positions
916}
917
918fn format_fixed(
919    out: &mut String,
920    number: &Number,
921    max_digits: i32,
922    group_sizes: Option<&[i32]>,
923    decimal_separator: &str,
924    group_separator: &str,
925) {
926    let mut cursor = 0usize;
927    let mut dig_pos = number.scale;
928    if dig_pos > 0 {
929        let positions = group_sizes.map(|sizes| group_positions(dig_pos, sizes)).unwrap_or_default();
930        let count = dig_pos as usize;
931        for i in 0..count {
932            out.push(number.digit(cursor));
933            cursor += 1;
934            let remaining = (count - i - 1) as i32;
935            if remaining > 0 && positions.contains(&remaining) {
936                out.push_str(group_separator);
937            }
938        }
939        dig_pos = 0;
940    } else {
941        out.push('0');
942    }
943
944    if max_digits > 0 {
945        let mut remaining = max_digits;
946        out.push_str(decimal_separator);
947        if dig_pos < 0 {
948            let zeros = (-dig_pos).min(remaining);
949            push_zeros(out, zeros as usize);
950            remaining -= zeros;
951        }
952        while remaining > 0 {
953            out.push(number.digit(cursor));
954            cursor += 1;
955            remaining -= 1;
956        }
957    }
958}
959
960fn format_scientific(out: &mut String, number: &Number, max_digits: i32, exponent_char: char, info: &NumberFormatInfo) {
961    out.push(number.digit(0));
962    if max_digits != 1 {
963        // E0 has no decimal point.
964        out.push_str(info.number_decimal_separator());
965    }
966    for i in 1..max_digits.max(1) as usize {
967        out.push(number.digit(i));
968    }
969    let exponent = if number.is_zero() { 0 } else { number.scale - 1 };
970    format_exponent(out, exponent, exponent_char, 3, true, info);
971}
972
973fn format_exponent(
974    out: &mut String,
975    value: i32,
976    exponent_char: char,
977    min_digits: usize,
978    positive_sign: bool,
979    info: &NumberFormatInfo,
980) {
981    out.push(exponent_char);
982    if value < 0 {
983        out.push_str(info.negative_sign());
984    } else if positive_sign {
985        out.push_str(info.positive_sign());
986    }
987    let digits = value.unsigned_abs().to_string();
988    push_zeros(out, min_digits.saturating_sub(digits.len()));
989    out.push_str(&digits);
990}
991
992fn format_general(out: &mut String, number: &Number, max_digits: i32, exponent_char: char, info: &NumberFormatInfo) {
993    let mut dig_pos = number.scale;
994    let mut scientific = false;
995    if dig_pos > max_digits || dig_pos < -3 {
996        dig_pos = 1;
997        scientific = true;
998    }
999
1000    let mut cursor = 0usize;
1001    if dig_pos > 0 {
1002        while dig_pos > 0 {
1003            out.push(number.digit(cursor));
1004            cursor += 1;
1005            dig_pos -= 1;
1006        }
1007    } else {
1008        out.push('0');
1009    }
1010
1011    if cursor < number.digits.len() || dig_pos < 0 {
1012        out.push_str(info.number_decimal_separator());
1013        while dig_pos < 0 {
1014            out.push('0');
1015            dig_pos += 1;
1016        }
1017        while cursor < number.digits.len() {
1018            out.push(number.digit(cursor));
1019            cursor += 1;
1020        }
1021    }
1022
1023    if scientific {
1024        format_exponent(out, number.scale - 1, exponent_char, 2, true, info);
1025    }
1026}
1027
1028// ---------------------------------------------------------------------------
1029// Custom numeric format strings
1030// ---------------------------------------------------------------------------
1031
1032/// Finds the start of a `;` separated section of a custom format string, or
1033/// 0 if the section does not exist or is empty.
1034fn find_section(format: &[char], section: i32) -> usize {
1035    if section == 0 {
1036        return 0;
1037    }
1038    let mut section = section;
1039    let mut src = 0;
1040    loop {
1041        if src >= format.len() {
1042            return 0;
1043        }
1044        let ch = format[src];
1045        src += 1;
1046        match ch {
1047            '\'' | '"' => {
1048                while src < format.len() && format[src] != '\0' {
1049                    let c = format[src];
1050                    src += 1;
1051                    if c == ch {
1052                        break;
1053                    }
1054                }
1055            }
1056            '\\' => {
1057                if src < format.len() && format[src] != '\0' {
1058                    src += 1;
1059                }
1060            }
1061            ';' => {
1062                section -= 1;
1063                if section != 0 {
1064                    continue;
1065                }
1066                if src < format.len() && format[src] != '\0' && format[src] != ';' {
1067                    return src;
1068                }
1069                return 0;
1070            }
1071            '\0' => return 0,
1072            _ => {}
1073        }
1074    }
1075}
1076
1077fn number_to_string_format(out: &mut String, number: &mut Number, format: &[char], info: &NumberFormatInfo) {
1078    let len = format.len();
1079    let at = |index: usize| format.get(index).copied().unwrap_or('\0');
1080
1081    let mut section = find_section(format, if number.is_zero() { 2 } else if number.negative { 1 } else { 0 });
1082    let mut digit_count: i32;
1083    let mut decimal_pos: i32;
1084    let mut first_digit: i32;
1085    let mut last_digit: i32;
1086    let mut scientific: bool;
1087    let mut thousand_pos: i32;
1088    let mut thousand_count: i32 = 0;
1089    let mut thousand_seps: bool;
1090    let mut scale_adjust: i32;
1091
1092    loop {
1093        digit_count = 0;
1094        decimal_pos = -1;
1095        first_digit = i32::MAX;
1096        last_digit = 0;
1097        scientific = false;
1098        thousand_pos = -1;
1099        thousand_seps = false;
1100        scale_adjust = 0;
1101        let mut src = section;
1102
1103        while src < len {
1104            let ch = format[src];
1105            src += 1;
1106            if ch == '\0' || ch == ';' {
1107                break;
1108            }
1109            match ch {
1110                '#' => digit_count += 1,
1111                '0' => {
1112                    if first_digit == i32::MAX {
1113                        first_digit = digit_count;
1114                    }
1115                    digit_count += 1;
1116                    last_digit = digit_count;
1117                }
1118                '.' => {
1119                    if decimal_pos < 0 {
1120                        decimal_pos = digit_count;
1121                    }
1122                }
1123                ',' => {
1124                    if digit_count > 0 && decimal_pos < 0 {
1125                        if thousand_pos >= 0 {
1126                            if thousand_pos == digit_count {
1127                                thousand_count += 1;
1128                                continue;
1129                            }
1130                            thousand_seps = true;
1131                        }
1132                        thousand_pos = digit_count;
1133                        thousand_count = 1;
1134                    }
1135                }
1136                '%' => scale_adjust += 2,
1137                PER_MILLE_SYMBOL => scale_adjust += 3,
1138                '\'' | '"' => {
1139                    while src < len && format[src] != '\0' {
1140                        let c = format[src];
1141                        src += 1;
1142                        if c == ch {
1143                            break;
1144                        }
1145                    }
1146                }
1147                '\\' => {
1148                    if src < len && format[src] != '\0' {
1149                        src += 1;
1150                    }
1151                }
1152                'E' | 'e' => {
1153                    if at(src) == '0' || ((at(src) == '+' || at(src) == '-') && at(src + 1) == '0') {
1154                        src += 1;
1155                        while src < len && format[src] == '0' {
1156                            src += 1;
1157                        }
1158                        scientific = true;
1159                    }
1160                }
1161                _ => {}
1162            }
1163        }
1164
1165        if decimal_pos < 0 {
1166            decimal_pos = digit_count;
1167        }
1168
1169        if thousand_pos >= 0 {
1170            if thousand_pos == decimal_pos {
1171                scale_adjust -= thousand_count * 3;
1172            } else {
1173                thousand_seps = true;
1174            }
1175        }
1176
1177        if !number.is_zero() {
1178            number.scale += scale_adjust;
1179            let pos = if scientific {
1180                i64::from(digit_count)
1181            } else {
1182                i64::from(number.scale) + i64::from(digit_count) - i64::from(decimal_pos)
1183            };
1184            round_number(number, pos, false);
1185            if number.is_zero() {
1186                let zero_section = find_section(format, 2);
1187                if zero_section != section {
1188                    section = zero_section;
1189                    continue;
1190                }
1191            }
1192        } else {
1193            if !number.floating {
1194                // Integers have no negative zero.
1195                number.negative = false;
1196            }
1197            number.scale = 0;
1198        }
1199        break;
1200    }
1201
1202    let first_digit = if first_digit < decimal_pos { decimal_pos - first_digit } else { 0 };
1203    let last_digit = if last_digit > decimal_pos { decimal_pos - last_digit } else { 0 };
1204    let mut dig_pos: i32;
1205    let mut adjust: i32;
1206    if scientific {
1207        dig_pos = decimal_pos;
1208        adjust = 0;
1209    } else {
1210        dig_pos = number.scale.max(decimal_pos);
1211        adjust = number.scale - decimal_pos;
1212    }
1213
1214    // The digit positions after which a group separator is written.
1215    let mut separator_positions: Vec<i32> = Vec::new();
1216    if thousand_seps {
1217        let total_digits = dig_pos + adjust.min(0);
1218        let num_digits = first_digit.max(total_digits);
1219        separator_positions = group_positions(num_digits, info.number_group_sizes());
1220    }
1221
1222    // A negative number that is (or rounded to) zero only gets its sign if
1223    // the format produces any text: see the end.
1224    let start = out.len();
1225    if number.negative && section == 0 && number.scale != 0 {
1226        out.push_str(info.negative_sign());
1227    }
1228
1229    let mut decimal_written = false;
1230    let mut cursor = 0usize;
1231    let mut src = section;
1232    let digits_len = number.digits.len();
1233
1234    let mut write_separator = |out: &mut String, dig_pos: i32| {
1235        if thousand_seps && dig_pos > 1 {
1236            if let Some(&last) = separator_positions.last() {
1237                if dig_pos == last + 1 {
1238                    out.push_str(info.number_group_separator());
1239                    separator_positions.pop();
1240                }
1241            }
1242        }
1243    };
1244
1245    while src < len {
1246        let ch = format[src];
1247        src += 1;
1248        if ch == '\0' || ch == ';' {
1249            break;
1250        }
1251
1252        if adjust > 0 && matches!(ch, '#' | '0' | '.') {
1253            // The number has more integer digits than the format.
1254            while adjust > 0 {
1255                out.push(number.digit(cursor));
1256                cursor += 1;
1257                write_separator(out, dig_pos);
1258                dig_pos -= 1;
1259                adjust -= 1;
1260            }
1261        }
1262
1263        match ch {
1264            '#' | '0' => {
1265                let digit = if adjust < 0 {
1266                    adjust += 1;
1267                    if dig_pos <= first_digit { Some('0') } else { None }
1268                } else if cursor < digits_len {
1269                    cursor += 1;
1270                    Some(number.digit(cursor - 1))
1271                } else if dig_pos > last_digit {
1272                    Some('0')
1273                } else {
1274                    None
1275                };
1276                if let Some(digit) = digit {
1277                    out.push(digit);
1278                    write_separator(out, dig_pos);
1279                }
1280                dig_pos -= 1;
1281            }
1282            '.' => {
1283                // Repeated decimal points are not echoed.
1284                if dig_pos == 0
1285                    && !decimal_written
1286                    && (last_digit < 0 || (decimal_pos < digit_count && cursor < digits_len))
1287                {
1288                    out.push_str(info.number_decimal_separator());
1289                    decimal_written = true;
1290                }
1291            }
1292            PER_MILLE_SYMBOL => out.push_str(info.per_mille_symbol()),
1293            '%' => out.push_str(info.percent_symbol()),
1294            ',' => {}
1295            '\'' | '"' => {
1296                while src < len && format[src] != '\0' && format[src] != ch {
1297                    out.push(format[src]);
1298                    src += 1;
1299                }
1300                if src < len && format[src] != '\0' {
1301                    src += 1;
1302                }
1303            }
1304            '\\' => {
1305                if src < len && format[src] != '\0' {
1306                    out.push(format[src]);
1307                    src += 1;
1308                }
1309            }
1310            'E' | 'e' => {
1311                if scientific {
1312                    let mut positive_sign = false;
1313                    let mut min_digits = 0usize;
1314                    if at(src) == '0' {
1315                        // E0 formats like E-0.
1316                        min_digits += 1;
1317                    } else if at(src) == '+' && at(src + 1) == '0' {
1318                        positive_sign = true;
1319                    } else if at(src) == '-' && at(src + 1) == '0' {
1320                        // E-0: nothing to do.
1321                    } else {
1322                        out.push(ch);
1323                        continue;
1324                    }
1325                    src += 1;
1326                    while src < len && format[src] == '0' {
1327                        min_digits += 1;
1328                        src += 1;
1329                    }
1330                    let exponent = if number.is_zero() { 0 } else { number.scale - decimal_pos };
1331                    format_exponent(out, exponent, ch, min_digits.min(10), positive_sign, info);
1332                    scientific = false;
1333                } else {
1334                    out.push(ch);
1335                    if src < len {
1336                        if format[src] == '+' || format[src] == '-' {
1337                            out.push(format[src]);
1338                            src += 1;
1339                        }
1340                        while src < len && format[src] == '0' {
1341                            out.push(format[src]);
1342                            src += 1;
1343                        }
1344                    }
1345                }
1346            }
1347            _ => out.push(ch),
1348        }
1349    }
1350
1351    if number.negative && section == 0 && number.scale == 0 && out.len() > start {
1352        out.insert_str(start, info.negative_sign());
1353    }
1354}