Skip to main content

lutra_compiler/pr/
literal.rs

1use std::fmt::Write;
2
3use enum_as_inner::EnumAsInner;
4
5#[derive(Debug, EnumAsInner, PartialEq, Clone)]
6pub enum Literal {
7    Number(String),
8    Boolean(bool),
9    Text(String),
10    Date(Date),
11    Time(Time),
12    DateTime(Date, Time),
13}
14
15#[derive(Debug, PartialEq, Clone)]
16pub struct Date {
17    pub year: i32,
18    pub month: u8,
19    pub day: u8,
20}
21
22#[derive(Debug, PartialEq, Clone)]
23pub struct Time {
24    pub hours: i32,
25    pub min: u8,
26    pub sec: u8,
27    pub micros: Option<u32>,
28}
29
30impl Literal {
31    pub fn as_integer(&self) -> Option<u64> {
32        let mut number = self.as_number()?.clone();
33        number.retain(|c| c != '_');
34
35        if let Some(digits) = number.strip_prefix("0x") {
36            return u64::from_str_radix(digits, 16).ok();
37        }
38        if let Some(digits) = number.strip_prefix("0o") {
39            return u64::from_str_radix(digits, 8).ok();
40        }
41        if let Some(digits) = number.strip_prefix("0b") {
42            return u64::from_str_radix(digits, 2).ok();
43        }
44        if let Ok(unsigned) = number.parse::<u64>() {
45            return Some(unsigned);
46        }
47        if let Ok(signed) = number.parse::<i64>() {
48            return Some(signed as u64);
49        }
50        None
51    }
52    pub fn as_float(&self) -> Option<f64> {
53        let mut number = self.as_number()?.clone();
54        number.retain(|c| c != '_');
55        number.parse::<f64>().ok()
56    }
57    pub fn as_decimal(&self) -> Option<i64> {
58        let number = self.as_number()?;
59        if number.contains(|c: char| !(c.is_ascii_digit() || matches!(c, '-' | '_' | '.'))) {
60            return None;
61        }
62
63        // detach minus
64        let (minus, digits) = number
65            .strip_prefix("-")
66            .map(|n| (true, n))
67            .unwrap_or((false, number));
68
69        // determine scale
70        let scale = digits
71            .bytes()
72            .skip_while(|c| *c != b'.')
73            .skip(1)
74            .filter(|c| *c != b'_')
75            .count();
76        if scale > 2 {
77            return None;
78        }
79
80        // parse digits
81        let mut r = digits
82            .bytes()
83            .filter(|c| *c != b'_' && *c != b'.')
84            .try_fold(0_i64, |v, d| {
85                v.checked_mul(10)?.checked_add((d - b'0') as i64)
86            })?;
87
88        // bring to scale 2
89        r = r.checked_mul(10_i64.pow(2 - scale as u32))?;
90
91        // apply minus
92        if minus {
93            r = r.checked_neg()?
94        }
95
96        Some(r)
97    }
98}
99
100impl std::fmt::Display for Literal {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            Literal::Number(i) => write!(f, "{i}"),
104
105            Literal::Text(s) => {
106                write!(f, "{}", quote_string(escape_all_except_quotes(s).as_str()))
107            }
108
109            Literal::Boolean(b) => f.write_str(if *b { "true" } else { "false" }),
110
111            Literal::Date(date) => {
112                f.write_char('@')?;
113                date.fmt(f)
114            }
115            Literal::Time(time) => {
116                f.write_char('@')?;
117                time.fmt(f)
118            }
119            Literal::DateTime(date, time) => {
120                f.write_char('@')?;
121                date.fmt(f)?;
122                f.write_char('T')?;
123                time.fmt(f)
124            }
125        }
126    }
127}
128
129impl Date {
130    pub fn to_epoch_days(&self) -> Option<i32> {
131        chrono::NaiveDate::from_ymd_opt(self.year, self.month as u32, self.day as u32)
132            .map(|d| d.to_epoch_days())
133    }
134}
135
136impl std::fmt::Display for Date {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        let Date { year, month, day } = self;
139        write!(f, "{year:04}-{month:02}-{day:02}")
140    }
141}
142
143impl Time {
144    pub fn to_microseconds(&self) -> i64 {
145        let h = self.hours.abs() as i64;
146        let min = h * 60 + self.min as i64;
147        let sec = min * 60 + self.sec as i64;
148        let mut micros = sec * 1000000 + self.micros.unwrap_or_default() as i64;
149        if self.hours < 0 {
150            micros *= -1;
151        }
152        micros
153    }
154}
155
156impl std::fmt::Display for Time {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        let Time {
159            hours: hour,
160            min,
161            sec,
162            micros: millis,
163        } = self;
164        write!(f, "{hour:02}:{min:02}:{sec:02}")?;
165        if let Some(millis) = millis {
166            write!(f, ".{millis:06}")?;
167        }
168        Ok(())
169    }
170}
171
172fn quote_string(s: &str) -> String {
173    if !s.contains('"') {
174        return format!(r#""{s}""#);
175    }
176
177    // When string contains quotes find the longest sequence of consecutive quotes,
178    // and then use the next highest odd number of quotes
179    // (quotes must be odd; even number of quotes are empty strings).
180    // i.e.:
181    // 0 -> 1
182    // 1 -> 3
183    // 2 -> 3
184    // 3 -> 5
185    let max_consecutive = s
186        .split(|c| c != '"')
187        .map(|quote_sequence| quote_sequence.len())
188        .max()
189        .unwrap_or(0);
190    let next_odd = max_consecutive.div_ceil(2) * 2 + 1;
191    let delim = '"'.to_string().repeat(next_odd);
192
193    format!("{delim}{s}{delim}")
194}
195
196pub fn escape_all_except_quotes(s: &str) -> String {
197    let mut result = String::new();
198    for ch in s.chars() {
199        if ch == '"' || ch == '\'' {
200            result.push(ch);
201        } else {
202            result.extend(ch.escape_default());
203        }
204    }
205    result
206}