Skip to main content

surql_parser/upstream/syn/lexer/strings/
datetime.rs

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