surrealdb_core/syn/lexer/compound/
datetime.rs

1use 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
26/// Lexes a datetime without the surrounding `'` or `"`
27pub 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		// if digits are missing they count as 0's
97		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			// The range checks on the digits ensure that the offset can't exceed 23:59 so below
117			// unwraps won't panic.
118			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			// The range checks on the digits ensure that the offset can't exceed 23:59 so below
125			// unwraps won't panic.
126			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		// this should never panic with a fixed offset.
147		.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}