Skip to main content

ironcalc_base/formatter/
format.rs

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