1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
//! This module implements parsing for ISO 8601 grammar.
use crate::{TemporalError, TemporalResult};
use datetime::DateRecord;
use nodes::{IsoDate, IsoDateTime, IsoTime, TimeZone};
use time::TimeSpec;
mod annotations;
pub(crate) mod datetime;
pub(crate) mod duration;
mod grammar;
mod nodes;
mod time;
pub(crate) mod time_zone;
use self::{datetime::DateTimeFlags, grammar::is_annotation_open};
#[cfg(test)]
mod tests;
// TODO: optimize where possible.
/// `assert_syntax!` is a parser specific utility macro for asserting a syntax test, and returning a
/// `SyntaxError` with the provided message if the test fails.
#[macro_export]
macro_rules! assert_syntax {
($cond:expr, $msg:literal) => {
if !$cond {
return Err(TemporalError::syntax().with_message($msg));
}
};
}
/// A utility function for parsing a `DateTime` string
pub(crate) fn parse_date_time(target: &str) -> TemporalResult<IsoParseRecord> {
datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut Cursor::new(target))
}
/// A utility function for parsing an `Instant` string
#[allow(unused)]
pub(crate) fn parse_instant(target: &str) -> TemporalResult<IsoParseRecord> {
datetime::parse_annotated_date_time(
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ,
&mut Cursor::new(target),
)
}
/// A utility function for parsing a `YearMonth` string
pub(crate) fn parse_year_month(target: &str) -> TemporalResult<IsoParseRecord> {
let mut cursor = Cursor::new(target);
let ym = datetime::parse_year_month(&mut cursor);
let Ok(year_month) = ym else {
cursor.pos = 0;
return datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut cursor);
};
let calendar = if cursor.check_or(false, is_annotation_open) {
let set = annotations::parse_annotation_set(false, &mut cursor)?;
set.calendar
} else {
None
};
cursor.close()?;
Ok(IsoParseRecord {
date: DateRecord {
year: year_month.0,
month: year_month.1,
day: 1,
},
time: None,
tz: None,
calendar,
})
}
/// A utilty function for parsing a `MonthDay` String.
pub(crate) fn parse_month_day(target: &str) -> TemporalResult<IsoParseRecord> {
let mut cursor = Cursor::new(target);
let md = datetime::parse_month_day(&mut cursor);
let Ok(month_day) = md else {
cursor.pos = 0;
return datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut cursor);
};
let calendar = if cursor.check_or(false, is_annotation_open) {
let set = annotations::parse_annotation_set(false, &mut cursor)?;
set.calendar
} else {
None
};
cursor.close()?;
Ok(IsoParseRecord {
date: DateRecord {
year: 0,
month: month_day.0,
day: month_day.1,
},
time: None,
tz: None,
calendar,
})
}
/// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions.
///
/// `IsoParseRecord` is converted into the ISO AST Nodes.
#[derive(Default, Debug)]
pub(crate) struct IsoParseRecord {
/// Parsed Date Record
pub(crate) date: DateRecord,
/// Parsed Time
pub(crate) time: Option<TimeSpec>,
/// Parsed `TimeZone` data (UTCOffset | IANA name)
pub(crate) tz: Option<TimeZone>,
/// The parsed calendar value.
pub(crate) calendar: Option<String>,
}
// TODO: Phase out the below and integrate parsing with Temporal components.
/// Parse a [`TemporalTimeZoneString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalTimeZoneString
#[derive(Debug, Clone, Copy)]
pub struct TemporalTimeZoneString;
impl TemporalTimeZoneString {
/// Parses a targeted string as a `TimeZone`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut Cursor) -> TemporalResult<TimeZone> {
time_zone::parse_time_zone(cursor)
}
}
/// Parser for a [`TemporalInstantString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
#[derive(Debug, Clone, Copy)]
pub struct TemporalInstantString;
impl TemporalInstantString {
/// Parses a targeted string as an `Instant`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut Cursor) -> TemporalResult<IsoDateTime> {
let parse_record = datetime::parse_annotated_date_time(
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ,
cursor,
)?;
let date = IsoDate {
year: parse_record.date.year,
month: parse_record.date.month,
day: parse_record.date.day,
calendar: parse_record.calendar,
};
let time = parse_record.time.map_or_else(IsoTime::default, |time| {
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction)
});
Ok(IsoDateTime {
date,
time,
tz: parse_record.tz,
})
}
}
// ==== Mini cursor implementation for Iso8601 targets ====
/// `Cursor` is a small cursor implementation for parsing Iso8601 grammar.
#[derive(Debug)]
pub struct Cursor {
pos: u32,
source: Vec<char>,
}
impl Cursor {
/// Create a new cursor from a source `String` value.
#[must_use]
pub fn new(source: &str) -> Self {
Self {
pos: 0,
source: source.chars().collect(),
}
}
/// Returns a string value from a slice of the cursor.
fn slice(&self, start: u32, end: u32) -> String {
self.source[start as usize..end as usize].iter().collect()
}
/// Get current position
const fn pos(&self) -> u32 {
self.pos
}
/// Peek the value at next position (current + 1).
fn peek(&self) -> Option<char> {
self.peek_n(1)
}
/// Peek the value at n len from current.
fn peek_n(&self, n: u32) -> Option<char> {
let target = (self.pos + n) as usize;
if target < self.source.len() {
Some(self.source[target])
} else {
None
}
}
/// Runs the provided check on the current position.
fn check<F>(&self, f: F) -> Option<bool>
where
F: FnOnce(char) -> bool,
{
self.peek_n(0).map(f)
}
/// Runs the provided check on current position returns the default value if None.
fn check_or<F>(&self, default: bool, f: F) -> bool
where
F: FnOnce(char) -> bool,
{
self.peek_n(0).map_or(default, f)
}
/// Returns `Cursor`'s current char and advances to the next position.
fn next(&mut self) -> Option<char> {
let result = self.peek_n(0);
self.advance();
result
}
/// Returns the next value as a digit.
///
/// # Errors
/// - Returns a SyntaxError if value is not an ascii digit
/// - Returns an AbruptEnd error if cursor ends.
fn next_digit(&mut self) -> TemporalResult<u8> {
let p_digit = self.abrupt_next()?.to_digit(10);
let Some(digit) = p_digit else {
return Err(TemporalError::syntax()
.with_message("Expected decimal digit, found non-digit character."));
};
Ok(digit as u8)
}
/// Utility method that returns next charactor unwrapped char
///
/// # Panics
///
/// This will panic if the next value has not been confirmed to exist.
fn expect_next(&mut self) -> char {
self.next().expect("Invalid use of expect_next.")
}
/// A utility next method that returns an `AbruptEnd` error if invalid.
fn abrupt_next(&mut self) -> TemporalResult<char> {
self.next().ok_or_else(TemporalError::abrupt_end)
}
/// Advances the cursor's position by 1.
fn advance(&mut self) {
self.pos += 1;
}
/// Utility function to advance when a condition is true
fn advance_if(&mut self, condition: bool) {
if condition {
self.advance();
}
}
/// Advances the cursor's position by `n`.
fn advance_n(&mut self, n: u32) {
self.pos += n;
}
/// Closes the current cursor by checking if all contents have been consumed. If not, returns an error for invalid syntax.
fn close(&mut self) -> TemporalResult<()> {
if (self.pos as usize) < self.source.len() {
return Err(TemporalError::syntax()
.with_message("Unexpected syntax at the end of an ISO target."));
}
Ok(())
}
}