Skip to main content

ironcalc_base/formatter/
format.rs

1use chrono::Datelike;
2
3use crate::{
4    locale::Locale,
5    number_format::{parse_finite_number, to_precision},
6};
7
8use super::{
9    dates::{date_to_serial_number, from_excel_date},
10    parser::{ParsePart, Parser, TextToken},
11};
12
13pub struct Formatted {
14    pub color: Option<i32>,
15    pub text: String,
16    pub error: Option<String>,
17}
18
19/// Returns the vector of chars of the fractional part of a *positive* number:
20/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
21fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec<char> {
22    let b = format!("{:.1$}", value.fract(), precision as usize)
23        .chars()
24        .collect::<Vec<char>>();
25    let l = b.len() - 1;
26    let mut last_non_zero = b.len() - 1;
27    for i in 0..l {
28        if b[l - i] != '0' {
29            last_non_zero = l - i + 1;
30            break;
31        }
32    }
33    if last_non_zero < 2 {
34        return vec![];
35    }
36    let max_len = if int_len > 15 {
37        2_usize
38    } else {
39        15_usize - int_len + 1
40    };
41    let last_non_zero = usize::min(last_non_zero, max_len + 1);
42    b[2..last_non_zero].to_vec()
43}
44
45/// Return true if we need to add a separator in position digit_index
46/// It normally happens if if digit_index -1 is 3, 6, 9,... digit_index ≡ 1 mod 3
47fn use_group_separator(use_thousands: bool, digit_index: i32, group_sizes: &str) -> bool {
48    if use_thousands {
49        if group_sizes == "#,##0.###" {
50            if digit_index > 1 && (digit_index - 1) % 3 == 0 {
51                return true;
52            }
53        } else if group_sizes == "#,##,##0.###"
54            && (digit_index == 3 || (digit_index > 3 && digit_index % 2 == 0))
55        {
56            return true;
57        }
58    }
59    false
60}
61
62pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Formatted {
63    let mut parser = Parser::new(format);
64    parser.parse();
65    let parts = parser.parts;
66    // There are four parts:
67    // 1) Positive numbers
68    // 2) Negative numbers
69    // 3) Zero
70    // 4) Text
71    // If you specify only one section of format code, the code in that section is used for all numbers.
72    // If you specify two sections of format code, the first section of code is used
73    // for positive numbers and zeros, and the second section of code is used for negative numbers.
74    // When you skip code sections in your number format,
75    // you must include a semicolon for each of the missing sections of code.
76    // You can use the ampersand (&) text operator to join, or concatenate, two values.
77    let mut value = value_original;
78    let part;
79    match parts.len() {
80        1 => {
81            part = &parts[0];
82        }
83        2 => {
84            if value >= 0.0 {
85                part = &parts[0]
86            } else {
87                value = -value;
88                part = &parts[1];
89            }
90        }
91        3 => {
92            if value > 0.0 {
93                part = &parts[0]
94            } else if value < 0.0 {
95                value = -value;
96                part = &parts[1];
97            } else {
98                value = 0.0;
99                part = &parts[2];
100            }
101        }
102        4 => {
103            if value > 0.0 {
104                part = &parts[0]
105            } else if value < 0.0 {
106                value = -value;
107                part = &parts[1];
108            } else {
109                value = 0.0;
110                part = &parts[2];
111            }
112        }
113        _ => {
114            return Formatted {
115                text: "#VALUE!".to_owned(),
116                color: None,
117                error: Some("Too many parts".to_owned()),
118            };
119        }
120    }
121    match part {
122        ParsePart::Error(..) => Formatted {
123            text: "#VALUE!".to_owned(),
124            color: None,
125            error: Some("Problem parsing format string".to_owned()),
126        },
127        ParsePart::General(..) => {
128            // FIXME: This is "General formatting"
129            // We should have different codepaths for general formatting and errors
130            let value_abs = value.abs();
131            if (1.0e-8..1.0e+11).contains(&value_abs) {
132                let mut text = format!("{value:.9}");
133                text = text.trim_end_matches('0').trim_end_matches('.').to_string();
134                if locale.numbers.symbols.decimal != "." {
135                    text = text.replace('.', &locale.numbers.symbols.decimal.to_string());
136                }
137                Formatted {
138                    text,
139                    color: None,
140                    error: None,
141                }
142            } else {
143                if value_abs == 0.0 {
144                    return Formatted {
145                        text: "0".to_string(),
146                        color: None,
147                        error: None,
148                    };
149                }
150                let exponent = value_abs.log10().floor();
151                value /= 10.0_f64.powf(exponent);
152                let sign = if exponent < 0.0 { '-' } else { '+' };
153                let s = format!("{value:.5}");
154                let mut text = format!(
155                    "{}E{}{:02}",
156                    s.trim_end_matches('0').trim_end_matches('.'),
157                    sign,
158                    exponent.abs()
159                );
160                if locale.numbers.symbols.decimal != "." {
161                    text = text.replace('.', &locale.numbers.symbols.decimal.to_string());
162                }
163                Formatted {
164                    text,
165                    color: None,
166                    error: None,
167                }
168            }
169        }
170        ParsePart::Date(p) => {
171            let tokens = &p.tokens;
172            let mut text = "".to_string();
173            let time_fract = value.fract();
174            let hours = (time_fract * 24.0).floor();
175            let minutes = ((time_fract * 24.0 - hours) * 60.0).floor();
176            let seconds = ((((time_fract * 24.0 - hours) * 60.0) - minutes) * 60.0).round();
177            let date = from_excel_date(value as i64).ok();
178            for token in tokens {
179                match token {
180                    TextToken::Literal(c) => {
181                        text = format!("{text}{c}");
182                    }
183                    TextToken::Text(t) => {
184                        text = format!("{text}{t}");
185                    }
186                    TextToken::Ghost(_) => {
187                        // we just leave a whitespace
188                        // This is what the TEXT function does
189                        text = format!("{text} ");
190                    }
191                    TextToken::Spacer(_) => {
192                        // we just leave a whitespace
193                        // This is what the TEXT function does
194                        text = format!("{text} ");
195                    }
196                    TextToken::Raw => {
197                        text = format!("{text}{value}");
198                    }
199                    TextToken::Digit(_) => {}
200                    TextToken::Period => {}
201                    TextToken::Day => match date {
202                        Some(date) => {
203                            let day = date.day() as usize;
204                            text = format!("{text}{day}");
205                        }
206                        None => {
207                            return Formatted {
208                                text: "#VALUE!".to_owned(),
209                                color: None,
210                                error: Some(format!("Invalid date value: '{value}'")),
211                            }
212                        }
213                    },
214                    TextToken::DayPadded => {
215                        let date = match date {
216                            Some(d) => d,
217                            None => {
218                                return Formatted {
219                                    text: "#VALUE!".to_owned(),
220                                    color: None,
221                                    error: Some(format!("Invalid date value: '{value}'")),
222                                }
223                            }
224                        };
225                        let day = date.day() as usize;
226                        text = format!("{text}{day:02}");
227                    }
228                    TextToken::DayNameShort => {
229                        let date = match date {
230                            Some(d) => d,
231                            None => {
232                                return Formatted {
233                                    text: "#VALUE!".to_owned(),
234                                    color: None,
235                                    error: Some(format!("Invalid date value: '{value}'")),
236                                }
237                            }
238                        };
239                        let mut day = date.weekday().number_from_monday() as usize;
240                        if day == 7 {
241                            day = 0;
242                        }
243                        text = format!("{}{}", text, locale.dates.day_names_short[day]);
244                    }
245                    TextToken::DayName => {
246                        let date = match date {
247                            Some(d) => d,
248                            None => {
249                                return Formatted {
250                                    text: "#VALUE!".to_owned(),
251                                    color: None,
252                                    error: Some(format!("Invalid date value: '{value}'")),
253                                }
254                            }
255                        };
256                        let mut day = date.weekday().number_from_monday() as usize;
257                        if day == 7 {
258                            day = 0;
259                        }
260                        text = format!("{}{}", text, locale.dates.day_names[day]);
261                    }
262                    TextToken::Month => {
263                        let date = match date {
264                            Some(d) => d,
265                            None => {
266                                return Formatted {
267                                    text: "#VALUE!".to_owned(),
268                                    color: None,
269                                    error: Some(format!("Invalid date value: '{value}'")),
270                                }
271                            }
272                        };
273                        let month = date.month() as usize;
274                        text = format!("{text}{month}");
275                    }
276                    TextToken::MonthPadded => {
277                        let date = match date {
278                            Some(d) => d,
279                            None => {
280                                return Formatted {
281                                    text: "#VALUE!".to_owned(),
282                                    color: None,
283                                    error: Some(format!("Invalid date value: '{value}'")),
284                                }
285                            }
286                        };
287                        let month = date.month() as usize;
288                        text = format!("{text}{month:02}");
289                    }
290                    TextToken::MonthNameShort => {
291                        let date = match date {
292                            Some(d) => d,
293                            None => {
294                                return Formatted {
295                                    text: "#VALUE!".to_owned(),
296                                    color: None,
297                                    error: Some(format!("Invalid date value: '{value}'")),
298                                }
299                            }
300                        };
301                        let month = date.month() as usize;
302                        text = format!("{}{}", text, locale.dates.months_short[month - 1]);
303                    }
304                    TextToken::MonthName => {
305                        let date = match date {
306                            Some(d) => d,
307                            None => {
308                                return Formatted {
309                                    text: "#VALUE!".to_owned(),
310                                    color: None,
311                                    error: Some(format!("Invalid date value: '{value}'")),
312                                }
313                            }
314                        };
315                        let month = date.month() as usize;
316                        text = format!("{}{}", text, locale.dates.months[month - 1]);
317                    }
318                    TextToken::MonthLetter => {
319                        let date = match date {
320                            Some(d) => d,
321                            None => {
322                                return Formatted {
323                                    text: "#VALUE!".to_owned(),
324                                    color: None,
325                                    error: Some(format!("Invalid date value: '{value}'")),
326                                }
327                            }
328                        };
329                        let month = date.month() as usize;
330                        let months_letter = &locale.dates.months_letter[month - 1];
331                        text = format!("{text}{months_letter}");
332                    }
333                    TextToken::YearShort => {
334                        let date = match date {
335                            Some(d) => d,
336                            None => {
337                                return Formatted {
338                                    text: "#VALUE!".to_owned(),
339                                    color: None,
340                                    error: Some(format!("Invalid date value: '{value}'")),
341                                }
342                            }
343                        };
344                        text = format!("{}{}", text, date.format("%y"));
345                    }
346                    TextToken::Year => {
347                        let date = match date {
348                            Some(d) => d,
349                            None => {
350                                return Formatted {
351                                    text: "#VALUE!".to_owned(),
352                                    color: None,
353                                    error: Some(format!("Invalid date value: '{value}'")),
354                                }
355                            }
356                        };
357                        text = format!("{}{}", text, date.year());
358                    }
359                    TextToken::Hour => {
360                        let mut hour = hours as i32;
361                        if p.use_ampm {
362                            if hour == 0 {
363                                hour = 12;
364                            } else if hour > 12 {
365                                hour -= 12;
366                            }
367                        }
368                        text = format!("{text}{hour}");
369                    }
370                    TextToken::HourPadded => {
371                        let mut hour = hours as i32;
372                        if p.use_ampm {
373                            if hour == 0 {
374                                hour = 12;
375                            } else if hour > 12 {
376                                hour -= 12;
377                            }
378                        }
379                        text = format!("{text}{hour:02}");
380                    }
381                    TextToken::Second => {
382                        let second = seconds as i32;
383                        text = format!("{text}{second}");
384                    }
385                    TextToken::SecondPadded => {
386                        let second = seconds as i32;
387                        text = format!("{text}{second:02}");
388                    }
389                    TextToken::AMPM => {
390                        let ampm = if hours < 12.0 { "AM" } else { "PM" };
391                        text = format!("{text}{ampm}");
392                    }
393                    TextToken::Minute => {
394                        let minute = minutes as i32;
395                        text = format!("{text}{minute}");
396                    }
397                    TextToken::MinutePadded => {
398                        let minute = minutes as i32;
399                        text = format!("{text}{minute:02}");
400                    }
401                    TextToken::ElapsedHour => {
402                        let hour = (value * 24.0).floor() as i32;
403                        text = format!("{text}{hour}");
404                    }
405                    TextToken::ElapsedHourPadded => {
406                        let hour = (value * 24.0).floor() as i32;
407                        text = format!("{text}{hour:02}");
408                    }
409                    TextToken::ElapsedMinute => {
410                        let minute = (value * 24.0 * 60.0).floor() as i32;
411                        text = format!("{text}{minute}");
412                    }
413                    TextToken::ElapsedMinutePadded => {
414                        let minute = (value * 24.0 * 60.0).floor() as i32;
415                        text = format!("{text}{minute:02}");
416                    }
417                    TextToken::ElapsedSecond => {
418                        let second = (value * 24.0 * 60.0 * 60.0).floor() as i32;
419                        text = format!("{text}{second}");
420                    }
421                    TextToken::ElapsedSecondPadded => {
422                        let second = (value * 24.0 * 60.0 * 60.0).floor() as i32;
423                        text = format!("{text}{second:02}");
424                    }
425                }
426            }
427            Formatted {
428                text,
429                color: p.color,
430                error: None,
431            }
432        }
433        ParsePart::Number(p) => {
434            let mut text = "".to_string();
435            if let Some(c) = p.currency {
436                text = format!("{c}");
437            }
438            let tokens = &p.tokens;
439            value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
440            // p.precision is the number of significant digits _after_ the decimal point
441            value = to_precision(
442                value,
443                (p.precision as usize) + format!("{}", value.abs().floor()).len(),
444            );
445            let mut value_abs = value.abs();
446            let mut exponent_part: Vec<char> = vec![];
447            let mut exponent_is_negative = value_abs < 10.0;
448            if p.is_scientific {
449                if value_abs == 0.0 {
450                    exponent_part = vec!['0'];
451                    exponent_is_negative = false;
452                } else {
453                    // TODO: Implement engineering formatting.
454                    let exponent = value_abs.log10().floor();
455                    exponent_part = format!("{}", exponent.abs()).chars().collect();
456                    value /= 10.0_f64.powf(exponent);
457                    value = to_precision(value, 15);
458                    value_abs = value.abs();
459                }
460            }
461            let l_exp = exponent_part.len() as i32;
462            let int_number = if p.precision == 0 {
463                value_abs.round()
464            } else {
465                value_abs.floor()
466            };
467            let mut int_part: Vec<char> = format!("{}", int_number).chars().collect();
468            if int_number as i64 == 0 {
469                int_part = vec![];
470            }
471            let fract_part = get_fract_part(value_abs, p.precision, int_part.len());
472            // ln is the number of digits of the integer part of the value
473            let ln = int_part.len() as i32;
474            // digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
475            let digit_count = p.digit_count;
476            // digit_index points to the digit index in value that we have already formatted
477            let mut digit_index = 0;
478
479            let symbols = &locale.numbers.symbols;
480            let group_sizes = locale.numbers.decimal_formats.standard.to_owned();
481            let group_separator = symbols.group.to_owned();
482            let decimal_separator = symbols.decimal.to_owned();
483            // There probably are better ways to check if a number at a given precision is negative :/
484            let is_negative = value < -(10.0_f64.powf(-(p.precision as f64)));
485            let mut needs_period = false;
486
487            for token in tokens {
488                match token {
489                    TextToken::Literal(c) => {
490                        text = format!("{text}{c}");
491                    }
492                    TextToken::Text(t) => {
493                        text = format!("{text}{t}");
494                    }
495                    TextToken::Ghost(_) => {
496                        // we just leave a whitespace
497                        // This is what the TEXT function does
498                        text = format!("{text} ");
499                    }
500                    TextToken::Spacer(_) => {
501                        // we just leave a whitespace
502                        // This is what the TEXT function does
503                        text = format!("{text} ");
504                    }
505                    TextToken::Raw => {
506                        text = format!("{text}{value}");
507                    }
508                    TextToken::Period => {
509                        // if !fract_part.is_empty() &&  {
510                        //     text = format!("{text}{decimal_separator}");
511                        // }
512                        needs_period = true;
513                    }
514                    TextToken::Digit(digit) => {
515                        if digit.number.is_integer() {
516                            // 1. Integer part
517                            let index = digit.index;
518                            let number_index = ln - digit_count + index;
519                            if index == 0 && is_negative {
520                                text = format!("-{text}");
521                            }
522                            if ln <= digit_count {
523                                // The number of digits is less or equal than the number of digit tokens
524                                // i.e. the value is 123 and the format_code is ##### (ln = 3 and digit_count = 5)
525                                if !(number_index < 0 && digit.kind == '#') {
526                                    let c = if number_index < 0 {
527                                        if digit.kind == '0' {
528                                            '0'
529                                        } else {
530                                            // digit.kind = '?'
531                                            ' '
532                                        }
533                                    } else {
534                                        int_part[number_index as usize]
535                                    };
536                                    let sep = if use_group_separator(
537                                        p.use_thousands,
538                                        ln - digit_index,
539                                        &group_sizes,
540                                    ) {
541                                        &group_separator
542                                    } else {
543                                        ""
544                                    };
545                                    text = format!("{text}{c}{sep}");
546                                }
547                                digit_index += 1;
548                            } else {
549                                // The number is larger than the formatting code 12345 and 0##
550                                // We just hit the first formatting digit (0 in the example above) so we write as many digits as we can (123 in the example)
551                                for i in digit_index..number_index + 1 {
552                                    let sep = if use_group_separator(
553                                        p.use_thousands,
554                                        ln - i,
555                                        &group_sizes,
556                                    ) {
557                                        &group_separator
558                                    } else {
559                                        ""
560                                    };
561                                    text = format!("{}{}{}", text, int_part[i as usize], sep);
562                                }
563                                digit_index = number_index + 1;
564                            }
565                        } else if digit.number.is_decimal() {
566                            // 2. After the decimal point
567                            let index = digit.index as usize;
568                            if index < fract_part.len() {
569                                if needs_period {
570                                    text = format!(
571                                        "{}{}{}",
572                                        text, decimal_separator, fract_part[index]
573                                    );
574                                } else {
575                                    text = format!("{}{}", text, fract_part[index]);
576                                }
577                            } else if digit.kind == '0' {
578                                if needs_period {
579                                    text = format!("{text}{}0", decimal_separator);
580                                } else {
581                                    text = format!("{text}0");
582                                }
583                            } else if digit.kind == '?' {
584                                text = format!("{text} ");
585                            } else if digit.kind == '#' && needs_period {
586                                // FIXME: This is what Excel does, but it transforms "3" into "3."
587                                text = format!("{text}{}", decimal_separator);
588                            }
589                            needs_period = false;
590                        } else if digit.number.is_exponent() {
591                            // 3. Exponent part
592                            let index = digit.index;
593                            if index == 0 {
594                                if exponent_is_negative {
595                                    text = format!("{text}E-");
596                                } else if p.scientific_minus {
597                                    text = format!("{text}E");
598                                } else {
599                                    text = format!("{text}E+");
600                                }
601                            }
602                            let number_index = l_exp - (p.exponent_digit_count - index);
603                            if l_exp <= p.exponent_digit_count {
604                                if !(number_index < 0 && digit.kind == '#') {
605                                    let c = if number_index < 0 {
606                                        if digit.kind == '?' {
607                                            ' '
608                                        } else {
609                                            '0'
610                                        }
611                                    } else {
612                                        exponent_part[number_index as usize]
613                                    };
614
615                                    text = format!("{text}{c}");
616                                }
617                            } else {
618                                for i in 0..number_index + 1 {
619                                    text = format!("{}{}", text, exponent_part[i as usize]);
620                                }
621                                digit_index += number_index + 1;
622                            }
623                        }
624                    }
625                    // Date tokens should not be present
626                    TextToken::Day => {}
627                    TextToken::DayPadded => {}
628                    TextToken::DayNameShort => {}
629                    TextToken::DayName => {}
630                    TextToken::Month => {}
631                    TextToken::MonthPadded => {}
632                    TextToken::MonthNameShort => {}
633                    TextToken::MonthName => {}
634                    TextToken::MonthLetter => {}
635                    TextToken::YearShort => {}
636                    TextToken::Year => {}
637                    TextToken::Hour => {}
638                    TextToken::HourPadded => {}
639                    TextToken::Minute => {}
640                    TextToken::MinutePadded => {}
641                    TextToken::Second => {}
642                    TextToken::SecondPadded => {}
643                    TextToken::AMPM => {}
644                    TextToken::ElapsedHour => {}
645                    TextToken::ElapsedHourPadded => {}
646                    TextToken::ElapsedMinute => {}
647                    TextToken::ElapsedMinutePadded => {}
648                    TextToken::ElapsedSecond => {}
649                    TextToken::ElapsedSecondPadded => {}
650                }
651            }
652            Formatted {
653                text,
654                color: p.color,
655                error: None,
656            }
657        }
658    }
659}
660
661fn parse_day(day_str: &str) -> Result<(u32, String), String> {
662    let bytes = day_str.bytes();
663    let bytes_len = bytes.len();
664    if bytes_len <= 2 {
665        match day_str.parse::<u32>() {
666            Ok(y) => {
667                if bytes_len == 2 {
668                    return Ok((y, "dd".to_string()));
669                } else {
670                    return Ok((y, "d".to_string()));
671                }
672            }
673            Err(_) => return Err("Not a valid day".to_string()),
674        }
675    }
676    Err("Not a valid day".to_string())
677}
678
679fn parse_month(month_str: &str, locale: &Locale) -> Result<(u32, String), String> {
680    let bytes = month_str.bytes();
681    let bytes_len = bytes.len();
682    if bytes_len <= 2 {
683        match month_str.parse::<u32>() {
684            Ok(y) => {
685                if bytes_len == 2 {
686                    return Ok((y, "mm".to_string()));
687                } else {
688                    return Ok((y, "m".to_string()));
689                }
690            }
691            Err(_) => return Err("Not a valid month".to_string()),
692        }
693    }
694
695    let month_names_short = &locale.dates.months_short;
696    let month_names_long = &locale.dates.months;
697    if let Some(m) = month_names_short.iter().position(|r| r == month_str) {
698        return Ok((m as u32 + 1, "mmm".to_string()));
699    }
700    if let Some(m) = month_names_long.iter().position(|r| r == month_str) {
701        return Ok((m as u32 + 1, "mmmm".to_string()));
702    }
703    Err("Not a valid month".to_string())
704}
705
706fn parse_year(year_str: &str) -> Result<(i32, String), String> {
707    // year is either 2 digits or 4 digits
708    // 23 -> 2023
709    // 75 -> 1975
710    // 30 is the split number (yeah, that's not going to be a problem any time soon)
711    // 30 => 1930
712    // 29 => 2029
713    let bytes = year_str.bytes();
714    let bytes_len = bytes.len();
715    if bytes_len != 2 && bytes_len != 4 {
716        return Err("Not a valid year".to_string());
717    }
718    match year_str.parse::<i32>() {
719        Ok(y) => {
720            if y < 30 {
721                Ok((2000 + y, "yy".to_string()))
722            } else if y < 100 {
723                Ok((1900 + y, "yy".to_string()))
724            } else {
725                Ok((y, "yyyy".to_string()))
726            }
727        }
728        Err(_) => Err("Not a valid year".to_string()),
729    }
730}
731
732// Check if it is a date. Other spreadsheet engines support a wide variety of dates formats
733// Here we support a small subset of them.
734//
735// The grammar is:
736//
737// date -> long_date | short_date | iso-date
738// short_date -> month separator year
739// long_date -> day separator month separator year
740// iso_date -> long_year separator number_month separator number_day
741// separator -> "/" | "-" | "."
742// day -> number | padded number
743// month -> number_month | name_month
744// number_month -> number | padded number |
745// name_month -> short name | full name
746// year -> short_year | long year
747//
748// NOTE 1: The separator has to be the same
749// NOTE 2: In some engines "2/3" is implemented ad "2/March of the present year"
750// NOTE 3: I did not implement the "short date"
751pub(crate) fn parse_date(value: &str, locale: &Locale) -> Result<(i32, String), String> {
752    let separator = if value.contains('/') {
753        '/'
754    } else if value.contains('-') {
755        '-'
756    } else if value.contains('.') {
757        '.'
758    } else {
759        return Err("Not a valid date".to_string());
760    };
761
762    let parts: Vec<&str> = value.split(separator).collect();
763    let mut is_iso_date = false;
764    let mut day_first = true;
765    let (day_str, month_str, year_str) = if parts.len() == 3 {
766        if parts[0].len() == 4 {
767            // ISO date  yyyy-mm-dd
768            if !parts[1].chars().all(char::is_numeric) {
769                return Err("Not a valid date".to_string());
770            }
771            if !parts[2].chars().all(char::is_numeric) {
772                return Err("Not a valid date".to_string());
773            }
774            is_iso_date = true;
775            (parts[2], parts[1], parts[0])
776        } else {
777            //  localized date dd-mm-yyyy or mm-dd-yyyy
778            // TODO: A bit hacky, but works for now
779            if locale.dates.date_formats.short.starts_with('d') {
780                (parts[0], parts[1], parts[2])
781            } else {
782                day_first = false;
783                (parts[1], parts[0], parts[2])
784            }
785        }
786    } else {
787        return Err("Not a valid date".to_string());
788    };
789    let (day, day_format) = parse_day(day_str)?;
790    let (month, month_format) = parse_month(month_str, locale)?;
791    let (year, year_format) = parse_year(year_str)?;
792    let serial_number = match date_to_serial_number(day, month, year) {
793        Ok(n) => n,
794        Err(_) => return Err("Not a valid date".to_string()),
795    };
796    if is_iso_date {
797        Ok((
798            serial_number,
799            format!("yyyy{separator}{month_format}{separator}{day_format}"),
800        ))
801    } else if !day_first {
802        Ok((
803            serial_number,
804            format!("{month_format}{separator}{day_format}{separator}{year_format}"),
805        ))
806    } else {
807        Ok((
808            serial_number,
809            format!("{day_format}{separator}{month_format}{separator}{year_format}"),
810        ))
811    }
812}
813
814/// Parses a formatted number, returning the numeric value together with the format
815/// Uses heuristics to guess the format string
816/// "$ 123,345.678" => (123345.678, "$#,##0.00")
817/// "30.34%" => (0.3034, "0.00%")
818/// 100€ => (100, "100€")
819pub(crate) fn parse_formatted_number(
820    original: &str,
821    currencies: &[&str],
822    locale: &Locale,
823) -> Result<(f64, Option<String>), String> {
824    let value = original.trim();
825    let scientific_format = "0.00E+00";
826
827    let symbols = &locale.numbers.symbols;
828    let decimal_separator = symbols.decimal.chars().next().unwrap_or('.');
829    let group_separator = symbols.group.chars().next().unwrap_or(',');
830
831    // Check if it is a percentage
832    if let Some(p) = value.strip_suffix('%') {
833        let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
834        if options.is_scientific {
835            return Ok((f / 100.0, Some(scientific_format.to_string())));
836        }
837        // We ignore the separator
838        if options.decimal_digits > 0 {
839            // Percentage format with decimals
840            return Ok((f / 100.0, Some("#,##0.00%".to_string())));
841        }
842        // Percentage format standard
843        return Ok((f / 100.0, Some("#,##0%".to_string())));
844    }
845
846    // check if it is a currency in currencies
847    for currency in currencies {
848        if let Some(p) = value.strip_prefix(&format!("-{currency}")) {
849            let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
850            if options.is_scientific {
851                return Ok((f, Some(scientific_format.to_string())));
852            }
853            if options.decimal_digits > 0 {
854                return Ok((-f, Some(format!("{currency}#,##0.00"))));
855            }
856            return Ok((-f, Some(format!("{currency}#,##0"))));
857        } else if let Some(p) = value.strip_prefix(currency) {
858            let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
859            if options.is_scientific {
860                return Ok((f, Some(scientific_format.to_string())));
861            }
862            if options.decimal_digits > 0 {
863                return Ok((f, Some(format!("{currency}#,##0.00"))));
864            }
865            return Ok((f, Some(format!("{currency}#,##0"))));
866        } else if let Some(p) = value.strip_suffix(currency) {
867            let (f, options) = parse_number(p.trim(), decimal_separator, group_separator)?;
868            if options.is_scientific {
869                return Ok((f, Some(scientific_format.to_string())));
870            }
871            if options.decimal_digits > 0 {
872                let currency_format = &format!("#,##0.00{currency}");
873                return Ok((f, Some(currency_format.to_string())));
874            }
875            let currency_format = &format!("#,##0{currency}");
876            return Ok((f, Some(currency_format.to_string())));
877        }
878    }
879
880    // check if it is a date. NOTE: we don't trim the original here
881    if let Ok((serial_number, format)) = parse_date(original, locale) {
882        return Ok((serial_number as f64, Some(format)));
883    }
884
885    // Lastly we check if it is a number
886    let (f, options) = parse_number(value, decimal_separator, group_separator)?;
887    if options.is_scientific {
888        return Ok((f, Some(scientific_format.to_string())));
889    }
890    if options.has_commas {
891        if options.decimal_digits > 0 {
892            // group separator and two decimal points
893            return Ok((f, Some("#,##0.00".to_string())));
894        }
895        // Group separator and no decimal points
896        return Ok((f, Some("#,##0".to_string())));
897    }
898    Ok((f, None))
899}
900
901struct NumberOptions {
902    has_commas: bool,
903    is_scientific: bool,
904    decimal_digits: usize,
905}
906
907// tries to parse 'value' as a number.
908// If it is a number it either uses commas as thousands separator or it does not
909fn parse_number(
910    value: &str,
911    decimal_separator: char,
912    group_separator: char,
913) -> Result<(f64, NumberOptions), String> {
914    let mut position = 0;
915    let characters: Vec<char> = value.chars().collect();
916    let len = characters.len();
917    if len == 0 {
918        return Err("Cannot parse number".to_string());
919    }
920    let mut chars = String::from("");
921    let mut group_separator_index = Vec::new();
922    // get the sign
923    let sign = if characters[0] == '-' {
924        position += 1;
925        -1.0
926    } else if characters[0] == '+' {
927        position += 1;
928        1.0
929    } else {
930        1.0
931    };
932
933    if position >= len {
934        return Err("Cannot parse number".to_string());
935    }
936
937    if characters[position] == group_separator {
938        return Err("Cannot parse number".to_string());
939    }
940
941    // numbers before the decimal point
942    while position < len {
943        let x = characters[position];
944        if x.is_ascii_digit() {
945            chars.push(x);
946        } else if x == group_separator {
947            group_separator_index.push(chars.len());
948        } else {
949            break;
950        }
951        position += 1;
952    }
953    // Check the group separator is in multiples of three
954    for index in &group_separator_index {
955        if (chars.len() - index) % 3 != 0 {
956            return Err("Cannot parse number".to_string());
957        }
958    }
959    let mut decimal_digits = 0;
960    if position < len && characters[position] == decimal_separator {
961        // numbers after the decimal point
962        chars.push('.');
963        position += 1;
964        let start_position = 0;
965        while position < len {
966            let x = characters[position];
967            if x.is_ascii_digit() {
968                chars.push(x);
969            } else {
970                break;
971            }
972            position += 1;
973        }
974        decimal_digits = position - start_position;
975    }
976    let mut is_scientific = false;
977    if position + 1 < len && (characters[position] == 'e' || characters[position] == 'E') {
978        // exponential side
979        is_scientific = true;
980        let x = characters[position + 1];
981        if x == '-' || x == '+' || x.is_ascii_digit() {
982            chars.push('e');
983            chars.push(x);
984            position += 2;
985            while position < len {
986                let x = characters[position];
987                if x.is_ascii_digit() {
988                    chars.push(x);
989                } else {
990                    break;
991                }
992                position += 1;
993            }
994        }
995    }
996    if position != len {
997        return Err("Could not parse number".to_string());
998    };
999    match parse_finite_number(&chars) {
1000        None => Err("Failed to parse to double".to_string()),
1001        Some(v) => Ok((
1002            sign * v,
1003            NumberOptions {
1004                has_commas: !group_separator_index.is_empty(),
1005                is_scientific,
1006                decimal_digits,
1007            },
1008        )),
1009    }
1010}