Skip to main content

ironcalc_base/formatter/
lexer.rs

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