Skip to main content

ironcalc_base/formatter/
lexer.rs

1use crate::number_format::parse_finite_number;
2
3pub struct Lexer {
4    position: usize,
5    len: usize,
6    chars: Vec<char>,
7    error_message: String,
8    error_position: usize,
9}
10
11#[derive(PartialEq, Debug)]
12pub enum Token {
13    Color(i32),              // [Red] or [Color 23]
14    Condition(Compare, f64), // [<=100] (Comparator, number)
15    Currency(char),          // [$€] ($ currency symbol)
16    Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or escaped \X
17    Spacer(char),  // *X
18    Ghost(char),   // _X
19    Text(String),  // "Text"
20    Separator,     // ;
21    Raw,           // @
22    Percent,       // %
23    Comma,         // ,
24    Period,        // .
25    Sharp,         // #
26    Zero,          // 0
27    QuestionMark,  // ?
28    Scientific,    // E+
29    ScientificMinus, // E-
30    General,       // General
31    // Dates and time
32    Day,                 // d
33    DayPadded,           // dd
34    DayNameShort,        // ddd
35    DayName,             // dddd+
36    Month,               // m (or minute)
37    MonthPadded,         // mm (or minute padded)
38    MonthNameShort,      // mmm
39    MonthName,           // mmmm or mmmmmm+
40    MonthLetter,         // mmmmm
41    YearShort,           // y or yy
42    Year,                // yyy+
43    Hour,                // h
44    HourPadded,          // hh
45    Second,              // s
46    SecondPadded,        // ss
47    ElapsedHour,         // [h]
48    ElapsedMinute,       // [m]
49    ElapsedSecond,       // [s]
50    ElapsedHourPadded,   // [hh]
51    ElapsedMinutePadded, // [mm]
52    ElapsedSecondPadded, // [ss]
53    AMPM,                // AM/PM (or A/P)
54    ILLEGAL,
55    EOF,
56}
57
58#[derive(PartialEq, Eq, Debug)]
59pub enum Compare {
60    Equal,
61    LessThan,
62    GreaterThan,
63    LessOrEqualThan,
64    GreaterOrEqualThan,
65}
66
67impl Token {
68    pub fn is_digit(&self) -> bool {
69        (self == &Token::Zero) || (self == &Token::Sharp) || (self == &Token::QuestionMark)
70    }
71
72    pub fn is_date(&self) -> bool {
73        self == &Token::Day
74            || self == &Token::DayPadded
75            || self == &Token::DayNameShort
76            || self == &Token::DayName
77            || self == &Token::MonthName
78            || self == &Token::MonthNameShort
79            || self == &Token::Month
80            || self == &Token::MonthPadded
81            || self == &Token::MonthLetter
82            || self == &Token::YearShort
83            || self == &Token::Year
84    }
85}
86
87impl Lexer {
88    pub fn new(format: &str) -> Lexer {
89        let chars: Vec<char> = format.chars().collect();
90        let len = chars.len();
91        Lexer {
92            chars,
93            position: 0,
94            len,
95            error_message: "".to_string(),
96            error_position: 0,
97        }
98    }
99
100    fn peek_char(&self) -> Option<char> {
101        let position = self.position;
102        if position < self.len {
103            Some(self.chars[position])
104        } else {
105            None
106        }
107    }
108
109    fn read_next_char(&mut self) -> Option<char> {
110        let position = self.position;
111        if position < self.len {
112            self.position = position + 1;
113            Some(self.chars[position])
114        } else {
115            None
116        }
117    }
118
119    fn set_error(&mut self, error: &str) {
120        self.error_message = error.to_string();
121        self.error_position = self.position;
122        self.position = self.len;
123    }
124
125    fn consume_string(&mut self) -> Option<String> {
126        let mut position = self.position;
127        let len = self.len;
128        let mut chars = "".to_string();
129        while position < len {
130            let x = self.chars[position];
131            position += 1;
132            if x != '"' {
133                chars.push(x);
134            } else if position < len && self.chars[position] == '"' {
135                chars.push(x);
136                position += 1;
137            } else {
138                self.position = position;
139                return Some(chars);
140            }
141        }
142        None
143    }
144
145    fn consume_number(&mut self) -> Option<f64> {
146        let mut position = self.position;
147        let len = self.len;
148        let mut chars = "".to_string();
149        // first optional '-' sign
150        if position < len && self.chars[position] == '-' {
151            chars.push('-');
152            position += 1;
153        }
154        // numbers before the '.'
155        while position < len {
156            let x = self.chars[position];
157            if x.is_ascii_digit() {
158                chars.push(x);
159            } else {
160                break;
161            }
162            position += 1;
163        }
164        if position < len && self.chars[position] == '.' {
165            // numbers after the'.'
166            chars.push('.');
167            position += 1;
168            while position < len {
169                let x = self.chars[position];
170                if x.is_ascii_digit() {
171                    chars.push(x);
172                } else {
173                    break;
174                }
175                position += 1;
176            }
177        }
178        if position + 1 < len && self.chars[position].eq_ignore_ascii_case(&'e') {
179            // exponential side
180            let x = self.chars[position + 1];
181            if x == '-' || x == '+' || x.is_ascii_digit() {
182                chars.push('e');
183                chars.push(x);
184                position += 2;
185                while position < len {
186                    let x = self.chars[position];
187                    if x.is_ascii_digit() {
188                        chars.push(x);
189                    } else {
190                        break;
191                    }
192                    position += 1;
193                }
194            }
195        }
196        self.position = position;
197        parse_finite_number(&chars)
198    }
199
200    fn consume_condition(&mut self) -> Option<(Compare, f64)> {
201        let cmp;
202        match self.read_next_char() {
203            Some('<') => {
204                if let Some('=') = self.peek_char() {
205                    self.read_next_char();
206                    cmp = Compare::LessOrEqualThan;
207                } else {
208                    cmp = Compare::LessThan;
209                }
210            }
211            Some('>') => {
212                if let Some('=') = self.peek_char() {
213                    self.read_next_char();
214                    cmp = Compare::GreaterOrEqualThan;
215                } else {
216                    cmp = Compare::GreaterThan;
217                }
218            }
219            Some('=') => {
220                cmp = Compare::Equal;
221            }
222            _ => {
223                return None;
224            }
225        }
226
227        let start = self.position;
228        let value = match self.consume_number() {
229            Some(v) => v,
230            // Without a number, as in `[>]`, the condition compares with 0
231            None if self.position == start => 0.0,
232            // There is something that should be a number and is not one,
233            // like `1e400` that is too large to hold: that is no condition
234            None => return None,
235        };
236        if self.read_next_char() == Some(']') {
237            return Some((cmp, value));
238        }
239        None
240    }
241
242    // Consume elapsed time like [h], [hh], [m], [mm], [s], [ss]
243    fn consume_elapsed_time(&mut self) -> Option<Token> {
244        // We are called right after '['
245        let start_pos = self.position;
246
247        // First char must be one of: h, m, s
248        let unit = match self.read_next_char() {
249            Some(c) if ['h', 'H', 'm', 'M', 's', 'S'].contains(&c) => c.to_ascii_lowercase(),
250            _ => {
251                // Not elapsed time → rewind
252                self.position = start_pos;
253                return None;
254            }
255        };
256
257        // Count how many times the same unit repeats (so we detect h vs hh, etc.)
258        let mut count = 1;
259        while let Some(ch) = self.peek_char() {
260            if ch.to_ascii_lowercase() == unit {
261                count += 1;
262                self.read_next_char();
263            } else {
264                break;
265            }
266        }
267
268        // Must be closed by ']'
269        if self.read_next_char() != Some(']') {
270            // Not a well-formed [h], [hh], etc. → rewind
271            self.position = start_pos;
272            return None;
273        }
274
275        // Map (unit, count) to a token
276        let token = match (unit, count) {
277            ('h', 1) => Token::ElapsedHour,
278            ('h', 2) => Token::ElapsedHourPadded,
279            ('m', 1) => Token::ElapsedMinute,
280            ('m', 2) => Token::ElapsedMinutePadded,
281            ('s', 1) => Token::ElapsedSecond,
282            ('s', 2) => Token::ElapsedSecondPadded,
283            // Anything else like [hhh], [mmm], [sss] → not considered elapsed time
284            _ => {
285                self.position = start_pos;
286                return None;
287            }
288        };
289
290        Some(token)
291    }
292
293    fn consume_color(&mut self) -> Option<i32> {
294        let colors = [
295            "black", "white", "red", "green", "blue", "yellow", "magenta",
296        ];
297        let mut chars = "".to_string();
298        while let Some(ch) = self.read_next_char() {
299            if ch == ']' {
300                let lc = chars.to_lowercase();
301                if let Some(index) = colors.iter().position(|&x| x == lc) {
302                    return Some(index as i32);
303                }
304                if !chars.starts_with("Color") {
305                    return None;
306                }
307                if let Ok(index) = chars[5..].trim().parse::<i32>() {
308                    if index < 57 && index > 0 {
309                        return Some(index);
310                    } else {
311                        return None;
312                    }
313                }
314                return None;
315            } else {
316                chars.push(ch);
317            }
318        }
319        None
320    }
321
322    pub fn peek_token(&mut self) -> Token {
323        let position = self.position;
324        let token = self.next_token();
325        self.position = position;
326        token
327    }
328
329    pub fn next_token(&mut self) -> Token {
330        let ch = self.read_next_char();
331        match ch {
332            Some(x) => match x {
333                '$' | '€' | '(' | ')' | '/' | ':' | '+' | '-' | '^' | '\'' | '{' | '}' | '<'
334                | '=' | '!' | '~' | '>' | ' ' => Token::Literal(x),
335                '?' => Token::QuestionMark,
336                ';' => Token::Separator,
337                '#' => Token::Sharp,
338                ',' => Token::Comma,
339                '.' => Token::Period,
340                '0' => Token::Zero,
341                '@' => Token::Raw,
342                '%' => Token::Percent,
343                '[' => {
344                    if let Some(c) = self.peek_char() {
345                        if c == '<' || c == '>' || c == '=' {
346                            // Condition
347                            if let Some((cmp, value)) = self.consume_condition() {
348                                Token::Condition(cmp, value)
349                            } else {
350                                self.set_error("Failed to parse condition");
351                                Token::ILLEGAL
352                            }
353                        } else if c == '$' {
354                            // currency: [$<symbol>] or [$<symbol>-<locale>]
355                            // IronCalc ignores the currency/locale modifiers for now
356                            // They are of two types:
357                            //  * BCP-47 locale tag (like [$€-fr-FR])
358                            //  * hex LCID (old format)
359                            //      Like [$$-409] (=1033 for en-US) or
360                            //           [$$-40C] (=1036 for fr-FR)
361                            self.read_next_char();
362                            if let Some(currency) = self.read_next_char() {
363                                // ignore the locale part for now, just check if it ends with ']'
364                                if Some('-') == self.peek_char() {
365                                    self.read_next_char();
366
367                                    while let Some(c) = self.peek_char() {
368                                        if c == ']' {
369                                            break;
370                                        }
371                                        self.read_next_char();
372                                    }
373                                }
374                                if self.read_next_char() == Some(']') {
375                                    return Token::Currency(currency);
376                                }
377                            }
378                            self.set_error("Failed to parse currency");
379                            Token::ILLEGAL
380                        } else {
381                            // try bracketed (elapsed) time
382                            if let Some(token) = self.consume_elapsed_time() {
383                                return token;
384                            }
385                            // Color
386                            if let Some(index) = self.consume_color() {
387                                return Token::Color(index);
388                            }
389                            self.set_error("Failed to parse color");
390                            Token::ILLEGAL
391                        }
392                    } else {
393                        self.set_error("Unexpected end of input");
394                        Token::ILLEGAL
395                    }
396                }
397                '_' => {
398                    if let Some(y) = self.read_next_char() {
399                        Token::Ghost(y)
400                    } else {
401                        self.set_error("Unexpected end of input");
402                        Token::ILLEGAL
403                    }
404                }
405                '*' => {
406                    if let Some(y) = self.read_next_char() {
407                        Token::Spacer(y)
408                    } else {
409                        self.set_error("Unexpected end of input");
410                        Token::ILLEGAL
411                    }
412                }
413                '\\' => {
414                    if let Some(y) = self.read_next_char() {
415                        Token::Literal(y)
416                    } else {
417                        self.set_error("Unexpected end of input");
418                        Token::ILLEGAL
419                    }
420                }
421                '"' => {
422                    if let Some(s) = self.consume_string() {
423                        Token::Text(s)
424                    } else {
425                        self.set_error("Did not find end of text string");
426                        Token::ILLEGAL
427                    }
428                }
429                'E' => {
430                    if let Some(s) = self.read_next_char() {
431                        if s == '+' {
432                            Token::Scientific
433                        } else if s == '-' {
434                            Token::ScientificMinus
435                        } else {
436                            self.set_error(&format!("Unexpected char: {s}. Expected + or -"));
437                            Token::ILLEGAL
438                        }
439                    } else {
440                        self.set_error("Unexpected end of input");
441                        Token::ILLEGAL
442                    }
443                }
444                'd' | 'D' => {
445                    let mut d = 1;
446                    while let Some(c) = self.peek_char() {
447                        if c.eq_ignore_ascii_case(&'d') {
448                            d += 1;
449                            self.read_next_char();
450                        } else {
451                            break;
452                        }
453                    }
454                    match d {
455                        1 => Token::Day,
456                        2 => Token::DayPadded,
457                        3 => Token::DayNameShort,
458                        _ => Token::DayName,
459                    }
460                }
461                'm' | 'M' => {
462                    let mut m = 1;
463                    while let Some(c) = self.peek_char() {
464                        if c.eq_ignore_ascii_case(&'m') {
465                            m += 1;
466                            self.read_next_char();
467                        } else {
468                            break;
469                        }
470                    }
471                    match m {
472                        1 => Token::Month,       // (or minute)
473                        2 => Token::MonthPadded, // (or minute padded)
474                        3 => Token::MonthNameShort,
475                        4 => Token::MonthName,
476                        5 => Token::MonthLetter,
477                        _ => Token::MonthName,
478                    }
479                }
480                'y' | 'Y' => {
481                    let mut y = 1;
482                    while let Some(c) = self.peek_char() {
483                        if c.eq_ignore_ascii_case(&'y') {
484                            y += 1;
485                            self.read_next_char();
486                        } else {
487                            break;
488                        }
489                    }
490                    if y == 1 || y == 2 {
491                        Token::YearShort
492                    } else {
493                        Token::Year
494                    }
495                }
496                'h' | 'H' => {
497                    let mut h = 1;
498                    while let Some(c) = self.peek_char() {
499                        if c.eq_ignore_ascii_case(&'h') {
500                            h += 1;
501                            self.read_next_char();
502                        } else {
503                            break;
504                        }
505                    }
506                    if h == 1 {
507                        Token::Hour
508                    } else if h == 2 {
509                        Token::HourPadded
510                    } else {
511                        self.set_error("Unexpected character after 'h'");
512                        Token::ILLEGAL
513                    }
514                }
515                's' | 'S' => {
516                    let mut s = 1;
517                    while let Some(c) = self.peek_char() {
518                        if c.eq_ignore_ascii_case(&'s') {
519                            s += 1;
520                            self.read_next_char();
521                        } else {
522                            break;
523                        }
524                    }
525                    if s == 1 {
526                        Token::Second
527                    } else if s == 2 {
528                        Token::SecondPadded
529                    } else {
530                        self.set_error("Unexpected character after 's'");
531                        Token::ILLEGAL
532                    }
533                }
534                'A' | 'a' => {
535                    if let Some('M') | Some('m') = self.peek_char() {
536                        self.read_next_char();
537                    } else {
538                        self.set_error("Unexpected character after 'A'");
539                        return Token::ILLEGAL;
540                    }
541                    if let Some('/') = self.peek_char() {
542                        self.read_next_char();
543                    } else {
544                        self.set_error("Unexpected character after 'AM'");
545                        return Token::ILLEGAL;
546                    }
547                    if let Some('P') | Some('p') = self.peek_char() {
548                        self.read_next_char();
549                    } else {
550                        self.set_error("Unexpected character after 'AM'");
551                        return Token::ILLEGAL;
552                    }
553                    if let Some('M') | Some('m') = self.peek_char() {
554                        self.read_next_char();
555                    } else {
556                        self.set_error("Unexpected character after 'AMP'");
557                        return Token::ILLEGAL;
558                    }
559                    Token::AMPM
560                }
561                'g' | 'G' => {
562                    for c in "eneral".chars() {
563                        let cc = self.read_next_char();
564                        if Some(c) != cc {
565                            self.set_error(&format!("Unexpected character: {x}"));
566                            return Token::ILLEGAL;
567                        }
568                    }
569                    Token::General
570                }
571                _ => {
572                    self.set_error(&format!("Unexpected character: {x}"));
573                    Token::ILLEGAL
574                }
575            },
576            None => Token::EOF,
577        }
578    }
579}
580
581pub fn is_likely_date_number_format(format: &str) -> bool {
582    let mut lexer = Lexer::new(format);
583    loop {
584        let token = lexer.next_token();
585        if token == Token::EOF {
586            return false;
587        }
588        if token.is_date() {
589            return true;
590        }
591    }
592}