Skip to main content

vla_lang/
numfmt.rs

1//! A number format's code (KERNEL.47, 2026-10-17): Excel's own spelling of
2//! how a number is drawn, beside the two formats the grid had, `general`
3//! and `text`, so that a cell holding 1 under `0.00` draws `1.00`. The view
4//! record prints a code in its `format` row and a number's text under it in
5//! a `text` row (`crate::view`), so that a viewport draws the text and
6//! computes nothing, and the writer gives Excel the code (`frazaro-core`).
7//!
8//! What this version takes, in Excel's syntax as `Range.NumberFormat` reads
9//! it, spelled the same on every machine (the point `.`, the thousands
10//! `,`): one to four sections by `;`, a positive number's, a negative's,
11//! zero's and a text's. In a number's section, the digit placeholders `0`,
12//! a digit or 0, and `#`, a digit or nothing; one point; `,` between
13//! integer placeholders for the thousands, and after the last placeholder
14//! for each thousandth; `%` for each hundredfold; and literals: a quoted
15//! text, `\` and the character after it, the characters Excel draws
16//! unquoted (`$ - + ( ) : ! ^ & ' ~ { } < > =` and the space), and any
17//! character outside ASCII, none of which means anything in a code, so that
18//! `▲0.00;▼0.00;=0.00` draws a glyph beside its number. In a text's section,
19//! literals and `@`, the text itself. A bare `$` is a dollar here, the
20//! record having no reading machine whose currency it could be. Anything
21//! else is refused by name ([`Unsupported`]): a bracket (a color, a
22//! condition, a locale), a date's or a time's letters, an exponent, a
23//! fraction, a width or a fill, `General` inside a code.
24//!
25//! How a number becomes its text, as Excel draws it: the section is chosen
26//! by the number's sign before it is rounded (one section for every number,
27//! a negative drawn with its `-`, but for one that draws as zero; two, the
28//! second for below zero, drawn without its `-`; three, the third for
29//! zero); the number's fifteen significant digits, the precision Excel
30//! documents and `round15`, `ROUND` and `&` already take, the nearest and a
31//! tie toward zero, are moved in decimal for each `%` and each thousandth,
32//! never by a multiply in binary; then rounded half away from zero to the
33//! section's decimals, the digits past the fifteenth drawn as zeros. So
34//! 2.675 and 1.005 under `0.00` are `2.68` and `1.01`, where Rust's own
35//! `{:.2}` rounds the binary value with ties to even and spells them `2.67`
36//! and `1.00`; -0.4 under `0` is `0`; and 1234567890123455 under `0` is
37//! `1234567890123450`. The digits come from the shortest spelling when it
38//! has fifteen significant digits or fewer, which is then the fifteen-digit
39//! rounding exactly (a double's half step is at most a fifth of half the
40//! fifteenth digit's unit), and from `{:.14e}` when it has more. The two
41//! rules after "but" and "toward zero" are Excel's word against the draft's
42//! prediction, from the reference's first run of the golden (2026-10-17).
43//! The reference is Excel itself: `scripts/formats.txt` and the text Excel
44//! draws for each of its cases, `scripts/formats_golden.txt`, written by
45//! `VlaWriteFormatsGolden` (VLA_Tests.bas), which this module's test reads.
46
47use std::fmt;
48use std::sync::Arc;
49
50use crate::calc::Value;
51
52/// The longest code Excel takes, in characters.
53pub const MOST_CHARS: usize = 255;
54
55/// The most decimals a section draws, Excel's ceiling.
56pub const MOST_DECIMALS: usize = 30;
57
58/// The codes of Excel's built-in formats that Excel draws alike in every
59/// locale, by their ids in a styles part. 0 is General and 49 is Text, the
60/// grid's two symbols; 5 to 8, 14 and the rest draw by the reading
61/// machine's locale and are never written for a code.
62const BUILTINS: [(&str, u32); 8] = [
63    ("0", 1),
64    ("0.00", 2),
65    ("#,##0", 3),
66    ("#,##0.00", 4),
67    ("0%", 9),
68    ("0.00%", 10),
69    ("#,##0 ;(#,##0)", 37),
70    ("#,##0.00;(#,##0.00)", 39),
71];
72
73/// A code this version draws, parsed once and kept with its text as
74/// written. Two codes are the same exactly when their texts are.
75#[derive(Clone)]
76pub struct Code(Arc<Parsed>);
77
78struct Parsed {
79    text: String,
80    sections: Vec<Section>,
81}
82
83/// What a code holds that this version does not draw: the construct, as
84/// `grid-format-code` names it, and what draws instead.
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct Unsupported {
87    pub what: String,
88    pub fix: String,
89}
90
91impl Unsupported {
92    fn new(what: impl Into<String>, fix: impl Into<String>) -> Unsupported {
93        Unsupported {
94            what: what.into(),
95            fix: fix.into(),
96        }
97    }
98}
99
100/// One piece of a section as it draws.
101#[derive(Clone, Debug, PartialEq)]
102enum Part {
103    /// A digit placeholder: `0` (true) draws a digit or 0, `#` a digit or
104    /// nothing.
105    Digit(bool),
106    Point,
107    Percent,
108    Literal(String),
109    /// The text itself, in a text's section.
110    At,
111}
112
113/// One section, read for drawing.
114#[derive(Clone, Debug, Default)]
115struct Section {
116    parts: Vec<Part>,
117    /// Whether the section holds a digit placeholder at all.
118    placeholders: bool,
119    /// The placeholders before the point.
120    integer: usize,
121    /// The integer digits always drawn: the placeholders from the first
122    /// `0` before the point to the point.
123    min_integer: usize,
124    /// The placeholders after the point, `0` true.
125    decimals: Vec<bool>,
126    /// Whether the integer digits are grouped by thousands.
127    grouping: bool,
128    /// The places the point moves right before rounding: 2 for each `%`,
129    /// -3 for each thousandth.
130    shift: i32,
131}
132
133/// A token of a code, before its section is read.
134#[derive(Clone, Debug, PartialEq)]
135enum Token {
136    Digit(bool),
137    Point,
138    Comma,
139    Percent,
140    Literal(String),
141    At,
142}
143
144impl Code {
145    /// A code as written, or what it holds that this version does not draw.
146    pub fn parse(text: &str) -> Result<Code, Unsupported> {
147        if text.is_empty() {
148            return Err(Unsupported::new(
149                "no code at all",
150                "write general for Excel's General format",
151            ));
152        }
153        if text.eq_ignore_ascii_case("general") {
154            return Err(Unsupported::new(
155                "Excel's General format as a code",
156                "write it general, a symbol, unquoted",
157            ));
158        }
159        if text == "@" {
160            return Err(Unsupported::new(
161                "Excel's Text format as a code",
162                "write it text, a symbol, unquoted",
163            ));
164        }
165        if text.chars().count() > MOST_CHARS {
166            return Err(Unsupported::new(
167                format!("more than {MOST_CHARS} characters"),
168                format!("shorten it to Excel's {MOST_CHARS}"),
169            ));
170        }
171        let raw = tokens(text)?;
172        if raw.len() > 4 {
173            return Err(Unsupported::new(
174                "a fifth section",
175                "keep four, a positive number's, a negative's, zero's and a text's",
176            ));
177        }
178        let mut sections = Vec::with_capacity(raw.len());
179        for (i, toks) in raw.into_iter().enumerate() {
180            sections.push(if i == 3 {
181                text_section(toks)?
182            } else {
183                number_section(toks)?
184            });
185        }
186        Ok(Code(Arc::new(Parsed {
187            text: text.to_string(),
188            sections,
189        })))
190    }
191
192    /// The code as written.
193    pub fn as_str(&self) -> &str {
194        &self.0.text
195    }
196
197    /// The id of the built-in format the code is, where Excel has one that
198    /// it draws alike in every locale.
199    pub fn builtin(&self) -> Option<u32> {
200        BUILTINS
201            .iter()
202            .find(|(code, _)| *code == self.0.text)
203            .map(|&(_, id)| id)
204    }
205
206    /// The text a value draws as under the code, as Excel's `Range.Text`
207    /// gives it in a column wide enough: a number's, and a text's where the
208    /// code has a text's section; `None` for a text under a code without
209    /// one, a truth value, an error and an empty value, each drawn as the
210    /// record spells it, and for a number no cell can hold.
211    pub fn text(&self, value: &Value) -> Option<String> {
212        match value {
213            Value::Number(n) => self.number(*n),
214            Value::Text(t) => self.0.sections.get(3).map(|s| s.text_of(t)),
215            _ => None,
216        }
217    }
218
219    fn number(&self, x: f64) -> Option<String> {
220        if !x.is_finite() {
221            return None;
222        }
223        let sections = &self.0.sections;
224        let numeric = sections.len().min(3);
225        let (section, minus) = if x == 0.0 {
226            // 0 and -0 alike: zero's section where there is one.
227            (&sections[if numeric == 3 { 2 } else { 0 }], false)
228        } else if x < 0.0 {
229            if numeric >= 2 {
230                (&sections[1], false)
231            } else {
232                (&sections[0], true)
233            }
234        } else {
235            (&sections[0], false)
236        };
237        let mut out = String::new();
238        let zero = section.draw(x.abs(), &mut out);
239        // One section's minus, before every literal; none where the number
240        // draws as zero, as Excel draws -0.4 under 0 as 0 (the reference's
241        // golden, 2026-10-17).
242        if minus && !zero {
243            out.insert(0, '-');
244        }
245        Some(out)
246    }
247}
248
249impl PartialEq for Code {
250    fn eq(&self, other: &Code) -> bool {
251        self.0.text == other.0.text
252    }
253}
254
255impl Eq for Code {}
256
257impl fmt::Debug for Code {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        f.debug_tuple("Code").field(&self.0.text).finish()
260    }
261}
262
263/// A code's tokens, a list a section, split at each `;` outside quotes; or
264/// the first thing it holds that this version does not draw.
265fn tokens(text: &str) -> Result<Vec<Vec<Token>>, Unsupported> {
266    let chars: Vec<char> = text.chars().collect();
267    let mut sections: Vec<Vec<Token>> = vec![Vec::new()];
268    let mut i = 0;
269    while i < chars.len() {
270        let c = chars[i];
271        let toks = sections.last_mut().expect("one section at least");
272        match c {
273            ';' => sections.push(Vec::new()),
274            '"' => {
275                let start = i + 1;
276                let Some(len) = chars[start..].iter().position(|&x| x == '"') else {
277                    return Err(Unsupported::new("a quote left open", "close it"));
278                };
279                let quoted: String = chars[start..start + len].iter().collect();
280                if quoted.chars().any(is_control) {
281                    return Err(control());
282                }
283                toks.push(Token::Literal(quoted));
284                i = start + len;
285            }
286            '\\' => {
287                let Some(&next) = chars.get(i + 1) else {
288                    return Err(Unsupported::new(
289                        "a backslash with nothing after it",
290                        "drop it, or write after it the character it draws",
291                    ));
292                };
293                if is_control(next) {
294                    return Err(control());
295                }
296                toks.push(Token::Literal(next.to_string()));
297                i += 1;
298            }
299            '0' => toks.push(Token::Digit(true)),
300            '#' => toks.push(Token::Digit(false)),
301            '.' => toks.push(Token::Point),
302            ',' => toks.push(Token::Comma),
303            '%' => toks.push(Token::Percent),
304            '@' => toks.push(Token::At),
305            '$' | '-' | '+' | '(' | ')' | ':' | '!' | '^' | '&' | '\'' | '~' | '{' | '}' | '<'
306            | '>' | '=' | ' ' => toks.push(Token::Literal(c.to_string())),
307            '[' => {
308                let bracket: String = match chars[i..].iter().position(|&x| x == ']') {
309                    Some(len) => chars[i..=i + len].iter().collect(),
310                    None => chars[i..].iter().collect(),
311                };
312                return Err(Unsupported::new(
313                    format!("{bracket}, a color, a condition, a locale or an elapsed time"),
314                    "drop it",
315                ));
316            }
317            '?' => {
318                return Err(Unsupported::new(
319                    "?, a digit's width",
320                    "write 0 or # in its place",
321                ))
322            }
323            '/' => {
324                return Err(Unsupported::new(
325                    "/, a fraction's bar",
326                    "quote it, \"/\", to draw it as written",
327                ))
328            }
329            '_' => {
330                return Err(Unsupported::new(
331                    "_, a character's width",
332                    "drop it, or quote a space in its place",
333                ))
334            }
335            '*' => return Err(Unsupported::new("*, a fill", "drop it")),
336            c if c.is_ascii_alphabetic() => return Err(letter(&chars, i)),
337            c if c.is_ascii_digit() => {
338                return Err(Unsupported::new(
339                    format!("{c}, a digit outside quotes"),
340                    format!("quote it, \"{c}\", to draw it as written"),
341                ))
342            }
343            c if is_control(c) => return Err(control()),
344            c if c.is_ascii() => {
345                return Err(Unsupported::new(
346                    format!("{c}, a character Excel draws only quoted"),
347                    format!("quote it, \"{c}\", to draw it as written"),
348                ))
349            }
350            c => toks.push(Token::Literal(c.to_string())),
351        }
352        i += 1;
353    }
354    Ok(sections)
355}
356
357fn is_control(c: char) -> bool {
358    (c as u32) < 0x20 || c == '\u{7F}'
359}
360
361fn control() -> Unsupported {
362    Unsupported::new("a control character", "drop it")
363}
364
365/// A letter outside quotes: `General` inside a code, an exponent, a date's
366/// or a time's letter, or a letter Excel draws only quoted.
367fn letter(chars: &[char], i: usize) -> Unsupported {
368    let word: String = chars[i..].iter().take(7).collect();
369    if word.eq_ignore_ascii_case("general") {
370        return Unsupported::new(
371            "General inside a code",
372            "write the digits with 0 and # in its place",
373        );
374    }
375    let c = chars[i];
376    let sign = chars.get(i + 1).copied();
377    if (c == 'E' || c == 'e') && (sign == Some('+') || sign == Some('-')) {
378        let sign = sign.unwrap_or('+');
379        return Unsupported::new(
380            format!("{c}{sign}, a scientific exponent"),
381            "write the digits with 0 and # alone",
382        );
383    }
384    match c.to_ascii_lowercase() {
385        'y' | 'm' | 'd' | 'h' | 's' | 'a' | 'p' | 'b' | 'e' | 'g' => Unsupported::new(
386            format!("{c}, a date's or a time's letter"),
387            format!("quote it, \"{c}\", to draw it as written; this version draws no date or time"),
388        ),
389        _ => Unsupported::new(
390            format!("{c}, a letter outside quotes"),
391            format!("quote it, \"{c}\", to draw it as written"),
392        ),
393    }
394}
395
396/// A number's section from its tokens.
397fn number_section(toks: Vec<Token>) -> Result<Section, Unsupported> {
398    if toks.contains(&Token::At) {
399        return Err(Unsupported::new(
400            "@ before the fourth section",
401            "put a text's section fourth, after zero's",
402        ));
403    }
404    if toks.iter().filter(|t| **t == Token::Point).count() > 1 {
405        return Err(Unsupported::new(
406            "a second point",
407            "keep one, or quote the other",
408        ));
409    }
410    let point = toks.iter().position(|t| *t == Token::Point);
411    let is_digit = |t: &Token| matches!(t, Token::Digit(_));
412    let before_point = |k: usize| point.map_or(true, |p| k < p);
413    let mut s = Section {
414        placeholders: toks.iter().any(is_digit),
415        ..Section::default()
416    };
417    let mut zeros: Vec<bool> = Vec::new();
418    for (k, t) in toks.iter().enumerate() {
419        match t {
420            Token::Digit(zero) => {
421                if before_point(k) {
422                    s.integer += 1;
423                    zeros.push(*zero);
424                } else {
425                    s.decimals.push(*zero);
426                }
427                s.parts.push(Part::Digit(*zero));
428            }
429            Token::Point | Token::Percent if !s.placeholders => {
430                let what = if *t == Token::Point { "." } else { "%" };
431                return Err(Unsupported::new(
432                    format!("{what} beside no digit placeholder"),
433                    format!("write a digit with 0 or #, or quote it, \"{what}\""),
434                ));
435            }
436            Token::Point => s.parts.push(Part::Point),
437            Token::Percent => {
438                s.shift += 2;
439                s.parts.push(Part::Percent);
440            }
441            Token::Literal(text) => match s.parts.last_mut() {
442                Some(Part::Literal(last)) => last.push_str(text),
443                _ => s.parts.push(Part::Literal(text.clone())),
444            },
445            Token::Comma => {
446                if !toks[..k].iter().any(is_digit) {
447                    return Err(Unsupported::new(
448                        "a comma before any digit placeholder",
449                        "drop it, or quote it, \",\"",
450                    ));
451                }
452                match toks[k + 1..].iter().position(is_digit).map(|j| k + 1 + j) {
453                    // After the last placeholder: a thousandth.
454                    None => s.shift -= 3,
455                    // Between integer placeholders: the thousands.
456                    Some(j) if before_point(j) => s.grouping = true,
457                    Some(_) if !before_point(k) => {
458                        return Err(Unsupported::new(
459                            "a comma among the decimals",
460                            "drop it, or quote it, \",\"",
461                        ))
462                    }
463                    Some(_) => {
464                        return Err(Unsupported::new(
465                            "a comma just before the point",
466                            "move it after the last digit placeholder for thousandths, or drop it",
467                        ))
468                    }
469                }
470            }
471            Token::At => unreachable!("refused above"),
472        }
473    }
474    if s.decimals.len() > MOST_DECIMALS {
475        return Err(Unsupported::new(
476            format!("more than {MOST_DECIMALS} decimals"),
477            format!("keep {MOST_DECIMALS}, Excel's most"),
478        ));
479    }
480    s.min_integer = zeros
481        .iter()
482        .position(|zero| *zero)
483        .map_or(0, |first| zeros.len() - first);
484    Ok(s)
485}
486
487/// A text's section from its tokens: literals and `@` alone.
488fn text_section(toks: Vec<Token>) -> Result<Section, Unsupported> {
489    let mut s = Section::default();
490    for t in toks {
491        let what = match t {
492            Token::Literal(text) => {
493                match s.parts.last_mut() {
494                    Some(Part::Literal(last)) => last.push_str(&text),
495                    _ => s.parts.push(Part::Literal(text)),
496                }
497                continue;
498            }
499            Token::At => {
500                s.parts.push(Part::At);
501                continue;
502            }
503            Token::Digit(true) => "0",
504            Token::Digit(false) => "#",
505            Token::Point => ".",
506            Token::Comma => ",",
507            Token::Percent => "%",
508        };
509        return Err(Unsupported::new(
510            format!("{what} in a text's section"),
511            format!("quote it, \"{what}\", to draw it as written"),
512        ));
513    }
514    Ok(s)
515}
516
517impl Section {
518    /// A text drawn by a text's section.
519    fn text_of(&self, text: &str) -> String {
520        let mut out = String::new();
521        for p in &self.parts {
522            match p {
523                Part::Literal(l) => out.push_str(l),
524                Part::At => out.push_str(text),
525                _ => {}
526            }
527        }
528        out
529    }
530
531    /// A number's magnitude drawn by a number's section onto `out`; whether
532    /// it drew as zero, its digits all 0 once rounded. A section with no
533    /// placeholder draws no number, and so never a zero.
534    fn draw(&self, magnitude: f64, out: &mut String) -> bool {
535        if !self.placeholders {
536            for p in &self.parts {
537                if let Part::Literal(l) = p {
538                    out.push_str(l);
539                }
540            }
541            return false;
542        }
543        let (digits, point) = decimal(magnitude);
544        let (int, frac) = rounded(&digits, point + self.shift, self.decimals.len());
545        let zero = int.is_empty() && frac.iter().all(|&d| d == 0);
546        // A `#` drops a trailing 0; a `0` keeps its digit and every one
547        // before it.
548        let mut kept = frac.len();
549        while kept > 0 && !self.decimals[kept - 1] && frac[kept - 1] == 0 {
550            kept -= 1;
551        }
552        let mut int = int;
553        if int.len() < self.min_integer {
554            let mut padded = vec![0; self.min_integer - int.len()];
555            padded.extend_from_slice(&int);
556            int = padded;
557        }
558        let len = int.len();
559        let push_digit = |j: usize, out: &mut String| {
560            out.push(char::from(b'0' + int[j]));
561            let from_right = len - 1 - j;
562            if self.grouping && from_right > 0 && from_right % 3 == 0 {
563                out.push(',');
564            }
565        };
566        let mut after_point = false;
567        let (mut k, mut d) = (0, 0);
568        for p in &self.parts {
569            match p {
570                Part::Digit(_) if !after_point => {
571                    // The integer digits stand right to left across the
572                    // placeholders, the leftmost taking every digit past
573                    // the others.
574                    let r = self.integer - 1 - k;
575                    if len > r {
576                        let first = if k == 0 { 0 } else { len - 1 - r };
577                        for j in first..=len - 1 - r {
578                            push_digit(j, out);
579                        }
580                    }
581                    k += 1;
582                }
583                Part::Digit(_) => {
584                    if d < kept {
585                        out.push(char::from(b'0' + frac[d]));
586                    }
587                    d += 1;
588                }
589                Part::Point => {
590                    if self.integer == 0 {
591                        // No placeholder before the point: its digits, if
592                        // any, stand before it.
593                        for j in 0..len {
594                            push_digit(j, out);
595                        }
596                    }
597                    out.push('.');
598                    after_point = true;
599                }
600                Part::Percent => out.push('%'),
601                Part::Literal(l) => out.push_str(l),
602                Part::At => {}
603            }
604        }
605        zero
606    }
607}
608
609/// A positive finite number's significant digits, no 0 at either end, and
610/// the count of its integer digits: its shortest spelling's where that has
611/// fifteen significant digits or fewer, which is then its fifteen-digit
612/// rounding exactly, and the fifteen of `{:.14e}` where it has more, the
613/// nearest. A number exactly halfway between two of fifteen digits rounds
614/// toward zero, as Excel draws 1234567890123455 as 1234567890123450 (the
615/// reference's golden, 2026-10-17), where `{:.14e}` rounds a tie to even;
616/// such a number's shortest spelling is the tie itself, sixteen digits
617/// ending in 5. Zero has none.
618fn decimal(magnitude: f64) -> (Vec<u8>, i32) {
619    if magnitude == 0.0 {
620        return (Vec::new(), 0);
621    }
622    let spelled = |text: String| -> (Vec<u8>, i32) {
623        let (mantissa, exponent) = text.split_once('e').expect("an exponent");
624        let digits: Vec<u8> = mantissa
625            .bytes()
626            .filter(u8::is_ascii_digit)
627            .map(|b| b - b'0')
628            .collect();
629        (
630            digits,
631            exponent.parse::<i32>().expect("a whole exponent") + 1,
632        )
633    };
634    let (mut digits, mut point) = spelled(format!("{magnitude:e}"));
635    if digits.len() == 16 && digits[15] == 5 && exactly(magnitude, &digits, point) {
636        digits.pop();
637    } else if digits.len() > 15 {
638        (digits, point) = spelled(format!("{magnitude:.14e}"));
639    }
640    while digits.last() == Some(&0) {
641        digits.pop();
642    }
643    (digits, point)
644}
645
646/// Whether a positive number is exactly the decimal its digits and point
647/// spell, read in whole numbers: the decimal `d` times 10^p is a double only
648/// as a whole number, or, for p below 0, where 5^-p divides `d`, the whole
649/// number d / 5^-p over 2^-p, a power of two scaling the double exactly.
650fn exactly(magnitude: f64, digits: &[u8], point: i32) -> bool {
651    let d = digits
652        .iter()
653        .fold(0u128, |acc, &x| acc * 10 + u128::from(x));
654    let p = point - digits.len() as i32;
655    if p >= 0 {
656        if p > 20 || magnitude.fract() != 0.0 || magnitude >= 1e38 {
657            return false;
658        }
659        magnitude as u128 == d * 10u128.pow(p as u32)
660    } else {
661        let q = (-p) as u32;
662        if q > 40 {
663            return false;
664        }
665        let five = 5u128.pow(q);
666        if d % five != 0 {
667            return false;
668        }
669        let scaled = magnitude * 2f64.powi(q as i32);
670        scaled.fract() == 0.0 && scaled < 1e38 && scaled as u128 == d / five
671    }
672}
673
674/// Digits standing for a number with `point` integer digits (the digit at
675/// index j is worth 10^(point-1-j)), rounded half away from zero to
676/// `decimals` places: the integer digits, none for a number under 1, and
677/// the decimals, exactly `decimals` of them.
678fn rounded(digits: &[u8], point: i32, decimals: usize) -> (Vec<u8>, Vec<u8>) {
679    let at = |j: i32| -> u8 {
680        if j < 0 {
681            0
682        } else {
683            digits.get(j as usize).copied().unwrap_or(0)
684        }
685    };
686    let keep = point + decimals as i32;
687    let mut kept: Vec<u8> = (0..keep.max(0)).map(at).collect();
688    if keep >= 0 && at(keep) >= 5 {
689        let mut i = kept.len();
690        loop {
691            if i == 0 {
692                kept.insert(0, 1);
693                break;
694            }
695            i -= 1;
696            if kept[i] == 9 {
697                kept[i] = 0;
698            } else {
699                kept[i] += 1;
700                break;
701            }
702        }
703    }
704    // The last digit kept stands at the last decimal's place, a carry past
705    // the first or not; a number under the first decimal's place has its
706    // zeros put before it.
707    if kept.len() < decimals {
708        let mut padded = vec![0; decimals - kept.len()];
709        padded.extend_from_slice(&kept);
710        kept = padded;
711    }
712    let split = kept.len() - decimals;
713    let frac = kept[split..].to_vec();
714    let int = match kept[..split].iter().position(|&x| x != 0) {
715        Some(lead) => kept[lead..split].to_vec(),
716        None => Vec::new(),
717    };
718    (int, frac)
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    fn code(text: &str) -> Code {
726        Code::parse(text).unwrap_or_else(|u| panic!("{text}: {u:?}"))
727    }
728
729    fn drawn(text: &str, x: f64) -> String {
730        code(text).text(&Value::Number(x)).expect("a number draws")
731    }
732
733    fn refused(text: &str) -> Unsupported {
734        match Code::parse(text) {
735            Ok(c) => panic!("{text} was taken: {c:?}"),
736            Err(u) => u,
737        }
738    }
739
740    #[test]
741    fn a_number_draws_with_its_decimals() {
742        assert_eq!(drawn("0.00", 1.0), "1.00");
743        assert_eq!(drawn("0.00", 12.3), "12.30");
744        assert_eq!(drawn("0.00", 0.0), "0.00");
745        assert_eq!(drawn("0.00", -0.0), "0.00");
746        assert_eq!(drawn("0", 0.4), "0");
747        assert_eq!(drawn("#,##0.00", 1234.5), "1,234.50");
748        assert_eq!(drawn("#,##0", 1234567.0), "1,234,567");
749        assert_eq!(drawn("#,##0", 999.0), "999");
750        assert_eq!(drawn("0.00", 1e16), "10000000000000000.00");
751    }
752
753    #[test]
754    fn rounding_is_excel_s_half_away_from_zero_over_fifteen_digits() {
755        // Rust's own {:.2} spells these 2.67, 1.00 and 0.12, and {:.0}
756        // spells 2.5 as 2: ties to even on the binary value.
757        assert_eq!(drawn("0.00", 2.675), "2.68");
758        assert_eq!(drawn("0.00", 1.005), "1.01");
759        assert_eq!(drawn("0.00", 0.125), "0.13");
760        assert_eq!(drawn("0", 2.5), "3");
761        assert_eq!(drawn("0", 0.5), "1");
762        assert_eq!(drawn("0", -2.5), "-3");
763        assert_eq!(drawn("0.00", 9.995), "10.00");
764        assert_eq!(drawn("0.00", 0.005), "0.01");
765        assert_eq!(drawn("0.00", 0.0049), "0.00");
766        assert_eq!(drawn("0.00", 0.1 + 0.2), "0.30");
767        // The fifteenth digit rounded by the sixteenth, the rest zeros.
768        assert_eq!(drawn("0", 123456789012345678.0), "123456789012346000");
769        // An exact tie at the fifteenth digit toward zero, as Excel draws it
770        // (the reference's golden), where {:.14e} rounds to even: whole, and
771        // with a fraction, the tie read in whole numbers either way.
772        assert_eq!(drawn("0", 1234567890123455.0), "1234567890123450");
773        assert_eq!(drawn("0", 1234567890123465.0), "1234567890123460");
774        assert_eq!(drawn("0", 617283945061727.5), "617283945061727");
775        // Not a tie: the nearest, the sixteenth digit 5 with more after it,
776        // written out, or past the double's shortest spelling, which reads
777        // 0.2234567890123455 for 0.22345678901234550250...
778        assert_eq!(drawn("0", 1234567890123455.5), "1234567890123460");
779        assert_eq!(
780            drawn("0.000000000000000", 0.2234567890123455),
781            "0.223456789012346"
782        );
783        assert_eq!(drawn("0", 1e20), "100000000000000000000");
784        assert_eq!(drawn("0.00", 5e-324), "0.00");
785    }
786
787    #[test]
788    fn sections_choose_by_sign_before_rounding() {
789        assert_eq!(drawn("0.00", -1.5), "-1.50");
790        // One section's minus none where the number draws as zero, as Excel
791        // draws it (the reference's golden); a second section's draws it.
792        assert_eq!(drawn("0.00", -0.001), "0.00");
793        assert_eq!(drawn("0", -0.4), "0");
794        assert_eq!(drawn("#,##0.00", -0.001), "0.00");
795        assert_eq!(drawn("0.00;(0.00)", -1.5), "(1.50)");
796        assert_eq!(drawn("0.00;(0.00)", -0.001), "(0.00)");
797        assert_eq!(drawn("0.00;(0.00)", 0.0), "0.00");
798        assert_eq!(drawn("0.00;(0.00);\"zero\"", 0.0), "zero");
799        assert_eq!(drawn("0.00;(0.00);\"zero\"", -0.0), "zero");
800        assert_eq!(drawn("0.00;(0.00);\"zero\"", 0.001), "0.00");
801        assert_eq!(drawn("▲0.00;▼0.00;=0.00", 1.5), "▲1.50");
802        assert_eq!(drawn("▲0.00;▼0.00;=0.00", -1.5), "▼1.50");
803        assert_eq!(drawn("▲0.00;▼0.00;=0.00", 0.0), "=0.00");
804        assert_eq!(drawn("0;;0", -3.0), "");
805        assert_eq!(drawn("0.00;-0.00", -2.0), "-2.00");
806        assert_eq!(drawn("\"x\"", -5.0), "-x");
807    }
808
809    #[test]
810    fn placeholders_draw_digits_zeros_and_nothing() {
811        assert_eq!(drawn("#", 0.0), "");
812        assert_eq!(drawn("#,###", 0.0), "");
813        assert_eq!(drawn("#.##", 0.5), ".5");
814        assert_eq!(drawn("0.#", 1.0), "1.");
815        assert_eq!(drawn("0.0#", 1.5), "1.5");
816        assert_eq!(drawn("0.0#", 1.25), "1.25");
817        assert_eq!(drawn("0.#0", 1.5), "1.50");
818        assert_eq!(drawn("000", 5.0), "005");
819        assert_eq!(drawn("#0#", 5.0), "05");
820        assert_eq!(drawn("000-00-0000", 123456789.0), "123-45-6789");
821        assert_eq!(drawn("000-00-0000", 5.0), "000-00-0005");
822        assert_eq!(drawn("0-0", 123.0), "12-3");
823        assert_eq!(drawn(".00", 12.5), "12.50");
824        assert_eq!(drawn(".00", 0.5), ".50");
825    }
826
827    #[test]
828    fn percents_and_thousandths_move_the_point_in_decimal() {
829        assert_eq!(drawn("0%", 0.5), "50%");
830        assert_eq!(drawn("0.00%", 0.12345), "12.35%");
831        assert_eq!(drawn("0.0%", 0.145), "14.5%");
832        assert_eq!(drawn("#,##0,", 1234567.891), "1,235");
833        assert_eq!(drawn("0.0,,", 12200000.0), "12.2");
834        assert_eq!(drawn("#,##0,\"K\"", 2500.0), "3K");
835        assert_eq!(drawn("0%%", 0.5), "5000%%");
836    }
837
838    #[test]
839    fn literals_draw_as_written() {
840        assert_eq!(drawn("\\$#,##0.00", 1234.5), "$1,234.50");
841        assert_eq!(drawn("$#,##0.00", -5.0), "-$5.00");
842        assert_eq!(drawn("0.00\" kg\"", 2.0), "2.00 kg");
843        assert_eq!(drawn("(000) 000-0000", 5551234567.0), "(555) 123-4567");
844        assert_eq!(drawn("#,##0 ;(#,##0)", 1234.0), "1,234 ");
845        assert_eq!(drawn("#,##0 ;(#,##0)", -1234.0), "(1,234)");
846        assert_eq!(drawn("\\€#,##0.00", 3.0), "€3.00");
847    }
848
849    #[test]
850    fn a_text_draws_by_the_fourth_section_alone() {
851        let c = code("0;-0;0;\"<\"@\">\"");
852        assert_eq!(c.text(&Value::Text("abc".into())), Some("<abc>".into()));
853        assert_eq!(code("0.00").text(&Value::Text("abc".into())), None);
854        assert_eq!(
855            code("0;-0;0;").text(&Value::Text("abc".into())),
856            Some(String::new())
857        );
858        assert_eq!(code("0.00").text(&Value::Bool(true)), None);
859        assert_eq!(code("0.00").text(&Value::Empty), None);
860        assert_eq!(code("0.00").text(&Value::Number(f64::INFINITY)), None);
861    }
862
863    #[test]
864    fn excel_s_built_ins_are_known_by_their_codes() {
865        assert_eq!(code("0.00").builtin(), Some(2));
866        assert_eq!(code("#,##0.00").builtin(), Some(4));
867        assert_eq!(code("#,##0 ;(#,##0)").builtin(), Some(37));
868        assert_eq!(code("0.000").builtin(), None);
869        assert_eq!(code("▲0.00;▼0.00;=0.00").builtin(), None);
870        assert_eq!(code("0.00"), code("0.00"));
871        assert_ne!(code("0.00"), code("0.00;-0.00"));
872    }
873
874    #[test]
875    fn what_this_version_does_not_draw_is_named() {
876        let named = |text: &str, word: &str| {
877            let u = refused(text);
878            assert!(u.what.contains(word), "{text}: {u:?}");
879            assert!(!u.fix.is_empty(), "{text}: {u:?}");
880        };
881        named("", "no code");
882        named("General", "General format");
883        named("general", "General format");
884        named("@", "Text format");
885        named("[Red]0.00", "[Red]");
886        named("0.00;[Red]-0.00", "color");
887        named("yyyy-mm-dd", "date");
888        named("0.00E+00", "E+");
889        named("# ?/?", "?");
890        named("0/0", "/");
891        named("_(0_)", "_");
892        named("* 0", "*");
893        named("General;-General", "General inside");
894        named("0 kg", "k");
895        named("0 1", "a digit");
896        named("0|", "|");
897        named("0.00.0", "second point");
898        named("0;0;0;@;0", "fifth section");
899        named("@;0", "@ before");
900        named("0;-0;0;0", "in a text's section");
901        named(",0", "before any digit");
902        named("0.0,0", "among the decimals");
903        named("0,.00", "just before the point");
904        named("\"x\".", "beside no digit");
905        named("\"open", "quote left open");
906        named("0\\", "backslash");
907        named(&format!("0.{}", "0".repeat(31)), "decimals");
908        named(&"0".repeat(256), "characters");
909        named("0\t", "control");
910        assert!(Code::parse(&format!("0.{}", "0".repeat(30))).is_ok());
911    }
912}