use crate::{
civil::{Date, DateTime, Time, TimeSecond, Weekday},
tz::{
Abbreviation, AmbiguousOffset, AmbiguousTimestamp, Offset, OffsetInfo,
TimeZoneId, Transition, REASONABLE_ABBREVIATION_MAX,
},
Timestamp,
};
#[derive(Debug, Eq, PartialEq)]
pub enum TzEnv {
Rule(TimeZone),
Implementation(TimeZoneId),
}
impl TzEnv {
pub fn parse<B: AsRef<[u8]>>(bytes: B) -> Result<TzEnv, ParseError> {
let bytes = bytes.as_ref();
if bytes.get(0) == Some(&b':') {
let Ok(string) = core::str::from_utf8(&bytes[1..]) else {
return Err(ParseErrorKind::TzEnvColonInvalidUtf8.into());
};
let Some(smallstr) = TimeZoneId::new(string) else {
return Err(ParseErrorKind::TzEnvColonTooBig.into());
};
Ok(TzEnv::Implementation(smallstr))
} else {
TimeZone::parse(bytes).map(TzEnv::Rule)
}
}
#[cfg(feature = "std")]
pub fn parse_os_str<O: AsRef<std::ffi::OsStr>>(
osstr: O,
) -> Result<TzEnv, ParseError> {
let bytes = crate::util::os_str_bytes(osstr.as_ref())
.ok_or(ParseError::from(ParseErrorKind::TzEnvInvalidUtf8))?;
TzEnv::parse(bytes)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(align(8))]
pub struct TimeZone {
pub std_abbrev: Abbreviation,
pub std_offset: Offset,
pub dst: Option<Dst>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Dst {
pub abbrev: Abbreviation,
pub offset: Offset,
pub rule: Rule,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Rule {
pub start: DayTime,
pub end: DayTime,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DayTime {
pub date: Day,
pub time: TransitionCivilTime,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Day {
JulianOne(i16),
JulianZero(i16),
WeekdayOfMonth {
month: i8,
week: i8,
weekday: Weekday,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TransitionCivilTime {
pub second: i32,
}
impl TimeZone {
pub fn parse<B: AsRef<[u8]>>(bytes: B) -> Result<TimeZone, ParseError> {
let bytes = bytes.as_ref();
let parser = Parser { ianav3plus: true, ..Parser::new(bytes) };
parser.parse()
}
pub fn parse_prefix<'b, B: AsRef<[u8]> + ?Sized>(
bytes: &'b B,
) -> Result<(TimeZone, usize), ParseError> {
let bytes = bytes.as_ref();
let parser = Parser { ianav3plus: true, ..Parser::new(bytes) };
parser.parse_prefix()
}
}
impl TimeZone {
pub fn to_offset(&self, timestamp: Timestamp) -> Offset {
if self.dst.is_none() {
return self.std_offset;
}
let dt = timestamp.to_datetime(Offset::UTC);
self.dst_info_utc(dt.date().year())
.filter(|dst_info| dst_info.in_dst(dt))
.map(|dst_info| dst_info.offset())
.unwrap_or_else(|| self.std_offset)
}
pub fn to_offset_info(&self, timestamp: Timestamp) -> OffsetInfo {
if self.dst.is_none() {
return OffsetInfo {
offset: self.std_offset,
abbreviation: self.std_abbrev.clone(),
dst: super::Dst::No,
};
}
let dt = timestamp.to_datetime(Offset::UTC);
self.dst_info_utc(dt.date().year())
.filter(|dst_info| dst_info.in_dst(dt))
.map(|dst_info| OffsetInfo {
offset: dst_info.offset(),
abbreviation: dst_info.dst.abbrev.clone(),
dst: super::Dst::Yes,
})
.unwrap_or_else(|| OffsetInfo {
offset: self.std_offset,
abbreviation: self.std_abbrev.clone(),
dst: super::Dst::No,
})
}
pub fn to_ambiguous_timestamp(&self, dt: DateTime) -> AmbiguousTimestamp {
let year = dt.date().year();
let std_offset = self.std_offset;
let Some(dst_info) = self.dst_info_wall(year) else {
return AmbiguousOffset::Unambiguous { offset: std_offset }
.into_ambiguous_timestamp(dt);
};
let dst_offset = dst_info.offset();
let diff = std_offset.until(dst_offset);
let ambiguous_offset = if diff == 0 {
debug_assert_eq!(std_offset, dst_offset);
AmbiguousOffset::Unambiguous { offset: std_offset }
} else if diff.is_negative() {
if dst_info.in_dst(dt) {
AmbiguousOffset::Unambiguous { offset: dst_offset }
} else {
let fold_start = dst_info.start.saturating_add_seconds(diff);
let gap_end =
dst_info.end.saturating_add_seconds(diff.saturating_neg());
if fold_start <= dt && dt < dst_info.start {
AmbiguousOffset::Fold {
before: std_offset,
after: dst_offset,
}
} else if dst_info.end <= dt && dt < gap_end {
AmbiguousOffset::Gap {
before: dst_offset,
after: std_offset,
}
} else {
AmbiguousOffset::Unambiguous { offset: std_offset }
}
}
} else {
if !dst_info.in_dst(dt) {
AmbiguousOffset::Unambiguous { offset: std_offset }
} else {
let gap_end = dst_info.start.saturating_add_seconds(diff);
let fold_start =
dst_info.end.saturating_add_seconds(diff.saturating_neg());
if dst_info.start <= dt && dt < gap_end {
AmbiguousOffset::Gap {
before: std_offset,
after: dst_offset,
}
} else if fold_start <= dt && dt < dst_info.end {
AmbiguousOffset::Fold {
before: dst_offset,
after: std_offset,
}
} else {
AmbiguousOffset::Unambiguous { offset: dst_offset }
}
}
};
ambiguous_offset.into_ambiguous_timestamp(dt)
}
pub fn previous_transition(
&self,
timestamp: Timestamp,
) -> Option<Transition> {
let dt = timestamp.to_datetime(Offset::UTC);
let dst_info = self.dst_info_utc(dt.date().year())?;
let (earlier, later) = dst_info.ordered();
let (prev, dst_info) = if dt > later {
(later, dst_info)
} else if dt > earlier {
(earlier, dst_info)
} else {
let prev_year = if dt.date().year() == Date::MIN.year() {
return None;
} else {
dt.date().year() - 1
};
let dst_info = self.dst_info_utc(prev_year)?;
let (_, later) = dst_info.ordered();
(later, dst_info)
};
let timestamp = prev.to_timestamp(Offset::UTC).ok()?;
let dt = timestamp.to_datetime(Offset::UTC);
let (offset, abbreviation, dst) = if dst_info.in_dst(dt) {
(dst_info.offset(), dst_info.dst.abbrev.clone(), super::Dst::Yes)
} else {
(self.std_offset, self.std_abbrev.clone(), super::Dst::No)
};
let info = OffsetInfo { offset, abbreviation, dst };
Some(Transition { timestamp, info })
}
pub fn next_transition(&self, timestamp: Timestamp) -> Option<Transition> {
let dt = timestamp.to_datetime(Offset::UTC);
let dst_info = self.dst_info_utc(dt.date().year())?;
let (earlier, later) = dst_info.ordered();
let (next, dst_info) = if dt < earlier {
(earlier, dst_info)
} else if dt < later {
(later, dst_info)
} else {
let next_year = if dt.date().year() == Date::MAX.year() {
return None;
} else {
dt.date().year() + 1
};
let dst_info = self.dst_info_utc(next_year)?;
let (earlier, _) = dst_info.ordered();
(earlier, dst_info)
};
let timestamp = next.to_timestamp(Offset::UTC).ok()?;
let dt = timestamp.to_datetime(Offset::UTC);
let (offset, abbreviation, dst) = if dst_info.in_dst(dt) {
(dst_info.offset(), dst_info.dst.abbrev.clone(), super::Dst::Yes)
} else {
(self.std_offset, self.std_abbrev.clone(), super::Dst::No)
};
let info = OffsetInfo { offset, abbreviation, dst };
Some(Transition { timestamp, info })
}
fn dst_info_utc(&self, year: i16) -> Option<DstInfo<'_>> {
let dst = self.dst.as_ref()?;
let start = dst.rule.start.to_datetime(year, self.std_offset);
let mut end = dst.rule.end.to_datetime(year, dst.offset);
if start.date().month() == 1
&& start.date().day() == 1
&& start.time() == Time::MIN
&& year
!= end.saturating_add_seconds(self.std_offset.seconds()).date().year()
{
end = DateTime::from_parts(
Date::new(year, 12, 31)
.expect("12/31 is valid for all valid years"),
Time::MAX,
);
}
Some(DstInfo { dst, start, end })
}
fn dst_info_wall(&self, year: i16) -> Option<DstInfo<'_>> {
let dst = self.dst.as_ref()?;
let start = dst.rule.start.to_datetime(year, Offset::UTC);
let end = dst.rule.end.to_datetime(year, Offset::UTC);
Some(DstInfo { dst, start, end })
}
#[cfg(test)]
fn rule(&self) -> &Rule {
&self.dst.as_ref().unwrap().rule
}
}
impl DayTime {
fn to_datetime(&self, year: i16, offset: Offset) -> DateTime {
let mkmin =
|| DateTime::from_parts(Date::new(year, 1, 1).unwrap(), Time::MIN);
let mkmax = || {
DateTime::from_parts(Date::new(year, 12, 31).unwrap(), Time::MAX)
};
let Some(date) = self.date.to_date(year) else { return mkmax() };
let offset = self.time.second - offset.seconds();
let days = offset.div_euclid(86400);
let second = offset.rem_euclid(86400);
let Ok(date) = date.checked_add(days) else {
return if offset < 0 { mkmin() } else { mkmax() };
};
if date.year() < year {
mkmin()
} else if date.year() > year {
mkmax()
} else {
let time = TimeSecond::new(second).unwrap().to_time();
DateTime::from_parts(date, time)
}
}
}
impl Day {
fn to_date(&self, year: i16) -> Option<Date> {
match *self {
Day::JulianOne(day) => {
Some(
Date::from_day_of_year_no_leap(year, day)
.expect("Julian `J day` should be in bounds"),
)
}
Day::JulianZero(day) => {
Date::from_day_of_year(year, day + 1).ok()
}
Day::WeekdayOfMonth { month, week, weekday } => {
let first = Date::new(year, month, 1)
.expect("all valid year/month combinations support day 1");
let week = if week == 5 { -1 } else { week };
debug_assert!(week == -1 || (1..=4).contains(&week));
Some(
first
.nth_weekday_of_month(week, weekday)
.expect("nth weekday always exists"),
)
}
}
}
}
impl TransitionCivilTime {
pub const DEFAULT: TransitionCivilTime =
TransitionCivilTime { second: 2 * 60 * 60 };
}
#[derive(Debug, Eq, PartialEq)]
struct DstInfo<'a> {
dst: &'a Dst,
start: DateTime,
end: DateTime,
}
impl<'a> DstInfo<'a> {
fn in_dst(&self, utc_dt: DateTime) -> bool {
if self.start <= self.end {
self.start <= utc_dt && utc_dt < self.end
} else {
!(self.end <= utc_dt && utc_dt < self.start)
}
}
fn ordered(&self) -> (DateTime, DateTime) {
if self.start <= self.end {
(self.start, self.end)
} else {
(self.end, self.start)
}
}
fn offset(&self) -> Offset {
self.dst.offset
}
}
#[derive(Debug)]
struct Parser<'s> {
tz: &'s [u8],
pos: core::cell::Cell<usize>,
ianav3plus: bool,
}
impl<'s> Parser<'s> {
fn new<B: ?Sized + AsRef<[u8]>>(tz: &'s B) -> Parser<'s> {
Parser {
tz: tz.as_ref(),
pos: core::cell::Cell::new(0),
ianav3plus: false,
}
}
fn parse(&self) -> Result<TimeZone, ParseError> {
let (time_zone, len) = self.parse_prefix()?;
if !self.tz[len..].is_empty() {
return Err(ParseErrorKind::FoundRemaining.into());
}
Ok(time_zone)
}
fn parse_prefix(&self) -> Result<(TimeZone, usize), ParseError> {
let time_zone = self.parse_posix_time_zone()?;
Ok((time_zone, self.pos()))
}
#[inline(never)] fn parse_posix_time_zone(&self) -> Result<TimeZone, ParseError> {
if self.is_done() {
return Err(ParseErrorKind::Empty.into());
}
let std_abbrev = self
.parse_abbreviation()
.map_err(ParseErrorKind::AbbreviationStd)?;
let std_offset =
self.parse_posix_offset().map_err(ParseErrorKind::OffsetStd)?;
let mut dst = None;
if !self.is_done()
&& (self.byte().is_ascii_alphabetic() || self.byte() == b'<')
{
dst = Some(self.parse_posix_dst(std_offset)?);
}
Ok(TimeZone { std_abbrev, std_offset, dst })
}
fn parse_posix_dst(&self, std_offset: Offset) -> Result<Dst, ParseError> {
let abbrev = self
.parse_abbreviation()
.map_err(ParseErrorKind::AbbreviationDst)?;
if self.is_done() {
return Err(ParseErrorKind::FoundDstNoRule.into());
}
let mut offset = std_offset.checked_add(3600).unwrap();
if self.byte() != b',' {
offset = self
.parse_posix_offset()
.map_err(ParseErrorKind::OffsetDst)?;
if self.is_done() {
return Err(ParseErrorKind::FoundDstNoRuleWithOffset.into());
}
}
if self.byte() != b',' {
return Err(ParseErrorKind::ExpectedCommaAfterDst.into());
}
if !self.bump() {
return Err(ParseErrorKind::FoundEndAfterComma.into());
}
let rule = self.parse_rule().map_err(ParseErrorKind::Rule)?;
Ok(Dst { abbrev, offset, rule })
}
fn parse_abbreviation(&self) -> Result<Abbreviation, AbbreviationError> {
if self.byte() == b'<' {
if !self.bump() {
return Err(AbbreviationError::Quoted(
QuotedAbbreviationError::UnexpectedEndAfterOpening,
));
}
self.parse_quoted_abbreviation().map_err(AbbreviationError::Quoted)
} else {
self.parse_unquoted_abbreviation()
.map_err(AbbreviationError::Unquoted)
}
}
fn parse_unquoted_abbreviation(
&self,
) -> Result<Abbreviation, UnquotedAbbreviationError> {
let start = self.pos();
for _ in 0.. {
if !self.byte().is_ascii_alphabetic() {
break;
}
if self.pos() - start >= REASONABLE_ABBREVIATION_MAX {
return Err(UnquotedAbbreviationError::TooLong);
}
if !self.bump() {
break;
}
}
let end = self.pos();
let abbrev =
core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
UnquotedAbbreviationError::InvalidUtf8
})?;
if abbrev.len() < 3 {
return Err(UnquotedAbbreviationError::TooShort);
}
Abbreviation::new(abbrev).ok_or(UnquotedAbbreviationError::TooLong)
}
fn parse_quoted_abbreviation(
&self,
) -> Result<Abbreviation, QuotedAbbreviationError> {
let start = self.pos();
for _ in 0.. {
if !self.byte().is_ascii_alphanumeric()
&& self.byte() != b'+'
&& self.byte() != b'-'
{
break;
}
if self.pos() - start >= REASONABLE_ABBREVIATION_MAX {
return Err(QuotedAbbreviationError::TooLong);
}
if !self.bump() {
break;
}
}
let end = self.pos();
let abbrev =
core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
QuotedAbbreviationError::InvalidUtf8
})?;
if self.is_done() {
return Err(QuotedAbbreviationError::UnexpectedEnd);
}
if self.byte() != b'>' {
return Err(QuotedAbbreviationError::UnexpectedLastByte);
}
self.bump();
if abbrev.len() < 3 {
return Err(QuotedAbbreviationError::TooShort);
}
Abbreviation::new(abbrev).ok_or(QuotedAbbreviationError::TooLong)
}
fn parse_posix_offset(&self) -> Result<Offset, OffsetError> {
let sign = self.parse_optional_sign()?.unwrap_or(1);
let hour = self.parse_hour_posix()?;
let (mut minute, mut second) = (0, 0);
if self.maybe_byte() == Some(b':') {
if !self.bump() {
return Err(OffsetError::IncompleteMinutes);
}
minute = self.parse_minute()?;
if self.maybe_byte() == Some(b':') {
if !self.bump() {
return Err(OffsetError::IncompleteSeconds);
}
second = self.parse_second()?;
}
}
let mut offset = Offset::from_hours(hour).expect("hours are valid");
offset += i32::from(minute) * 60;
offset += i32::from(second);
if sign.is_positive() {
offset = -offset;
}
assert!(
-89999 <= offset.seconds() && offset.seconds() <= 89999,
"POSIX offset seconds {} is out of range",
offset.seconds(),
);
Ok(offset)
}
fn parse_rule(&self) -> Result<Rule, RuleError> {
let start =
self.parse_posix_datetime().map_err(RuleError::DateTimeStart)?;
if self.maybe_byte() != Some(b',') || !self.bump() {
return Err(RuleError::ExpectedEnd);
}
let end =
self.parse_posix_datetime().map_err(RuleError::DateTimeEnd)?;
Ok(Rule { start, end })
}
fn parse_posix_datetime(&self) -> Result<DayTime, DateTimeError> {
let mut daytime = DayTime {
date: self.parse_posix_date()?,
time: TransitionCivilTime::DEFAULT,
};
if self.maybe_byte() != Some(b'/') {
return Ok(daytime);
}
if !self.bump() {
return Err(DateTimeError::ExpectedTime);
}
daytime.time = self.parse_posix_time()?;
Ok(daytime)
}
fn parse_posix_date(&self) -> Result<Day, DateError> {
match self.byte() {
b'J' => {
if !self.bump() {
return Err(DateError::ExpectedJulianNoLeap);
}
Ok(Day::JulianOne(self.parse_posix_julian_day_no_leap()?))
}
b'0'..=b'9' => {
Ok(Day::JulianZero(self.parse_posix_julian_day_with_leap()?))
}
b'M' => {
if !self.bump() {
return Err(DateError::ExpectedMonthWeekWeekday);
}
let (month, week, weekday) = self.parse_weekday_of_month()?;
Ok(Day::WeekdayOfMonth { month, week, weekday })
}
_ => Err(DateError::UnexpectedByte),
}
}
fn parse_posix_julian_day_no_leap(
&self,
) -> Result<i16, JulianNoLeapError> {
let number = self
.parse_number_with_upto_n_digits(3)
.map_err(JulianNoLeapError::Parse)?;
let number =
i16::try_from(number).map_err(|_| JulianNoLeapError::Range)?;
if !(1 <= number && number <= 365) {
return Err(JulianNoLeapError::Range);
}
Ok(number)
}
fn parse_posix_julian_day_with_leap(
&self,
) -> Result<i16, JulianLeapError> {
let number = self
.parse_number_with_upto_n_digits(3)
.map_err(JulianLeapError::Parse)?;
let number =
i16::try_from(number).map_err(|_| JulianLeapError::Range)?;
if !(0 <= number && number <= 365) {
return Err(JulianLeapError::Range);
}
Ok(number)
}
fn parse_weekday_of_month(
&self,
) -> Result<(i8, i8, Weekday), WeekdayOfMonthError> {
let month = self.parse_month()?;
if self.maybe_byte() != Some(b'.') {
return Err(WeekdayOfMonthError::ExpectedDotAfterMonth);
}
if !self.bump() {
return Err(WeekdayOfMonthError::ExpectedWeekAfterMonth);
}
let week = self.parse_week()?;
if self.maybe_byte() != Some(b'.') {
return Err(WeekdayOfMonthError::ExpectedDotAfterWeek);
}
if !self.bump() {
return Err(WeekdayOfMonthError::ExpectedDayOfWeekAfterWeek);
}
let weekday = self.parse_weekday()?;
Ok((month, week, weekday))
}
fn parse_posix_time(&self) -> Result<TransitionCivilTime, TimeError> {
let (sign, hour) = if self.ianav3plus {
let sign = self.parse_optional_sign()?.unwrap_or(1);
let hour = self.parse_hour_ianav3plus()?;
(sign, hour)
} else {
(1, i16::from(self.parse_hour_posix()?))
};
let (mut minute, mut second) = (0, 0);
if self.maybe_byte() == Some(b':') {
if !self.bump() {
return Err(TimeError::IncompleteMinutes);
}
minute = self.parse_minute()?;
if self.maybe_byte() == Some(b':') {
if !self.bump() {
return Err(TimeError::IncompleteSeconds);
}
second = self.parse_second()?;
}
}
let mut time = TransitionCivilTime { second: i32::from(hour) * 3600 };
time.second += i32::from(minute) * 60;
time.second += i32::from(second);
time.second *= i32::from(sign);
assert!(
-604799 <= time.second && time.second <= 604799,
"POSIX time seconds {} is out of range",
time.second
);
Ok(time)
}
fn parse_month(&self) -> Result<i8, MonthError> {
let number = self
.parse_number_with_upto_n_digits(2)
.map_err(MonthError::Parse)?;
let number = i8::try_from(number).map_err(|_| MonthError::Range)?;
if !(1 <= number && number <= 12) {
return Err(MonthError::Range);
}
Ok(number)
}
fn parse_week(&self) -> Result<i8, WeekOfMonthError> {
let number = self
.parse_number_with_exactly_n_digits(1)
.map_err(WeekOfMonthError::Parse)?;
let number =
i8::try_from(number).map_err(|_| WeekOfMonthError::Range)?;
if !(1 <= number && number <= 5) {
return Err(WeekOfMonthError::Range);
}
Ok(number)
}
fn parse_weekday(&self) -> Result<Weekday, WeekdayError> {
let number = self
.parse_number_with_exactly_n_digits(1)
.map_err(WeekdayError::Parse)?;
let number = i8::try_from(number).map_err(|_| WeekdayError::Range)?;
Weekday::from_sunday_zero_offset(number)
.map_err(|_| WeekdayError::Range)
}
fn parse_hour_ianav3plus(&self) -> Result<i16, HourIanaError> {
assert!(self.ianav3plus);
let number = self
.parse_number_with_upto_n_digits(3)
.map_err(HourIanaError::Parse)?;
let number =
i16::try_from(number).map_err(|_| HourIanaError::Range)?;
if !(0 <= number && number <= 167) {
return Err(HourIanaError::Range);
}
Ok(number)
}
fn parse_hour_posix(&self) -> Result<i8, HourPosixError> {
let number = self
.parse_number_with_upto_n_digits(2)
.map_err(HourPosixError::Parse)?;
let number =
i8::try_from(number).map_err(|_| HourPosixError::Range)?;
if !(0 <= number && number <= 24) {
return Err(HourPosixError::Range);
}
Ok(number)
}
fn parse_minute(&self) -> Result<i8, MinuteError> {
let number = self
.parse_number_with_exactly_n_digits(2)
.map_err(MinuteError::Parse)?;
let number = i8::try_from(number).map_err(|_| MinuteError::Range)?;
if !(0 <= number && number <= 59) {
return Err(MinuteError::Range);
}
Ok(number)
}
fn parse_second(&self) -> Result<i8, SecondError> {
let number = self
.parse_number_with_exactly_n_digits(2)
.map_err(SecondError::Parse)?;
let number = i8::try_from(number).map_err(|_| SecondError::Range)?;
if !(0 <= number && number <= 59) {
return Err(SecondError::Range);
}
Ok(number)
}
fn parse_number_with_exactly_n_digits(
&self,
n: usize,
) -> Result<i32, NumberError> {
assert!(n >= 1, "numbers must have at least 1 digit");
let mut number: i32 = 0;
for _ in 0..n {
if self.is_done() {
return Err(NumberError::ExpectedLength);
}
let byte = self.byte();
let digit = match byte.checked_sub(b'0') {
None => {
return Err(NumberError::InvalidDigit);
}
Some(digit) if digit > 9 => {
return Err(NumberError::InvalidDigit);
}
Some(digit) => {
debug_assert!((0..=9).contains(&digit));
i32::from(digit)
}
};
number = number
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or(NumberError::TooBig)?;
self.bump();
}
Ok(number)
}
fn parse_number_with_upto_n_digits(
&self,
n: usize,
) -> Result<i32, NumberError> {
assert!(n >= 1, "numbers must have at least 1 digit");
let mut number: i32 = 0;
for i in 0..n {
if self.is_done() || !self.byte().is_ascii_digit() {
if i == 0 {
return Err(NumberError::Empty);
}
break;
}
let digit = i32::from(self.byte() - b'0');
number = number
.checked_mul(10)
.and_then(|n| n.checked_add(digit))
.ok_or(NumberError::TooBig)?;
self.bump();
}
Ok(number)
}
fn parse_optional_sign(&self) -> Result<Option<i8>, OptionalSignError> {
if self.is_done() {
return Ok(None);
}
Ok(match self.byte() {
b'-' => {
if !self.bump() {
return Err(OptionalSignError::ExpectedDigitAfterMinus);
}
Some(-1)
}
b'+' => {
if !self.bump() {
return Err(OptionalSignError::ExpectedDigitAfterPlus);
}
Some(1)
}
_ => None,
})
}
}
impl<'s> Parser<'s> {
fn bump(&self) -> bool {
if self.is_done() {
return false;
}
self.pos.set(
self.pos().checked_add(1).expect("pos cannot overflow usize"),
);
!self.is_done()
}
fn is_done(&self) -> bool {
self.pos() == self.tz.len()
}
fn byte(&self) -> u8 {
self.tz[self.pos()]
}
fn maybe_byte(&self) -> Option<u8> {
self.tz.get(self.pos()).copied()
}
fn pos(&self) -> usize {
self.pos.get()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct ParseError {
kind: ParseErrorKind,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum ParseErrorKind {
AbbreviationDst(AbbreviationError),
AbbreviationStd(AbbreviationError),
Empty,
ExpectedCommaAfterDst,
FoundDstNoRule,
FoundDstNoRuleWithOffset,
FoundEndAfterComma,
FoundRemaining,
OffsetDst(OffsetError),
OffsetStd(OffsetError),
Rule(RuleError),
TzEnvColonTooBig,
TzEnvColonInvalidUtf8,
#[allow(dead_code)] TzEnvInvalidUtf8,
}
impl core::fmt::Display for ParseError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::ParseErrorKind::*;
match self.kind {
AbbreviationDst(ref err) => {
f.write_str("failed to parse DST time zone abbreviation: ")?;
core::fmt::Display::fmt(err, f)
}
AbbreviationStd(ref err) => {
f.write_str(
"failed to parse standard time zone abbreviation: ",
)?;
core::fmt::Display::fmt(err, f)
}
Empty => f.write_str(
"an empty string is not a valid POSIX time zone \
transition rule",
),
ExpectedCommaAfterDst => f.write_str(
"expected `,` after parsing DST offset \
in POSIX time zone string",
),
FoundDstNoRule => f.write_str(
"found DST abbreviation in POSIX time zone string, \
but no transition rule \
(this is technically allowed by POSIX, but has \
unspecified behavior)",
),
FoundDstNoRuleWithOffset => f.write_str(
"found DST abbreviation and offset in POSIX time zone string, \
but no transition rule \
(this is technically allowed by POSIX, but has \
unspecified behavior)",
),
FoundEndAfterComma => f.write_str(
"after parsing DST offset in POSIX time zone string, \
found end of string after a trailing `,`",
),
FoundRemaining => f.write_str(
"expected entire POSIX TZ string to be a valid \
time zone transition rule, but found data after \
parsing a valid time zone transition rule",
),
OffsetDst(ref err) => {
f.write_str("failed to parse DST offset: ")?;
core::fmt::Display::fmt(err, f)
}
OffsetStd(ref err) => {
f.write_str("failed to parse standard offset: ")?;
core::fmt::Display::fmt(err, f)
}
Rule(ref err) => core::fmt::Display::fmt(err, f),
TzEnvColonTooBig => {
f.write_str(
"IANA time zone identifier is too big for core-only \
environments \
(must be less than or equal to ",
)?;
core::fmt::Display::fmt(&TimeZoneId::array_capacity_max(), f)?;
f.write_str(" bytes)")
}
TzEnvColonInvalidUtf8 => f.write_str(
"IANA time zone identifier is invalid UTF-8",
),
TzEnvInvalidUtf8 => f.write_str(
"POSIX transition rule or \
IANA time zone identifier is invalid UTF-8",
),
}
}
}
impl From<ParseErrorKind> for ParseError {
fn from(kind: ParseErrorKind) -> ParseError {
ParseError { kind }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum OffsetError {
HourPosix(HourPosixError),
IncompleteMinutes,
IncompleteSeconds,
Minute(MinuteError),
OptionalSign(OptionalSignError),
Second(SecondError),
}
impl core::fmt::Display for OffsetError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::OffsetError::*;
match *self {
HourPosix(ref err) => core::fmt::Display::fmt(err, f),
IncompleteMinutes => f.write_str(
"incomplete time in \
POSIX time zone string (missing minutes)",
),
IncompleteSeconds => f.write_str(
"incomplete time in \
POSIX time zone string (missing seconds)",
),
Minute(ref err) => core::fmt::Display::fmt(err, f),
Second(ref err) => core::fmt::Display::fmt(err, f),
OptionalSign(ref err) => {
f.write_str(
"failed to parse sign for time offset \
POSIX time zone string",
)?;
core::fmt::Display::fmt(err, f)
}
}
}
}
impl From<HourPosixError> for OffsetError {
fn from(err: HourPosixError) -> OffsetError {
OffsetError::HourPosix(err)
}
}
impl From<MinuteError> for OffsetError {
fn from(err: MinuteError) -> OffsetError {
OffsetError::Minute(err)
}
}
impl From<OptionalSignError> for OffsetError {
fn from(err: OptionalSignError) -> OffsetError {
OffsetError::OptionalSign(err)
}
}
impl From<SecondError> for OffsetError {
fn from(err: SecondError) -> OffsetError {
OffsetError::Second(err)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum RuleError {
DateTimeEnd(DateTimeError),
DateTimeStart(DateTimeError),
ExpectedEnd,
}
impl core::fmt::Display for RuleError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::RuleError::*;
match *self {
DateTimeEnd(ref err) => {
f.write_str("failed to parse end of DST transition rule: ")?;
core::fmt::Display::fmt(err, f)
}
DateTimeStart(ref err) => {
f.write_str("failed to parse start of DST transition rule: ")?;
core::fmt::Display::fmt(err, f)
}
ExpectedEnd => f.write_str(
"expected end of DST rule after parsing the start \
of the DST rule",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum DateTimeError {
Date(DateError),
ExpectedTime,
Time(TimeError),
}
impl core::fmt::Display for DateTimeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::DateTimeError::*;
match *self {
Date(ref err) => core::fmt::Display::fmt(err, f),
ExpectedTime => f.write_str(
"expected time specification after `/` following a date
specification in a POSIX time zone DST transition rule",
),
Time(ref err) => core::fmt::Display::fmt(err, f),
}
}
}
impl From<DateError> for DateTimeError {
fn from(err: DateError) -> DateTimeError {
DateTimeError::Date(err)
}
}
impl From<TimeError> for DateTimeError {
fn from(err: TimeError) -> DateTimeError {
DateTimeError::Time(err)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum DateError {
ExpectedJulianNoLeap,
ExpectedMonthWeekWeekday,
JulianLeap(JulianLeapError),
JulianNoLeap(JulianNoLeapError),
UnexpectedByte,
WeekdayOfMonth(WeekdayOfMonthError),
}
impl core::fmt::Display for DateError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::DateError::*;
match *self {
ExpectedJulianNoLeap => f.write_str(
"expected one-based Julian day after `J` in date \
specification of a POSIX time zone DST \
transition rule, but found the end of input",
),
ExpectedMonthWeekWeekday => f.write_str(
"expected month-week-weekday after `M` in date \
specification of a POSIX time zone DST \
transition rule, but found the end of input",
),
JulianLeap(ref err) => core::fmt::Display::fmt(err, f),
JulianNoLeap(ref err) => core::fmt::Display::fmt(err, f),
UnexpectedByte => f.write_str(
"expected `J`, a digit or `M` at the beginning of a date \
specification of a POSIX time zone DST transition rule",
),
WeekdayOfMonth(ref err) => core::fmt::Display::fmt(err, f),
}
}
}
impl From<JulianLeapError> for DateError {
fn from(err: JulianLeapError) -> DateError {
DateError::JulianLeap(err)
}
}
impl From<JulianNoLeapError> for DateError {
fn from(err: JulianNoLeapError) -> DateError {
DateError::JulianNoLeap(err)
}
}
impl From<WeekdayOfMonthError> for DateError {
fn from(err: WeekdayOfMonthError) -> DateError {
DateError::WeekdayOfMonth(err)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum JulianNoLeapError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for JulianNoLeapError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::JulianNoLeapError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid one-based Julian day digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed one-based Julian day, but it's not in supported \
range of `1..=365`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum JulianLeapError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for JulianLeapError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::JulianLeapError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid zero-based Julian day digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed zero-based Julian day, but it's not in supported \
range of `0..=365`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum AbbreviationError {
Quoted(QuotedAbbreviationError),
Unquoted(UnquotedAbbreviationError),
}
impl core::fmt::Display for AbbreviationError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::AbbreviationError::*;
match *self {
Quoted(ref err) => core::fmt::Display::fmt(err, f),
Unquoted(ref err) => core::fmt::Display::fmt(err, f),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum UnquotedAbbreviationError {
InvalidUtf8,
TooLong,
TooShort,
}
impl core::fmt::Display for UnquotedAbbreviationError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::UnquotedAbbreviationError::*;
match *self {
InvalidUtf8 => f.write_str(
"unquoted time zone abbreviation must be valid UTF-8",
),
TooLong => write!(
f,
"expected unquoted time zone abbreviation with at most \
{} bytes, but found an abbreviation that is longer",
REASONABLE_ABBREVIATION_MAX,
),
TooShort => f.write_str(
"expected unquoted time zone abbreviation to have length of \
3 or more bytes, but an abbreviation that is shorter",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum QuotedAbbreviationError {
InvalidUtf8,
TooLong,
TooShort,
UnexpectedEnd,
UnexpectedEndAfterOpening,
UnexpectedLastByte,
}
impl core::fmt::Display for QuotedAbbreviationError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::QuotedAbbreviationError::*;
match *self {
InvalidUtf8 => f.write_str(
"quoted time zone abbreviation must be valid UTF-8",
),
TooLong => write!(
f,
"expected quoted time zone abbreviation with at most \
{} bytes, but found an abbreviation that is longer",
REASONABLE_ABBREVIATION_MAX,
),
TooShort => f.write_str(
"expected quoted time zone abbreviation to have length of \
3 or more bytes, but an abbreviation that is shorter",
),
UnexpectedEnd => f.write_str(
"found non-empty quoted time zone abbreviation, but \
found end of input before an end-of-quoted abbreviation \
`>` character",
),
UnexpectedEndAfterOpening => f.write_str(
"found opening `<` quote for time zone abbreviation in \
POSIX time zone transition rule, and expected a name \
following it, but found the end of input instead",
),
UnexpectedLastByte => f.write_str(
"found non-empty quoted time zone abbreviation, but \
found did not find end-of-quoted abbreviation `>` \
character",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum WeekdayOfMonthError {
ExpectedDayOfWeekAfterWeek,
ExpectedDotAfterMonth,
ExpectedDotAfterWeek,
ExpectedWeekAfterMonth,
Month(MonthError),
WeekOfMonth(WeekOfMonthError),
Weekday(WeekdayError),
}
impl core::fmt::Display for WeekdayOfMonthError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::WeekdayOfMonthError::*;
match *self {
ExpectedDayOfWeekAfterWeek => f.write_str(
"expected day-of-week after week in POSIX time zone rule",
),
ExpectedDotAfterMonth => {
f.write_str("expected `.` after month in POSIX time zone rule")
}
ExpectedWeekAfterMonth => f.write_str(
"expected week after month in POSIX time zone rule",
),
ExpectedDotAfterWeek => {
f.write_str("expected `.` after week in POSIX time zone rule")
}
Month(ref err) => core::fmt::Display::fmt(err, f),
WeekOfMonth(ref err) => core::fmt::Display::fmt(err, f),
Weekday(ref err) => core::fmt::Display::fmt(err, f),
}
}
}
impl From<MonthError> for WeekdayOfMonthError {
fn from(err: MonthError) -> WeekdayOfMonthError {
WeekdayOfMonthError::Month(err)
}
}
impl From<WeekOfMonthError> for WeekdayOfMonthError {
fn from(err: WeekOfMonthError) -> WeekdayOfMonthError {
WeekdayOfMonthError::WeekOfMonth(err)
}
}
impl From<WeekdayError> for WeekdayOfMonthError {
fn from(err: WeekdayError) -> WeekdayOfMonthError {
WeekdayOfMonthError::Weekday(err)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum TimeError {
HourIana(HourIanaError),
HourPosix(HourPosixError),
IncompleteMinutes,
IncompleteSeconds,
Minute(MinuteError),
OptionalSign(OptionalSignError),
Second(SecondError),
}
impl core::fmt::Display for TimeError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::TimeError::*;
match *self {
HourIana(ref err) => core::fmt::Display::fmt(err, f),
HourPosix(ref err) => core::fmt::Display::fmt(err, f),
IncompleteMinutes => f.write_str(
"incomplete time zone transition time in \
POSIX time zone string (missing minutes)",
),
IncompleteSeconds => f.write_str(
"incomplete time zone transition time in \
POSIX time zone string (missing seconds)",
),
Minute(ref err) => core::fmt::Display::fmt(err, f),
Second(ref err) => core::fmt::Display::fmt(err, f),
OptionalSign(ref err) => {
f.write_str(
"failed to parse sign for time zone transition time",
)?;
core::fmt::Display::fmt(err, f)
}
}
}
}
impl From<HourIanaError> for TimeError {
fn from(err: HourIanaError) -> TimeError {
TimeError::HourIana(err)
}
}
impl From<HourPosixError> for TimeError {
fn from(err: HourPosixError) -> TimeError {
TimeError::HourPosix(err)
}
}
impl From<MinuteError> for TimeError {
fn from(err: MinuteError) -> TimeError {
TimeError::Minute(err)
}
}
impl From<OptionalSignError> for TimeError {
fn from(err: OptionalSignError) -> TimeError {
TimeError::OptionalSign(err)
}
}
impl From<SecondError> for TimeError {
fn from(err: SecondError) -> TimeError {
TimeError::Second(err)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum MonthError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for MonthError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::MonthError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid month digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed month, but it's not in supported \
range of `1..=12`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum WeekOfMonthError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for WeekOfMonthError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::WeekOfMonthError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid week-of-month digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed week-of-month, but it's not in supported \
range of `1..=5`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum WeekdayError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for WeekdayError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::WeekdayError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid weekday digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed weekday, but it's not in supported \
range of `0..=6` (with `0` corresponding to Sunday)",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum HourIanaError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for HourIanaError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::HourIanaError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid hour digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed hours, but it's not in supported \
range of `-167..=167`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum HourPosixError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for HourPosixError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::HourPosixError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid hour digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed hours, but it's not in supported \
range of `0..=24`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum MinuteError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for MinuteError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::MinuteError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid minute digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed minutes, but it's not in supported \
range of `0..=59`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum SecondError {
Parse(NumberError),
Range,
}
impl core::fmt::Display for SecondError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::SecondError::*;
match *self {
Parse(ref err) => {
f.write_str("invalid second digits: ")?;
core::fmt::Display::fmt(err, f)
}
Range => f.write_str(
"parsed seconds, but it's not in supported \
range of `0..=59`",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum NumberError {
Empty,
ExpectedLength,
InvalidDigit,
TooBig,
}
impl core::fmt::Display for NumberError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::NumberError::*;
match *self {
Empty => f.write_str("invalid number, no digits found"),
ExpectedLength => f.write_str(
"expected a fixed number of digits, \
but found incorrect number",
),
InvalidDigit => f.write_str("expected digit in range `0..=9`"),
TooBig => f.write_str(
"parsed number too big to fit into a 32-bit signed integer",
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum OptionalSignError {
ExpectedDigitAfterMinus,
ExpectedDigitAfterPlus,
}
impl core::fmt::Display for OptionalSignError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::OptionalSignError::*;
match *self {
ExpectedDigitAfterMinus => f.write_str(
"expected digit after `-` sign, \
but got end of input",
),
ExpectedDigitAfterPlus => f.write_str(
"expected digit after `+` sign, \
but got end of input",
),
}
}
}
#[cfg(test)]
mod tests {
use crate::civil::date;
use super::*;
fn posix_time_zone(input: impl AsRef<[u8]>) -> TimeZone {
let input = input.as_ref();
let tz = TimeZone::parse(input).unwrap();
tz
}
fn parser(s: &str) -> Parser<'_> {
Parser::new(s)
}
fn astr(s: &'static str) -> Abbreviation {
Abbreviation::array(s)
}
fn off(seconds: i32) -> Offset {
Offset::from_seconds(seconds).unwrap()
}
#[test]
fn parse() {
let p = parser("NZST-12NZDT,J60,J300");
assert_eq!(
p.parse().unwrap(),
TimeZone {
std_abbrev: astr("NZST"),
std_offset: off(12 * 60 * 60),
dst: Some(Dst {
abbrev: astr("NZDT"),
offset: off(13 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(60),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::JulianOne(300),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
}),
},
);
let p = Parser::new("NZST-12NZDT,J60,J300WAT");
assert!(p.parse().is_err());
}
#[test]
fn parse_posix_time_zone() {
let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3");
assert_eq!(
p.parse_posix_time_zone().unwrap(),
TimeZone {
std_abbrev: astr("NZST"),
std_offset: off(12 * 60 * 60),
dst: Some(Dst {
abbrev: astr("NZDT"),
offset: off(13 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::WeekdayOfMonth {
month: 9,
week: 5,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::WeekdayOfMonth {
month: 4,
week: 1,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 3 * 60 * 60 },
},
},
}),
},
);
let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3WAT");
assert_eq!(
p.parse_posix_time_zone().unwrap(),
TimeZone {
std_abbrev: astr("NZST"),
std_offset: off(12 * 60 * 60),
dst: Some(Dst {
abbrev: astr("NZDT"),
offset: off(13 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::WeekdayOfMonth {
month: 9,
week: 5,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::WeekdayOfMonth {
month: 4,
week: 1,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 3 * 60 * 60 },
},
},
}),
},
);
let p = Parser::new("NZST-12NZDT,J60,J300");
assert_eq!(
p.parse_posix_time_zone().unwrap(),
TimeZone {
std_abbrev: astr("NZST"),
std_offset: off(12 * 60 * 60),
dst: Some(Dst {
abbrev: astr("NZDT"),
offset: off(13 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(60),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::JulianOne(300),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
}),
},
);
let p = Parser::new("NZST-12NZDT,J60,J300WAT");
assert_eq!(
p.parse_posix_time_zone().unwrap(),
TimeZone {
std_abbrev: astr("NZST"),
std_offset: off(12 * 60 * 60),
dst: Some(Dst {
abbrev: astr("NZDT"),
offset: off(13 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(60),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::JulianOne(300),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
}),
},
);
}
#[test]
fn parse_posix_dst() {
let p = Parser::new("NZDT,M9.5.0,M4.1.0/3");
assert_eq!(
p.parse_posix_dst(off(12 * 60 * 60)).unwrap(),
Dst {
abbrev: astr("NZDT"),
offset: off(13 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::WeekdayOfMonth {
month: 9,
week: 5,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::WeekdayOfMonth {
month: 4,
week: 1,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 3 * 60 * 60 },
},
},
},
);
let p = Parser::new("NZDT,J60,J300");
assert_eq!(
p.parse_posix_dst(off(12 * 60 * 60)).unwrap(),
Dst {
abbrev: astr("NZDT"),
offset: off(13 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(60),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::JulianOne(300),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
},
);
let p = Parser::new("NZDT-7,J60,J300");
assert_eq!(
p.parse_posix_dst(off(12 * 60 * 60)).unwrap(),
Dst {
abbrev: astr("NZDT"),
offset: off(7 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(60),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::JulianOne(300),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
},
);
let p = Parser::new("NZDT+7,J60,J300");
assert_eq!(
p.parse_posix_dst(off(12 * 60 * 60)).unwrap(),
Dst {
abbrev: astr("NZDT"),
offset: off(-7 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(60),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::JulianOne(300),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
},
);
let p = Parser::new("NZDT7,J60,J300");
assert_eq!(
p.parse_posix_dst(off(12 * 60 * 60)).unwrap(),
Dst {
abbrev: astr("NZDT"),
offset: off(-7 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(60),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::JulianOne(300),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
},
);
let p = Parser::new("NZDT7,");
assert!(p.parse_posix_dst(off(12 * 60 * 60)).is_err());
let p = Parser::new("NZDT7!");
assert!(p.parse_posix_dst(off(12 * 60 * 60)).is_err());
}
#[test]
fn parse_abbreviation() {
let p = Parser::new("ABC");
assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
let p = Parser::new("<ABC>");
assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
let p = Parser::new("<+09>");
assert_eq!(p.parse_abbreviation().unwrap(), "+09");
let p = Parser::new("+09");
assert!(p.parse_abbreviation().is_err());
}
#[test]
fn parse_unquoted_abbreviation() {
let p = Parser::new("ABC");
assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
let p = Parser::new("ABCXYZ");
assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABCXYZ");
let p = Parser::new("ABC123");
assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
let tz = "a".repeat(6);
let p = Parser::new(&tz);
assert_eq!(p.parse_unquoted_abbreviation().unwrap(), &*tz);
let p = Parser::new("a");
assert!(p.parse_unquoted_abbreviation().is_err());
let p = Parser::new("ab");
assert!(p.parse_unquoted_abbreviation().is_err());
let p = Parser::new("ab1");
assert!(p.parse_unquoted_abbreviation().is_err());
let tz = "a".repeat(31);
#[cfg(feature = "alloc")]
{
let p = Parser::new(&tz);
assert_eq!(p.parse_unquoted_abbreviation().unwrap(), &*tz);
}
#[cfg(not(feature = "alloc"))]
{
let p = Parser::new(&tz);
assert!(p.parse_unquoted_abbreviation().is_err());
}
let p = Parser::new(b"ab\xFFcd");
assert!(p.parse_unquoted_abbreviation().is_err());
}
#[test]
fn parse_quoted_abbreviation() {
let p = Parser::new("ABC>");
assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
let p = Parser::new("ABCXYZ>");
assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABCXYZ");
let p = Parser::new("ABC>123");
assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
let p = Parser::new("ABC123>");
assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC123");
let p = Parser::new("ab1>");
assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ab1");
let p = Parser::new("+09>");
assert_eq!(p.parse_quoted_abbreviation().unwrap(), "+09");
let p = Parser::new("-09>");
assert_eq!(p.parse_quoted_abbreviation().unwrap(), "-09");
let tz = alloc::format!("{}>", "a".repeat(6));
let p = Parser::new(&tz);
assert_eq!(
p.parse_quoted_abbreviation().unwrap(),
tz.trim_end_matches(">")
);
let p = Parser::new("a>");
assert!(p.parse_quoted_abbreviation().is_err());
let p = Parser::new("ab>");
assert!(p.parse_quoted_abbreviation().is_err());
let tz = alloc::format!("{}>", "a".repeat(31));
#[cfg(feature = "alloc")]
{
let p = Parser::new(&tz);
assert_eq!(
p.parse_quoted_abbreviation().unwrap(),
tz.trim_end_matches(">")
);
}
#[cfg(not(feature = "alloc"))]
{
let p = Parser::new(&tz);
assert!(p.parse_quoted_abbreviation().is_err());
}
let p = Parser::new(b"ab\xFFcd>");
assert!(p.parse_quoted_abbreviation().is_err());
let p = Parser::new("ABC");
assert!(p.parse_quoted_abbreviation().is_err());
let p = Parser::new("ABC!>");
assert!(p.parse_quoted_abbreviation().is_err());
}
#[test]
fn parse_posix_offset() {
let p = Parser::new("5");
assert_eq!(p.parse_posix_offset().unwrap().seconds(), -5 * 60 * 60);
let p = Parser::new("+5");
assert_eq!(p.parse_posix_offset().unwrap().seconds(), -5 * 60 * 60);
let p = Parser::new("-5");
assert_eq!(p.parse_posix_offset().unwrap().seconds(), 5 * 60 * 60);
let p = Parser::new("-12:34:56");
assert_eq!(
p.parse_posix_offset().unwrap().seconds(),
12 * 60 * 60 + 34 * 60 + 56,
);
let p = Parser::new("a");
assert!(p.parse_posix_offset().is_err());
let p = Parser::new("-");
assert!(p.parse_posix_offset().is_err());
let p = Parser::new("+");
assert!(p.parse_posix_offset().is_err());
let p = Parser::new("-a");
assert!(p.parse_posix_offset().is_err());
let p = Parser::new("+a");
assert!(p.parse_posix_offset().is_err());
let p = Parser::new("-25");
assert!(p.parse_posix_offset().is_err());
let p = Parser::new("+25");
assert!(p.parse_posix_offset().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("25") };
assert!(p.parse_posix_offset().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("+25") };
assert!(p.parse_posix_offset().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("-25") };
assert!(p.parse_posix_offset().is_err());
}
#[test]
fn parse_rule() {
let p = Parser::new("M9.5.0,M4.1.0/3");
assert_eq!(
p.parse_rule().unwrap(),
Rule {
start: DayTime {
date: Day::WeekdayOfMonth {
month: 9,
week: 5,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
end: DayTime {
date: Day::WeekdayOfMonth {
month: 4,
week: 1,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 3 * 60 * 60 },
},
},
);
let p = Parser::new("M9.5.0");
assert!(p.parse_rule().is_err());
let p = Parser::new(",M9.5.0,M4.1.0/3");
assert!(p.parse_rule().is_err());
let p = Parser::new("M9.5.0/");
assert!(p.parse_rule().is_err());
let p = Parser::new("M9.5.0,M4.1.0/");
assert!(p.parse_rule().is_err());
}
#[test]
fn parse_posix_datetime() {
let p = Parser::new("J1");
assert_eq!(
p.parse_posix_datetime().unwrap(),
DayTime {
date: Day::JulianOne(1),
time: TransitionCivilTime { second: 2 * 60 * 60 }
},
);
let p = Parser::new("J1/3");
assert_eq!(
p.parse_posix_datetime().unwrap(),
DayTime {
date: Day::JulianOne(1),
time: TransitionCivilTime { second: 3 * 60 * 60 }
},
);
let p = Parser::new("M4.1.0/3");
assert_eq!(
p.parse_posix_datetime().unwrap(),
DayTime {
date: Day::WeekdayOfMonth {
month: 4,
week: 1,
weekday: Weekday::Sunday
},
time: TransitionCivilTime { second: 3 * 60 * 60 },
},
);
let p = Parser::new("1/3:45:05");
assert_eq!(
p.parse_posix_datetime().unwrap(),
DayTime {
date: Day::JulianZero(1),
time: TransitionCivilTime {
second: 3 * 60 * 60 + 45 * 60 + 5
},
},
);
let p = Parser::new("a");
assert!(p.parse_posix_datetime().is_err());
let p = Parser::new("J1/");
assert!(p.parse_posix_datetime().is_err());
let p = Parser::new("1/");
assert!(p.parse_posix_datetime().is_err());
let p = Parser::new("M4.1.0/");
assert!(p.parse_posix_datetime().is_err());
}
#[test]
fn parse_posix_date() {
let p = Parser::new("J1");
assert_eq!(p.parse_posix_date().unwrap(), Day::JulianOne(1));
let p = Parser::new("J365");
assert_eq!(p.parse_posix_date().unwrap(), Day::JulianOne(365));
let p = Parser::new("0");
assert_eq!(p.parse_posix_date().unwrap(), Day::JulianZero(0));
let p = Parser::new("1");
assert_eq!(p.parse_posix_date().unwrap(), Day::JulianZero(1));
let p = Parser::new("365");
assert_eq!(p.parse_posix_date().unwrap(), Day::JulianZero(365));
let p = Parser::new("M9.5.0");
assert_eq!(
p.parse_posix_date().unwrap(),
Day::WeekdayOfMonth {
month: 9,
week: 5,
weekday: Weekday::Sunday
},
);
let p = Parser::new("M9.5.6");
assert_eq!(
p.parse_posix_date().unwrap(),
Day::WeekdayOfMonth {
month: 9,
week: 5,
weekday: Weekday::Saturday
},
);
let p = Parser::new("M09.5.6");
assert_eq!(
p.parse_posix_date().unwrap(),
Day::WeekdayOfMonth {
month: 9,
week: 5,
weekday: Weekday::Saturday
},
);
let p = Parser::new("M12.1.1");
assert_eq!(
p.parse_posix_date().unwrap(),
Day::WeekdayOfMonth {
month: 12,
week: 1,
weekday: Weekday::Monday
},
);
let p = Parser::new("a");
assert!(p.parse_posix_date().is_err());
let p = Parser::new("j");
assert!(p.parse_posix_date().is_err());
let p = Parser::new("m");
assert!(p.parse_posix_date().is_err());
let p = Parser::new("n");
assert!(p.parse_posix_date().is_err());
let p = Parser::new("J366");
assert!(p.parse_posix_date().is_err());
let p = Parser::new("366");
assert!(p.parse_posix_date().is_err());
}
#[test]
fn parse_posix_julian_day_no_leap() {
let p = Parser::new("1");
assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
let p = Parser::new("001");
assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
let p = Parser::new("365");
assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
let p = Parser::new("3655");
assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
let p = Parser::new("0");
assert!(p.parse_posix_julian_day_no_leap().is_err());
let p = Parser::new("366");
assert!(p.parse_posix_julian_day_no_leap().is_err());
}
#[test]
fn parse_posix_julian_day_with_leap() {
let p = Parser::new("0");
assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 0);
let p = Parser::new("1");
assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
let p = Parser::new("001");
assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
let p = Parser::new("365");
assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
let p = Parser::new("3655");
assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
let p = Parser::new("366");
assert!(p.parse_posix_julian_day_with_leap().is_err());
}
#[test]
fn parse_weekday_of_month() {
let p = Parser::new("9.5.0");
assert_eq!(
p.parse_weekday_of_month().unwrap(),
(9, 5, Weekday::Sunday)
);
let p = Parser::new("9.1.6");
assert_eq!(
p.parse_weekday_of_month().unwrap(),
(9, 1, Weekday::Saturday)
);
let p = Parser::new("09.1.6");
assert_eq!(
p.parse_weekday_of_month().unwrap(),
(9, 1, Weekday::Saturday)
);
let p = Parser::new("9");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("9.");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("9.5");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("9.5.");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("0.5.0");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("13.5.0");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("9.0.0");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("9.6.0");
assert!(p.parse_weekday_of_month().is_err());
let p = Parser::new("9.5.7");
assert!(p.parse_weekday_of_month().is_err());
}
#[test]
fn parse_posix_time() {
let p = Parser::new("5");
assert_eq!(p.parse_posix_time().unwrap().second, 5 * 60 * 60);
let p = Parser::new("22");
assert_eq!(p.parse_posix_time().unwrap().second, 22 * 60 * 60);
let p = Parser::new("02");
assert_eq!(p.parse_posix_time().unwrap().second, 2 * 60 * 60);
let p = Parser::new("5:45");
assert_eq!(
p.parse_posix_time().unwrap().second,
5 * 60 * 60 + 45 * 60
);
let p = Parser::new("5:45:12");
assert_eq!(
p.parse_posix_time().unwrap().second,
5 * 60 * 60 + 45 * 60 + 12
);
let p = Parser::new("5:45:129");
assert_eq!(
p.parse_posix_time().unwrap().second,
5 * 60 * 60 + 45 * 60 + 12
);
let p = Parser::new("5:45:12:");
assert_eq!(
p.parse_posix_time().unwrap().second,
5 * 60 * 60 + 45 * 60 + 12
);
let p = Parser { ianav3plus: true, ..Parser::new("+5:45:12") };
assert_eq!(
p.parse_posix_time().unwrap().second,
5 * 60 * 60 + 45 * 60 + 12
);
let p = Parser { ianav3plus: true, ..Parser::new("-5:45:12") };
assert_eq!(
p.parse_posix_time().unwrap().second,
-(5 * 60 * 60 + 45 * 60 + 12)
);
let p = Parser { ianav3plus: true, ..Parser::new("-167:45:12") };
assert_eq!(
p.parse_posix_time().unwrap().second,
-(167 * 60 * 60 + 45 * 60 + 12),
);
let p = Parser::new("25");
assert!(p.parse_posix_time().is_err());
let p = Parser::new("12:2");
assert!(p.parse_posix_time().is_err());
let p = Parser::new("12:");
assert!(p.parse_posix_time().is_err());
let p = Parser::new("12:23:5");
assert!(p.parse_posix_time().is_err());
let p = Parser::new("12:23:");
assert!(p.parse_posix_time().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("168") };
assert!(p.parse_posix_time().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("-168") };
assert!(p.parse_posix_time().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("+168") };
assert!(p.parse_posix_time().is_err());
}
#[test]
fn parse_month() {
let p = Parser::new("1");
assert_eq!(p.parse_month().unwrap(), 1);
let p = Parser::new("01");
assert_eq!(p.parse_month().unwrap(), 1);
let p = Parser::new("12");
assert_eq!(p.parse_month().unwrap(), 12);
let p = Parser::new("0");
assert!(p.parse_month().is_err());
let p = Parser::new("00");
assert!(p.parse_month().is_err());
let p = Parser::new("001");
assert!(p.parse_month().is_err());
let p = Parser::new("13");
assert!(p.parse_month().is_err());
}
#[test]
fn parse_week() {
let p = Parser::new("1");
assert_eq!(p.parse_week().unwrap(), 1);
let p = Parser::new("5");
assert_eq!(p.parse_week().unwrap(), 5);
let p = Parser::new("55");
assert_eq!(p.parse_week().unwrap(), 5);
let p = Parser::new("0");
assert!(p.parse_week().is_err());
let p = Parser::new("6");
assert!(p.parse_week().is_err());
let p = Parser::new("00");
assert!(p.parse_week().is_err());
let p = Parser::new("01");
assert!(p.parse_week().is_err());
let p = Parser::new("05");
assert!(p.parse_week().is_err());
}
#[test]
fn parse_weekday() {
let p = Parser::new("0");
assert_eq!(p.parse_weekday().unwrap(), Weekday::Sunday);
let p = Parser::new("1");
assert_eq!(p.parse_weekday().unwrap(), Weekday::Monday);
let p = Parser::new("6");
assert_eq!(p.parse_weekday().unwrap(), Weekday::Saturday);
let p = Parser::new("00");
assert_eq!(p.parse_weekday().unwrap(), Weekday::Sunday);
let p = Parser::new("06");
assert_eq!(p.parse_weekday().unwrap(), Weekday::Sunday);
let p = Parser::new("60");
assert_eq!(p.parse_weekday().unwrap(), Weekday::Saturday);
let p = Parser::new("7");
assert!(p.parse_weekday().is_err());
}
#[test]
fn parse_hour_posix() {
let p = Parser::new("5");
assert_eq!(p.parse_hour_posix().unwrap(), 5);
let p = Parser::new("0");
assert_eq!(p.parse_hour_posix().unwrap(), 0);
let p = Parser::new("00");
assert_eq!(p.parse_hour_posix().unwrap(), 0);
let p = Parser::new("24");
assert_eq!(p.parse_hour_posix().unwrap(), 24);
let p = Parser::new("100");
assert_eq!(p.parse_hour_posix().unwrap(), 10);
let p = Parser::new("25");
assert!(p.parse_hour_posix().is_err());
let p = Parser::new("99");
assert!(p.parse_hour_posix().is_err());
}
#[test]
fn parse_hour_ianav3plus() {
let new = |input| Parser { ianav3plus: true, ..Parser::new(input) };
let p = new("5");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 5);
let p = new("0");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
let p = new("00");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
let p = new("000");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
let p = new("24");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 24);
let p = new("100");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
let p = new("1000");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
let p = new("167");
assert_eq!(p.parse_hour_ianav3plus().unwrap(), 167);
let p = new("168");
assert!(p.parse_hour_ianav3plus().is_err());
let p = new("999");
assert!(p.parse_hour_ianav3plus().is_err());
}
#[test]
fn parse_minute() {
let p = Parser::new("00");
assert_eq!(p.parse_minute().unwrap(), 0);
let p = Parser::new("24");
assert_eq!(p.parse_minute().unwrap(), 24);
let p = Parser::new("59");
assert_eq!(p.parse_minute().unwrap(), 59);
let p = Parser::new("599");
assert_eq!(p.parse_minute().unwrap(), 59);
let p = Parser::new("0");
assert!(p.parse_minute().is_err());
let p = Parser::new("1");
assert!(p.parse_minute().is_err());
let p = Parser::new("9");
assert!(p.parse_minute().is_err());
let p = Parser::new("60");
assert!(p.parse_minute().is_err());
}
#[test]
fn parse_second() {
let p = Parser::new("00");
assert_eq!(p.parse_second().unwrap(), 0);
let p = Parser::new("24");
assert_eq!(p.parse_second().unwrap(), 24);
let p = Parser::new("59");
assert_eq!(p.parse_second().unwrap(), 59);
let p = Parser::new("599");
assert_eq!(p.parse_second().unwrap(), 59);
let p = Parser::new("0");
assert!(p.parse_second().is_err());
let p = Parser::new("1");
assert!(p.parse_second().is_err());
let p = Parser::new("9");
assert!(p.parse_second().is_err());
let p = Parser::new("60");
assert!(p.parse_second().is_err());
}
#[test]
fn parse_number_with_exactly_n_digits() {
let p = Parser::new("1");
assert_eq!(p.parse_number_with_exactly_n_digits(1).unwrap(), 1);
let p = Parser::new("12");
assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
let p = Parser::new("123");
assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
let p = Parser::new("");
assert!(p.parse_number_with_exactly_n_digits(1).is_err());
let p = Parser::new("1");
assert!(p.parse_number_with_exactly_n_digits(2).is_err());
let p = Parser::new("12");
assert!(p.parse_number_with_exactly_n_digits(3).is_err());
}
#[test]
fn parse_number_with_upto_n_digits() {
let p = Parser::new("1");
assert_eq!(p.parse_number_with_upto_n_digits(1).unwrap(), 1);
let p = Parser::new("1");
assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 1);
let p = Parser::new("12");
assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
let p = Parser::new("12");
assert_eq!(p.parse_number_with_upto_n_digits(3).unwrap(), 12);
let p = Parser::new("123");
assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
let p = Parser::new("");
assert!(p.parse_number_with_upto_n_digits(1).is_err());
let p = Parser::new("a");
assert!(p.parse_number_with_upto_n_digits(1).is_err());
}
#[test]
fn to_dst_civil_datetime_utc_range() {
let tz = posix_time_zone("WART4WARST,J1/-3,J365/20");
let dst_info = DstInfo {
dst: tz.dst.as_ref().unwrap(),
start: date(2024, 1, 1).at(1, 0, 0, 0),
end: date(2024, 12, 31).at(23, 0, 0, 0),
};
assert_eq!(tz.dst_info_utc(2024), Some(dst_info));
let tz = posix_time_zone("WART4WARST,J1/-4,J365/21");
let dst_info = DstInfo {
dst: tz.dst.as_ref().unwrap(),
start: date(2024, 1, 1).at(0, 0, 0, 0),
end: date(2024, 12, 31).at(23, 59, 59, 999_999_999),
};
assert_eq!(tz.dst_info_utc(2024), Some(dst_info));
let tz = posix_time_zone("EST5EDT,M3.2.0,M11.1.0");
let dst_info = DstInfo {
dst: tz.dst.as_ref().unwrap(),
start: date(2024, 3, 10).at(7, 0, 0, 0),
end: date(2024, 11, 3).at(6, 0, 0, 0),
};
assert_eq!(tz.dst_info_utc(2024), Some(dst_info));
}
#[test]
fn regression_permanent_dst() {
let tz = posix_time_zone("XXX-2<+01>-1,0/0,J365/23");
let dst_info = DstInfo {
dst: tz.dst.as_ref().unwrap(),
start: date(2087, 1, 1).at(0, 0, 0, 0),
end: date(2087, 12, 31).at(23, 59, 59, 999_999_999),
};
assert_eq!(tz.dst_info_utc(2087), Some(dst_info));
}
#[test]
fn reasonable() {
assert!(TimeZone::parse(b"EST5").is_ok());
assert!(TimeZone::parse(b"EST5EDT").is_err());
assert!(TimeZone::parse(b"EST5EDT,J1,J365").is_ok());
let tz = posix_time_zone("EST24EDT,J1,J365");
assert_eq!(
tz,
TimeZone {
std_abbrev: astr("EST"),
std_offset: off(-24 * 60 * 60),
dst: Some(Dst {
abbrev: astr("EDT"),
offset: off(-23 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(1),
time: TransitionCivilTime::DEFAULT,
},
end: DayTime {
date: Day::JulianOne(365),
time: TransitionCivilTime::DEFAULT,
},
},
}),
},
);
let tz = posix_time_zone("EST-24EDT,J1,J365");
assert_eq!(
tz,
TimeZone {
std_abbrev: astr("EST"),
std_offset: off(24 * 60 * 60),
dst: Some(Dst {
abbrev: astr("EDT"),
offset: off(25 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::JulianOne(1),
time: TransitionCivilTime::DEFAULT,
},
end: DayTime {
date: Day::JulianOne(365),
time: TransitionCivilTime::DEFAULT,
},
},
}),
},
);
}
#[test]
fn posix_date_time_spec_to_datetime() {
let to_datetime = |daytime: &DayTime, year: i16| {
daytime.to_datetime(year, Offset::UTC)
};
let tz = posix_time_zone("EST5EDT,J1,J365/5:12:34");
assert_eq!(
to_datetime(&tz.rule().start, 2023),
date(2023, 1, 1).at(2, 0, 0, 0),
);
assert_eq!(
to_datetime(&tz.rule().end, 2023),
date(2023, 12, 31).at(5, 12, 34, 0),
);
let tz = posix_time_zone("EST+5EDT,M3.2.0/2,M11.1.0/2");
assert_eq!(
to_datetime(&tz.rule().start, 2024),
date(2024, 3, 10).at(2, 0, 0, 0),
);
assert_eq!(
to_datetime(&tz.rule().end, 2024),
date(2024, 11, 3).at(2, 0, 0, 0),
);
let tz = posix_time_zone("EST+5EDT,M1.1.1,M12.5.2");
assert_eq!(
to_datetime(&tz.rule().start, 2024),
date(2024, 1, 1).at(2, 0, 0, 0),
);
assert_eq!(
to_datetime(&tz.rule().end, 2024),
date(2024, 12, 31).at(2, 0, 0, 0),
);
let tz = posix_time_zone("EST5EDT,0/0,J365/25");
assert_eq!(
to_datetime(&tz.rule().start, 2024),
date(2024, 1, 1).at(0, 0, 0, 0),
);
assert_eq!(
to_datetime(&tz.rule().end, 2024),
date(2024, 12, 31).at(23, 59, 59, 999_999_999),
);
let tz = posix_time_zone("XXX3EDT4,0/0,J365/23");
assert_eq!(
to_datetime(&tz.rule().start, 2024),
date(2024, 1, 1).at(0, 0, 0, 0),
);
assert_eq!(
to_datetime(&tz.rule().end, 2024),
date(2024, 12, 31).at(23, 0, 0, 0),
);
let tz = posix_time_zone("XXX3EDT4,0/0,365");
assert_eq!(
to_datetime(&tz.rule().end, 2023),
date(2023, 12, 31).at(23, 59, 59, 999_999_999),
);
assert_eq!(
to_datetime(&tz.rule().end, 2024),
date(2024, 12, 31).at(2, 0, 0, 0),
);
let tz = posix_time_zone("XXX3EDT4,J1/-167:59:59,J365/167:59:59");
assert_eq!(
to_datetime(&tz.rule().start, 2024),
date(2024, 1, 1).at(0, 0, 0, 0),
);
assert_eq!(
to_datetime(&tz.rule().end, 2024),
date(2024, 12, 31).at(23, 59, 59, 999_999_999),
);
}
#[test]
fn posix_date_time_spec_time() {
let tz = posix_time_zone("EST5EDT,J1,J365/5:12:34");
assert_eq!(tz.rule().start.time, TransitionCivilTime::DEFAULT);
assert_eq!(
tz.rule().end.time,
TransitionCivilTime { second: 5 * 60 * 60 + 12 * 60 + 34 },
);
}
#[test]
fn posix_date_spec_to_date() {
let tz = posix_time_zone("EST+5EDT,M3.2.0/2,M11.1.0/2");
let start = tz.rule().start.date.to_date(2023);
assert_eq!(start, Some(date(2023, 3, 12)));
let end = tz.rule().end.date.to_date(2023);
assert_eq!(end, Some(date(2023, 11, 5)));
let start = tz.rule().start.date.to_date(2024);
assert_eq!(start, Some(date(2024, 3, 10)));
let end = tz.rule().end.date.to_date(2024);
assert_eq!(end, Some(date(2024, 11, 3)));
let tz = posix_time_zone("EST+5EDT,J60,J365");
let start = tz.rule().start.date.to_date(2023);
assert_eq!(start, Some(date(2023, 3, 1)));
let end = tz.rule().end.date.to_date(2023);
assert_eq!(end, Some(date(2023, 12, 31)));
let start = tz.rule().start.date.to_date(2024);
assert_eq!(start, Some(date(2024, 3, 1)));
let end = tz.rule().end.date.to_date(2024);
assert_eq!(end, Some(date(2024, 12, 31)));
let tz = posix_time_zone("EST+5EDT,59,365");
let start = tz.rule().start.date.to_date(2023);
assert_eq!(start, Some(date(2023, 3, 1)));
let end = tz.rule().end.date.to_date(2023);
assert_eq!(end, None);
let start = tz.rule().start.date.to_date(2024);
assert_eq!(start, Some(date(2024, 2, 29)));
let end = tz.rule().end.date.to_date(2024);
assert_eq!(end, Some(date(2024, 12, 31)));
let tz = posix_time_zone("EST+5EDT,M1.1.1,M12.5.2");
let start = tz.rule().start.date.to_date(2024);
assert_eq!(start, Some(date(2024, 1, 1)));
let end = tz.rule().end.date.to_date(2024);
assert_eq!(end, Some(date(2024, 12, 31)));
}
#[test]
fn posix_time_spec_to_civil_time() {
let tz = posix_time_zone("EST5EDT,J1,J365/5:12:34");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time.second,
2 * 60 * 60,
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time.second,
5 * 60 * 60 + 12 * 60 + 34,
);
let tz = posix_time_zone("EST5EDT,J1/23:59:59,J365/24:00:00");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time.second,
23 * 60 * 60 + 59 * 60 + 59,
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time.second,
24 * 60 * 60,
);
let tz = posix_time_zone("EST5EDT,J1/-1,J365/167:00:00");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time.second,
-1 * 60 * 60,
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time.second,
167 * 60 * 60,
);
}
#[test]
fn parse_iana() {
let p = TimeZone::parse(b"CRAZY5SHORT,M12.5.0/50,0/2").unwrap();
assert_eq!(
p,
TimeZone {
std_abbrev: astr("CRAZY"),
std_offset: off(-5 * 60 * 60),
dst: Some(Dst {
abbrev: astr("SHORT"),
offset: off(-4 * 60 * 60),
rule: Rule {
start: DayTime {
date: Day::WeekdayOfMonth {
month: 12,
week: 5,
weekday: Weekday::Sunday,
},
time: TransitionCivilTime { second: 50 * 60 * 60 },
},
end: DayTime {
date: Day::JulianZero(0),
time: TransitionCivilTime { second: 2 * 60 * 60 },
},
},
}),
},
);
assert!(TimeZone::parse(b"America/New_York").is_err());
assert!(TimeZone::parse(b":America/New_York").is_err());
}
#[test]
fn parse_empty_is_err() {
assert!(TimeZone::parse(b"").is_err());
}
#[test]
fn parse_weird_is_err() {
let s =
b"AAAAAAAAAAAAAAACAAAAAAAAAAAAQA8AACAAAAAAAAAAAAAAAAACAAAAAAAAAAA";
assert!(TimeZone::parse(s).is_err());
let s =
b"<AAAAAAAAAAAAAAACAAAAAAAAAAAAQA>8<AACAAAAAAAAAAAAAAAAACAAAAAAAAAAA>";
assert!(TimeZone::parse(s).is_err());
let s = b"PPPPPPPPPPPPPPPPPPPPnoofPPPAAA6DaPPPPPPPPPPPPPPPPPPPPPnoofPPPPP,n";
assert!(TimeZone::parse(s).is_err());
let s = b"oooooooooovooooooooooooooooool9<ooooo2o-o-oooooookoorooooooooroo8";
assert!(TimeZone::parse(s).is_err());
}
#[test]
fn parse_long() {
let ts = Timestamp::new(1782939639, 0).unwrap();
let p = posix_time_zone("EST5EDT,M3.2.0,M11.1.0");
assert_eq!(p.next_transition(ts).unwrap().abbreviation(), "EST");
assert_eq!(p.previous_transition(ts).unwrap().abbreviation(), "EDT");
let p = posix_time_zone("ABCDEF5abcdef,M3.2.0,M11.1.0");
assert_eq!(p.next_transition(ts).unwrap().abbreviation(), "ABCDEF");
assert_eq!(
p.previous_transition(ts).unwrap().abbreviation(),
"abcdef"
);
#[cfg(not(feature = "alloc"))]
{
let core_only_over_max = "ABCDEFG5abcdefg,M3.2.0,M11.1.0";
assert!(TimeZone::parse(core_only_over_max).is_err());
}
#[cfg(feature = "alloc")]
{
let alloc_max = "\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDE\
5\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcde\
,M3.2.0,M11.1.0\
";
let p = posix_time_zone(alloc_max);
assert_eq!(
p.next_transition(ts).unwrap().abbreviation(),
"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDE\
",
);
assert_eq!(
p.previous_transition(ts).unwrap().abbreviation(),
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcde\
",
);
let alloc_over_max = "\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWX\
ABCDEF\
5\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx\
abcdef\
,M3.2.0,M11.1.0\
";
assert!(TimeZone::parse(alloc_over_max).is_err());
}
}
#[test]
fn extremes() {
assert!(TimeZone::parse(b"XXX25YYY,M3.2.0,M11.1.0").is_err());
assert!(TimeZone::parse(b"XXX-25YYY,M3.2.0,M11.1.0").is_err());
assert!(
TimeZone::parse(b"XXX24:59:59YYY25:59:59,M3.2.0,M11.1.0").is_err()
);
assert!(TimeZone::parse(b"XXX-24:59:59YYY-25:59:59,M3.2.0,M11.1.0")
.is_err());
let p = TimeZone::parse(b"XXX24:59:59YYY,M3.2.0,M11.1.0").unwrap();
let ts = Timestamp::from_second(1783116302 + 6 * 30 * 86400).unwrap();
assert_eq!(p.to_offset(ts), Offset::from_seconds(-89_999).unwrap());
let ts = Timestamp::from_second(1783116302).unwrap();
assert_eq!(p.to_offset(ts), Offset::from_seconds(-86_399).unwrap());
let p = TimeZone::parse(b"XXX-24:59:59YYY,M3.2.0,M11.1.0").unwrap();
let ts = Timestamp::from_second(1783116302 + 6 * 30 * 86400).unwrap();
assert_eq!(p.to_offset(ts), Offset::from_seconds(89_999).unwrap());
let ts = Timestamp::from_second(1783116302).unwrap();
assert_eq!(p.to_offset(ts), Offset::MAX);
}
#[test]
fn parse_posix_tz() {
assert!(TzEnv::parse("EST5EDT").is_err());
let tz = TzEnv::parse(":EST5EDT").unwrap();
assert_eq!(
tz,
TzEnv::Implementation(TimeZoneId::new("EST5EDT").unwrap())
);
assert!(TzEnv::parse(b":EST5\xFFEDT").is_err());
}
}