lutra_compiler/pr/
literal.rs1use 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 let (minus, digits) = number
65 .strip_prefix("-")
66 .map(|n| (true, n))
67 .unwrap_or((false, number));
68
69 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 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 r = r.checked_mul(10_i64.pow(2 - scale as u32))?;
90
91 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 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}