surrealdb_core/syn/lexer/compound/
datetime.rs1use std::ops::RangeInclusive;
2
3use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Utc};
4
5use crate::syn::{
6 error::{bail, syntax_error, SyntaxError},
7 lexer::Lexer,
8 token::{t, Token},
9};
10
11pub fn datetime(lexer: &mut Lexer, start: Token) -> Result<DateTime<Utc>, SyntaxError> {
12 let double = match start.kind {
13 t!("d\"") => true,
14 t!("d'") => false,
15 x => panic!("Invalid start token of datetime compound: {x}"),
16 };
17 let datetime = datetime_inner(lexer)?;
18 if double {
19 lexer.expect('"')?;
20 } else {
21 lexer.expect('\'')?;
22 }
23 Ok(datetime)
24}
25
26pub fn datetime_inner(lexer: &mut Lexer) -> Result<DateTime<Utc>, SyntaxError> {
28 let date_start = lexer.reader.offset();
29
30 let year_neg = lexer.eat(b'-');
31 if !year_neg {
32 lexer.eat(b'+');
33 }
34
35 let year = parse_datetime_digits(lexer, 4, 0..=9999)?;
36 lexer.expect('-')?;
37 let month = parse_datetime_digits(lexer, 2, 1..=12)?;
38 lexer.expect('-')?;
39 let day = parse_datetime_digits(lexer, 2, 1..=31)?;
40
41 let year = if year_neg {
42 -(year as i32)
43 } else {
44 year as i32
45 };
46
47 let date = NaiveDate::from_ymd_opt(year, month as u32, day as u32).ok_or_else(
48 || syntax_error!("Invalid DateTime date: date outside of valid range", @lexer.span_since(date_start)),
49 )?;
50
51 if !lexer.eat_when(|x| x == b'T') {
52 let time = NaiveTime::default();
53 let date_time = NaiveDateTime::new(date, time);
54
55 let datetime =
56 Utc.fix().from_local_datetime(&date_time).earliest().unwrap().with_timezone(&Utc);
57
58 return Ok(datetime);
59 }
60
61 let time_start = lexer.reader.offset();
62
63 let hour = parse_datetime_digits(lexer, 2, 0..=24)?;
64 lexer.expect(':')?;
65 let minute = parse_datetime_digits(lexer, 2, 0..=59)?;
66 lexer.expect(':')?;
67 let second = parse_datetime_digits(lexer, 2, 0..=60)?;
68
69 let nanos_start = lexer.reader.offset();
70 let nanos = if lexer.eat(b'.') {
71 let mut number = 0u32;
72 let mut count = 0;
73
74 loop {
75 let Some(d) = lexer.reader.peek() else {
76 break;
77 };
78 if !d.is_ascii_digit() {
79 break;
80 }
81
82 if count == 9 {
83 bail!("Invalid datetime nanoseconds, expected no more then 9 digits", @lexer.span_since(nanos_start))
84 }
85
86 lexer.reader.next();
87 number *= 10;
88 number += (d - b'0') as u32;
89 count += 1;
90 }
91
92 if count == 0 {
93 bail!("Invalid datetime nanoseconds, expected at least a single digit", @lexer.span_since(nanos_start))
94 }
95
96 for _ in count..9 {
98 number *= 10;
99 }
100
101 number
102 } else {
103 0
104 };
105
106 let time = NaiveTime::from_hms_nano_opt(hour as u32, minute as u32, second as u32, nanos)
107 .ok_or_else(
108 || syntax_error!("Invalid DateTime time: time outside of valid range", @lexer.span_since(time_start)),
109 )?;
110
111 let timezone_start = lexer.reader.offset();
112 let timezone = match lexer.reader.peek() {
113 Some(b'-') => {
114 lexer.reader.next();
115 let (hour, minute) = parse_timezone(lexer)?;
116 FixedOffset::west_opt((hour * 3600 + minute * 60) as i32).unwrap()
119 }
120 Some(b'+') => {
121 lexer.reader.next();
122 let (hour, minute) = parse_timezone(lexer)?;
123
124 FixedOffset::east_opt((hour * 3600 + minute * 60) as i32).unwrap()
127 }
128 Some(b'Z') => {
129 lexer.reader.next();
130 Utc.fix()
131 }
132 Some(x) => {
133 let char = lexer.reader.convert_to_char(x)?;
134 bail!("Invalid datetime timezone, expected `Z` or a timezone offset, found {char}",@lexer.span_since(timezone_start));
135 }
136 None => {
137 bail!("Invalid end of file, expected datetime to finish",@lexer.span_since(time_start));
138 }
139 };
140
141 let date_time = NaiveDateTime::new(date, time);
142
143 let datetime = timezone
144 .from_local_datetime(&date_time)
145 .earliest()
146 .unwrap()
148 .with_timezone(&Utc);
149
150 Ok(datetime)
151}
152
153fn parse_timezone(lexer: &mut Lexer) -> Result<(u32, u32), SyntaxError> {
154 let hour = parse_datetime_digits(lexer, 2, 0..=23)? as u32;
155 lexer.expect(':')?;
156 let minute = parse_datetime_digits(lexer, 2, 0..=59)? as u32;
157
158 Ok((hour, minute))
159}
160
161fn parse_datetime_digits(
162 lexer: &mut Lexer,
163 count: usize,
164 range: RangeInclusive<usize>,
165) -> Result<usize, SyntaxError> {
166 let start = lexer.reader.offset();
167
168 let mut value = 0usize;
169
170 for _ in 0..count {
171 let offset = lexer.reader.offset();
172 match lexer.reader.next() {
173 Some(x) if x.is_ascii_digit() => {
174 value *= 10;
175 value += (x - b'0') as usize;
176 }
177 Some(x) => {
178 let char = lexer.reader.convert_to_char(x)?;
179 let span = lexer.span_since(offset);
180 bail!("Invalid datetime, expected digit character found `{char}`", @span);
181 }
182 None => {
183 bail!("Expected end of file, expected datetime digit character", @lexer.current_span());
184 }
185 }
186 }
187
188 if !range.contains(&value) {
189 let span = lexer.span_since(start);
190 bail!("Invalid datetime digit section, section not within allowed range",
191 @span => "This section must be within {}..={}",range.start(),range.end());
192 }
193
194 Ok(value)
195}