Skip to main content

ironcalc_base/formatter/
parser.rs

1use super::lexer::{Compare, Lexer, Token};
2
3pub struct Digit {
4    pub kind: char, // '#' | '?' | '0'
5    pub index: i32,
6    pub number: NumberState, // 'i' | 'd' | 'e' (integer, decimal or exponent)
7}
8
9pub enum TextToken {
10    Literal(char),
11    Text(String),
12    Ghost(char),
13    Spacer(char),
14    // Text
15    Raw,
16    Digit(Digit),
17    Period,
18    // Dates
19    Day,
20    DayPadded,
21    DayNameShort,
22    DayName,
23    Month,
24    MonthPadded,
25    MonthNameShort,
26    MonthName,
27    MonthLetter,
28    YearShort,
29    Year,
30    Hour,
31    HourPadded,
32    Minute,
33    MinutePadded,
34    Second,
35    SecondPadded,
36    ElapsedHour,
37    ElapsedHourPadded,
38    ElapsedMinute,
39    ElapsedMinutePadded,
40    ElapsedSecond,
41    ElapsedSecondPadded,
42    AMPM,
43}
44pub struct NumberPart {
45    pub color: Option<i32>,
46    pub condition: Option<(Compare, f64)>,
47    pub use_thousands: bool,
48    pub percent: i32, // multiply number by 100^percent
49    pub comma: i32,   // divide number by 1000^comma
50    pub tokens: Vec<TextToken>,
51    pub digit_count: i32, // number of digit tokens (#, 0 or ?) to the left of the decimal point
52    pub precision: i32,   // number of digits to the right of the decimal point
53    pub is_scientific: bool,
54    pub scientific_minus: bool,
55    pub exponent_digit_count: i32,
56    pub currency: Option<char>,
57}
58
59pub struct DatePart {
60    pub color: Option<i32>,
61    pub use_ampm: bool,
62    pub tokens: Vec<TextToken>,
63}
64
65pub struct ErrorPart {}
66
67pub struct GeneralPart {}
68
69pub enum ParsePart {
70    Number(NumberPart),
71    Date(DatePart),
72    Error(ErrorPart),
73    General(GeneralPart),
74}
75
76pub struct Parser {
77    pub parts: Vec<ParsePart>,
78    lexer: Lexer,
79}
80
81#[derive(PartialEq, Copy, Clone)]
82pub enum NumberState {
83    Integer,
84    Decimal,
85    Exponent,
86}
87
88impl NumberState {
89    pub fn is_integer(&self) -> bool {
90        matches!(self, NumberState::Integer)
91    }
92
93    pub fn is_decimal(&self) -> bool {
94        matches!(self, NumberState::Decimal)
95    }
96
97    pub fn is_exponent(&self) -> bool {
98        matches!(self, NumberState::Exponent)
99    }
100}
101
102impl ParsePart {
103    pub fn is_error(&self) -> bool {
104        match &self {
105            ParsePart::Date(..) => false,
106            ParsePart::Number(..) => false,
107            ParsePart::Error(..) => true,
108            ParsePart::General(..) => false,
109        }
110    }
111    pub fn is_date(&self) -> bool {
112        match &self {
113            ParsePart::Date(..) => true,
114            ParsePart::Number(..) => false,
115            ParsePart::Error(..) => false,
116            ParsePart::General(..) => false,
117        }
118    }
119}
120
121// Numbers:
122// [integer section][decimal point][fractional section][optional exponent]
123// So #,##0.00 is valid but 0.00#,## is not.
124
125impl Parser {
126    pub fn new(format: &str) -> Self {
127        let lexer = Lexer::new(format);
128        let parts = vec![];
129        Parser { parts, lexer }
130    }
131    pub fn parse(&mut self) {
132        while self.lexer.peek_token() != Token::EOF {
133            let part = self.parse_part();
134            self.parts.push(part);
135        }
136    }
137
138    fn parse_part(&mut self) -> ParsePart {
139        let mut token = self.lexer.next_token();
140        let mut digit_count = 0;
141        let mut precision = 0;
142        let mut is_date = false;
143        let mut use_ampm = false;
144        let mut is_number = false;
145        let mut found_decimal_dot = false;
146        let mut use_thousands = false;
147        let mut comma = 0;
148        let mut percent = 0;
149        let mut last_token_is_digit = false;
150        let mut color = None;
151        let mut condition = None;
152        let mut tokens = vec![];
153        let mut is_scientific = false;
154        let mut scientific_minus = false;
155        let mut exponent_digit_count = 0;
156        let mut number = NumberState::Integer;
157        let mut index = 0;
158        let mut currency = None;
159        let mut is_time = false;
160
161        while token != Token::EOF && token != Token::Separator {
162            let next_token = self.lexer.next_token();
163            let token_is_digit = token.is_digit();
164            is_number = is_number || token_is_digit;
165            let next_token_is_digit = next_token.is_digit();
166            if token_is_digit {
167                if is_scientific {
168                    exponent_digit_count += 1;
169                } else if found_decimal_dot {
170                    precision += 1;
171                } else {
172                    digit_count += 1;
173                }
174            }
175            match token {
176                Token::General => {
177                    if tokens.is_empty() {
178                        return ParsePart::General(GeneralPart {});
179                    } else {
180                        return ParsePart::Error(ErrorPart {});
181                    }
182                }
183                Token::Comma => {
184                    // If it is in between digit tokens then we use the thousand separator
185                    if last_token_is_digit && next_token_is_digit {
186                        use_thousands = true;
187                    } else if digit_count > 0 {
188                        comma += 1;
189                    } else {
190                        // Before the number is just a literal.
191                        tokens.push(TextToken::Literal(','));
192                    }
193                }
194                Token::Percent => {
195                    tokens.push(TextToken::Literal('%'));
196                    percent += 1;
197                }
198                Token::Period => {
199                    if is_number && !found_decimal_dot {
200                        tokens.push(TextToken::Period);
201                        found_decimal_dot = true;
202                        if number.is_integer() {
203                            number = NumberState::Decimal;
204                            index = 0;
205                        }
206                    } else {
207                        tokens.push(TextToken::Literal('.'));
208                    }
209                }
210                Token::Color(index) => {
211                    color = Some(index);
212                }
213                Token::Condition(cmp, value) => {
214                    condition = Some((cmp, value));
215                }
216                Token::Currency(c) => {
217                    currency = Some(c);
218                }
219                Token::QuestionMark => {
220                    tokens.push(TextToken::Digit(Digit {
221                        kind: '?',
222                        index,
223                        number,
224                    }));
225                    index += 1;
226                }
227                Token::Sharp => {
228                    tokens.push(TextToken::Digit(Digit {
229                        kind: '#',
230                        index,
231                        number,
232                    }));
233                    index += 1;
234                }
235                Token::Zero => {
236                    tokens.push(TextToken::Digit(Digit {
237                        kind: '0',
238                        index,
239                        number,
240                    }));
241                    index += 1;
242                }
243                Token::Literal(value) => {
244                    if value == ':' {
245                        is_time = true;
246                    }
247                    tokens.push(TextToken::Literal(value));
248                }
249                Token::Text(value) => {
250                    tokens.push(TextToken::Text(value));
251                }
252                Token::Ghost(value) => {
253                    tokens.push(TextToken::Ghost(value));
254                }
255                Token::Spacer(value) => {
256                    tokens.push(TextToken::Spacer(value));
257                }
258                Token::Day => {
259                    is_date = true;
260                    tokens.push(TextToken::Day);
261                }
262                Token::DayPadded => {
263                    is_date = true;
264                    tokens.push(TextToken::DayPadded);
265                }
266                Token::DayNameShort => {
267                    is_date = true;
268                    tokens.push(TextToken::DayNameShort);
269                }
270                Token::DayName => {
271                    is_date = true;
272                    tokens.push(TextToken::DayName);
273                }
274                Token::MonthNameShort => {
275                    is_date = true;
276                    tokens.push(TextToken::MonthNameShort);
277                }
278                Token::MonthName => {
279                    is_date = true;
280                    tokens.push(TextToken::MonthName);
281                }
282                Token::Month => {
283                    if is_time {
284                        // minute
285                        tokens.push(TextToken::Minute);
286                    } else {
287                        is_date = true;
288                        tokens.push(TextToken::Month);
289                    }
290                }
291                Token::MonthPadded => {
292                    if is_time {
293                        // minute padded
294                        tokens.push(TextToken::MinutePadded);
295                    } else {
296                        is_date = true;
297                        tokens.push(TextToken::MonthPadded);
298                    }
299                }
300                Token::MonthLetter => {
301                    is_date = true;
302                    tokens.push(TextToken::MonthLetter);
303                }
304                Token::YearShort => {
305                    is_date = true;
306                    tokens.push(TextToken::YearShort);
307                }
308                Token::Year => {
309                    is_date = true;
310                    tokens.push(TextToken::Year);
311                }
312                Token::Hour => {
313                    is_date = true;
314                    is_time = true;
315                    tokens.push(TextToken::Hour);
316                }
317                Token::HourPadded => {
318                    is_date = true;
319                    is_time = true;
320                    tokens.push(TextToken::HourPadded);
321                }
322                Token::Second => {
323                    is_date = true;
324                    is_time = true;
325                    tokens.push(TextToken::Second);
326                }
327                Token::SecondPadded => {
328                    is_date = true;
329                    is_time = true;
330                    tokens.push(TextToken::SecondPadded);
331                }
332                Token::AMPM => {
333                    is_date = true;
334                    use_ampm = true;
335                    tokens.push(TextToken::AMPM);
336                }
337                Token::Scientific => {
338                    if !is_scientific {
339                        index = 0;
340                        number = NumberState::Exponent;
341                    }
342                    is_scientific = true;
343                }
344                Token::ScientificMinus => {
345                    if !is_scientific {
346                        index = 0;
347                        number = NumberState::Exponent;
348                    }
349                    is_scientific = true;
350                    scientific_minus = true;
351                }
352                Token::Separator => {}
353                Token::Raw => {
354                    tokens.push(TextToken::Raw);
355                }
356                Token::ILLEGAL => {
357                    return ParsePart::Error(ErrorPart {});
358                }
359                Token::EOF => {}
360                Token::ElapsedHour => {
361                    is_date = true;
362                    is_time = true;
363                    tokens.push(TextToken::ElapsedHour);
364                }
365                Token::ElapsedMinute => {
366                    is_date = true;
367                    is_time = true;
368                    tokens.push(TextToken::ElapsedMinute);
369                }
370                Token::ElapsedSecond => {
371                    is_date = true;
372                    is_time = true;
373                    tokens.push(TextToken::ElapsedSecond);
374                }
375                Token::ElapsedHourPadded => {
376                    is_date = true;
377                    is_time = true;
378                    tokens.push(TextToken::ElapsedHourPadded);
379                }
380                Token::ElapsedMinutePadded => {
381                    is_date = true;
382                    is_time = true;
383                    tokens.push(TextToken::ElapsedMinutePadded);
384                }
385                Token::ElapsedSecondPadded => {
386                    is_date = true;
387                    is_time = true;
388                    tokens.push(TextToken::ElapsedSecondPadded);
389                }
390            }
391            last_token_is_digit = token_is_digit;
392            token = next_token;
393        }
394        if is_date {
395            if is_number {
396                return ParsePart::Error(ErrorPart {});
397            }
398            ParsePart::Date(DatePart {
399                color,
400                use_ampm,
401                tokens,
402            })
403        } else {
404            ParsePart::Number(NumberPart {
405                color,
406                condition,
407                use_thousands,
408                percent,
409                comma,
410                tokens,
411                digit_count,
412                precision,
413                is_scientific,
414                scientific_minus,
415                exponent_digit_count,
416                currency,
417            })
418        }
419    }
420}