#![allow(warnings)]
use core::cell::Cell;
use alloc::{
boxed::Box,
string::{String, ToString},
};
use crate::{
civil::{Date, DateTime, Time, Weekday},
error::{err, Error, ErrorContext},
span::Span,
timestamp::Timestamp,
tz::{AmbiguousOffset, Dst, Offset},
util::{
escape::{Byte, Bytes},
parse,
rangeint::{ri16, ri8, RFrom, RInto},
t::{self, Minute, Month, Second, Sign, SpanZoneOffset, Year, C},
},
SignedDuration,
};
const ABBREVIATION_MAX: usize = 255;
type PosixHour = ri8<0, 25>;
type IanaHour = ri16<0, 167>;
type PosixJulianDayNoLeap = ri16<1, 365>;
type PosixJulianDayWithLeap = ri16<0, 365>;
type PosixWeek = ri8<1, 5>;
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum PosixTz {
Rule(PosixTimeZone),
Implementation(Box<str>),
}
impl PosixTz {
pub(crate) fn parse(bytes: impl AsRef<[u8]>) -> Result<PosixTz, Error> {
let bytes = bytes.as_ref();
if bytes.get(0) == Some(&b':') {
let Ok(string) = core::str::from_utf8(&bytes[1..]) else {
return Err(err!(
"POSIX time zone string with a ':' prefix contains \
invalid UTF-8: {:?}",
Bytes(&bytes[1..]),
));
};
Ok(PosixTz::Implementation(string.to_string().into_boxed_str()))
} else {
let parser = Parser { ianav3plus: true, ..Parser::new(bytes) };
parser.parse().map(PosixTz::Rule)
}
}
#[cfg(feature = "std")]
pub(crate) fn parse_os_str(
osstr: impl AsRef<std::ffi::OsStr>,
) -> Result<PosixTz, Error> {
PosixTz::parse(parse::os_str_bytes(osstr.as_ref())?)
}
#[cfg(test)]
fn unwrap_rule(self) -> PosixTimeZone {
match self {
PosixTz::Rule(rule) => rule,
_ => unreachable!("expected PosixTz::Rule variant"),
}
}
#[cfg(test)]
fn unwrap_implementation(self) -> Box<str> {
match self {
PosixTz::Implementation(string) => string,
_ => unreachable!("expected PosixTz::Implementation variant"),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct IanaTz(ReasonablePosixTimeZone);
impl IanaTz {
pub(crate) fn parse_v2(bytes: impl AsRef<[u8]>) -> Result<IanaTz, Error> {
let bytes = bytes.as_ref();
let posix_tz = Parser::new(bytes).parse().map_err(|e| {
e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes)))
})?;
let Ok(reasonable) = posix_tz.reasonable() else {
return Err(err!(
"TZ string {:?} in v2 tzfile has DST but no transition rules",
Bytes(bytes),
));
};
Ok(IanaTz(reasonable))
}
pub(crate) fn parse_v3plus(
bytes: impl AsRef<[u8]>,
) -> Result<IanaTz, Error> {
let bytes = bytes.as_ref();
let posix_tz = Parser { ianav3plus: true, ..Parser::new(bytes) }
.parse()
.map_err(|e| {
e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes)))
})?;
let Ok(reasonable) = posix_tz.reasonable() else {
return Err(err!(
"TZ string {:?} in v3+ tzfile has DST but no transition rules",
Bytes(bytes),
));
};
Ok(IanaTz(reasonable))
}
pub(crate) fn into_tz(self) -> ReasonablePosixTimeZone {
self.0
}
}
#[derive(Debug)]
pub(crate) struct ReasonablePosixTimeZone {
original: Box<str>,
std_abbrev: Box<str>,
std_offset: PosixOffset,
dst: Option<ReasonablePosixDst>,
}
impl ReasonablePosixTimeZone {
pub(crate) fn as_str(&self) -> &str {
&self.original
}
pub(crate) fn to_offset(
&self,
timestamp: Timestamp,
) -> (Offset, Dst, &str) {
if self.dst.is_none() {
return (self.std_offset(), Dst::No, &self.std_abbrev);
}
let dt = Offset::UTC.to_datetime(timestamp);
self.dst_info_utc(dt.date().year_ranged())
.filter(|dst_info| dst_info.in_dst(dt))
.map(|dst_info| (dst_info.offset, Dst::Yes, &*dst_info.dst.abbrev))
.unwrap_or_else(|| (self.std_offset(), Dst::No, &self.std_abbrev))
}
pub(crate) fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
let year = dt.date().year_ranged();
let std_offset = self.std_offset();
let Some(dst_info) = self.dst_info_wall(year) else {
return AmbiguousOffset::Unambiguous { offset: std_offset };
};
let diff = dst_info.offset - std_offset;
if diff.get_seconds_ranged() == 0 {
debug_assert_eq!(std_offset, dst_info.offset);
AmbiguousOffset::Unambiguous { offset: std_offset }
} else if diff.is_negative() {
if dst_info.in_dst(dt) {
AmbiguousOffset::Unambiguous { offset: dst_info.offset }
} else {
let fold_start = dst_info.start.saturating_add(diff);
let gap_end = dst_info.end.saturating_sub(diff);
if fold_start <= dt && dt < dst_info.start {
AmbiguousOffset::Fold {
before: std_offset,
after: dst_info.offset,
}
} else if dst_info.end <= dt && dt < gap_end {
AmbiguousOffset::Gap {
before: dst_info.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(diff);
let fold_start = dst_info.end.saturating_sub(diff);
if dst_info.start <= dt && dt < gap_end {
AmbiguousOffset::Gap {
before: std_offset,
after: dst_info.offset,
}
} else if fold_start <= dt && dt < dst_info.end {
AmbiguousOffset::Fold {
before: dst_info.offset,
after: std_offset,
}
} else {
AmbiguousOffset::Unambiguous { offset: dst_info.offset }
}
}
}
}
pub(crate) fn previous_transition(
&self,
timestamp: Timestamp,
) -> Option<Timestamp> {
let dt = Offset::UTC.to_datetime(timestamp);
let dst_info = self.dst_info_utc(dt.date().year_ranged())?;
let (earlier, later) = dst_info.ordered();
let prev = if dt > later {
later
} else if dt > earlier {
earlier
} else {
let prev_year = dt.date().year_ranged().checked_sub(C(1))?;
let dst_info = self.dst_info_utc(prev_year)?;
let (_, later) = dst_info.ordered();
later
};
Offset::UTC.to_timestamp(prev).ok()
}
pub(crate) fn next_transition(
&self,
timestamp: Timestamp,
) -> Option<Timestamp> {
let dt = Offset::UTC.to_datetime(timestamp);
let dst_info = self.dst_info_utc(dt.date().year_ranged())?;
let (earlier, later) = dst_info.ordered();
let next = if dt < earlier {
earlier
} else if dt < later {
later
} else {
let next_year = dt.date().year_ranged().checked_add(C(1))?;
let dst_info = self.dst_info_utc(next_year)?;
let (earlier, _) = dst_info.ordered();
earlier
};
Offset::UTC.to_timestamp(next).ok()
}
fn std_offset(&self) -> Offset {
self.std_offset.to_offset()
}
fn dst_offset(&self) -> Option<Offset> {
Some(self.dst.as_ref()?.posix_offset(&self.std_offset).to_offset())
}
fn dst_info_utc(&self, year: impl RInto<Year>) -> Option<DstInfo<'_>> {
let year = year.rinto();
let dst = self.dst.as_ref()?;
let std_offset = self.std_offset.to_offset();
let dst_offset = dst.posix_offset(&self.std_offset).to_offset();
let start = dst.rule.start.to_datetime(year, std_offset);
let end = dst.rule.end.to_datetime(year, dst_offset);
Some(DstInfo { dst, offset: dst_offset, start, end })
}
fn dst_info_wall(&self, year: impl RInto<Year>) -> Option<DstInfo<'_>> {
let year = year.rinto();
let dst = self.dst.as_ref()?;
let dst_offset = dst.posix_offset(&self.std_offset).to_offset();
let start = dst.rule.start.to_datetime(year, Offset::ZERO);
let end = dst.rule.end.to_datetime(year, Offset::ZERO);
Some(DstInfo { dst, offset: dst_offset, start, end })
}
#[cfg(test)]
fn rule(&self) -> Rule {
self.dst.as_ref().unwrap().rule
}
}
impl Eq for ReasonablePosixTimeZone {}
impl PartialEq for ReasonablePosixTimeZone {
fn eq(&self, rhs: &ReasonablePosixTimeZone) -> bool {
self.original == rhs.original
}
}
#[derive(Debug, Eq, PartialEq)]
struct DstInfo<'a> {
dst: &'a ReasonablePosixDst,
offset: Offset,
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)
}
}
}
#[derive(Debug, Eq, PartialEq)]
struct ReasonablePosixDst {
abbrev: Box<str>,
offset: Option<PosixOffset>,
rule: Rule,
}
impl ReasonablePosixDst {
fn posix_offset(&self, std_offset: &PosixOffset) -> PosixOffset {
if let Some(ref offset) = self.offset {
return *offset;
}
PosixOffset {
hour: std_offset.hour - (std_offset.sign() * C(1)),
..*std_offset
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct PosixTimeZone {
original: Box<str>,
std_abbrev: Box<str>,
std_offset: PosixOffset,
dst: Option<PosixDst>,
}
impl PosixTimeZone {
pub(crate) fn reasonable(
mut self,
) -> Result<ReasonablePosixTimeZone, PosixTimeZone> {
if let Some(mut dst) = self.dst.take() {
if let Some(rule) = dst.rule.take() {
Ok(ReasonablePosixTimeZone {
original: self.original,
std_abbrev: self.std_abbrev,
std_offset: self.std_offset,
dst: Some(ReasonablePosixDst {
abbrev: dst.abbrev,
offset: dst.offset,
rule,
}),
})
} else {
Err(PosixTimeZone { dst: Some(dst), ..self })
}
} else {
Ok(ReasonablePosixTimeZone {
original: self.original,
std_abbrev: self.std_abbrev,
std_offset: self.std_offset,
dst: None,
})
}
}
}
#[derive(Debug, Eq, PartialEq)]
struct PosixDst {
abbrev: Box<str>,
offset: Option<PosixOffset>,
rule: Option<Rule>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct PosixOffset {
sign: Option<Sign>,
hour: PosixHour,
minute: Option<Minute>,
second: Option<Second>,
}
impl PosixOffset {
fn to_offset(&self) -> Offset {
let sign = SpanZoneOffset::rfrom(-self.sign());
let hour = SpanZoneOffset::rfrom(self.hour);
let minute =
SpanZoneOffset::rfrom(self.minute.unwrap_or(C(0).rinto()));
let second =
SpanZoneOffset::rfrom(self.second.unwrap_or(C(0).rinto()));
let seconds = (hour * t::SECONDS_PER_HOUR)
+ (minute * t::SECONDS_PER_MINUTE)
+ second;
Offset::from_seconds_ranged(sign * seconds)
}
fn sign(&self) -> Sign {
self.sign.unwrap_or(Sign::N::<1>())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Rule {
start: PosixDateTimeSpec,
end: PosixDateTimeSpec,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct PosixDateTimeSpec {
date: PosixDateSpec,
time: Option<PosixTimeSpec>,
}
impl PosixDateTimeSpec {
fn to_datetime(&self, year: impl RInto<Year>, offset: Offset) -> DateTime {
let year = year.rinto();
let mkmin = || {
Date::new_ranged(year, C(1), C(1)).unwrap().to_datetime(Time::MIN)
};
let mkmax = || {
Date::new_ranged(year, C(12), C(31))
.unwrap()
.to_datetime(Time::MAX)
};
let Some(date) = self.date.to_civil_date(year) else { return mkmax() };
let mut dt = date.to_datetime(Time::MIN);
let dur_transition = self.time().to_duration();
let dur_offset = offset.to_duration();
dt = dt.checked_add(dur_transition).unwrap_or_else(|_| {
if dur_transition.is_negative() {
mkmin()
} else {
mkmax()
}
});
dt = dt.checked_sub(dur_offset).unwrap_or_else(|_| {
if dur_transition.is_negative() {
mkmax()
} else {
mkmin()
}
});
if dt.date().year() < year {
mkmin()
} else if dt.date().year() > year {
mkmax()
} else {
dt
}
}
fn time(self) -> PosixTimeSpec {
const DEFAULT: PosixTimeSpec = PosixTimeSpec {
sign: None,
hour: IanaHour::N::<2>(),
minute: None,
second: None,
};
self.time.unwrap_or(DEFAULT)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PosixDateSpec {
JulianOne(PosixJulianDayNoLeap),
JulianZero(PosixJulianDayWithLeap),
WeekdayOfMonth(WeekdayOfMonth),
}
impl PosixDateSpec {
fn to_civil_date(&self, year: impl RInto<Year>) -> Option<Date> {
match *self {
PosixDateSpec::JulianOne(day) => {
let first = Date::new_ranged(year, C(1), C(1)).unwrap();
Some(
first
.with()
.day_of_year_no_leap(day.get())
.build()
.expect("Julian 'J day' should be in bounds"),
)
}
PosixDateSpec::JulianZero(day) => {
let first = Date::new_ranged(year, C(1), C(1)).unwrap();
let off1 = day.get().checked_add(1).unwrap();
first.with().day_of_year(off1).build().ok()
}
PosixDateSpec::WeekdayOfMonth(wom) => {
let first = Date::new_ranged(year, wom.month, C(1)).unwrap();
let week = wom.week();
debug_assert!(week == -1 || (1..=4).contains(&week));
Some(
first
.nth_weekday_of_month(week, wom.weekday)
.expect("nth weekday always exists"),
)
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct WeekdayOfMonth {
month: Month,
week: PosixWeek,
weekday: Weekday,
}
impl WeekdayOfMonth {
fn week(&self) -> i8 {
if self.week == 5 {
-1
} else {
self.week.get()
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct PosixTimeSpec {
sign: Option<Sign>,
hour: IanaHour,
minute: Option<Minute>,
second: Option<Second>,
}
impl PosixTimeSpec {
fn to_civil_time(&self) -> Option<Time> {
if self.sign.map_or(false, |sign| sign != 1) {
return None;
}
if self.hour > 23 {
return None;
}
Some(Time::new_ranged(
self.hour.min(C(23)),
self.minute.unwrap_or(C(0).rinto()),
self.second.unwrap_or(C(0).rinto()),
C(0),
))
}
fn to_duration(&self) -> SignedDuration {
let sign = i64::from(self.sign());
let hour = sign * i64::from(self.hour);
let minute = sign * i64::from(self.minute.unwrap_or(C(0).rinto()));
let second = sign * i64::from(self.second.unwrap_or(C(0).rinto()));
SignedDuration::from_secs(second + (60 * minute) + (60 * 60 * hour))
}
fn sign(&self) -> Sign {
self.sign.unwrap_or(Sign::N::<1>())
}
}
#[derive(Debug)]
struct Parser<'s> {
tz: &'s [u8],
pos: 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: Cell::new(0), ianav3plus: false }
}
fn parse(&self) -> Result<PosixTimeZone, Error> {
let time_zone = self.parse_posix_time_zone()?;
if !self.remaining().is_empty() {
return Err(err!(
"expected entire TZ string to be a valid POSIX \
time zone, but found '{}' after what would otherwise \
be a valid POSIX TZ string",
Bytes(self.remaining()),
));
}
Ok(time_zone)
}
fn parse_posix_time_zone(&self) -> Result<PosixTimeZone, Error> {
let original = String::from_utf8_lossy(self.remaining()).into();
let std_abbrev = self
.parse_abbreviation()
.map_err(|e| e.context("failed to parse standard abbreviation"))?;
let std_offset = self
.parse_posix_offset()
.map_err(|e| e.context("failed to parse standard offset"))?;
let mut dst = None;
if !self.is_done()
&& (self.byte().is_ascii_alphabetic() || self.byte() == b'<')
{
dst = Some(self.parse_posix_dst()?);
}
Ok(PosixTimeZone { original, std_abbrev, std_offset, dst })
}
fn parse_posix_dst(&self) -> Result<PosixDst, Error> {
let abbrev = self
.parse_abbreviation()
.map_err(|e| e.context("failed to parse DST abbreviation"))?;
let mut dst = PosixDst { abbrev, offset: None, rule: None };
if self.is_done() {
return Ok(dst);
}
if self.byte() != b',' {
dst.offset = Some(
self.parse_posix_offset()
.map_err(|e| e.context("failed to parse DST offset"))?,
);
if self.is_done() {
return Ok(dst);
}
}
if self.byte() != b',' {
return Err(err!(
"after parsing DST offset in POSIX time zone string, \
found '{}' but expected a ','",
Byte(self.byte()),
));
}
if !self.bump() {
return Err(err!(
"after parsing DST offset in POSIX time zone string, \
found end of string after a trailing ','",
));
}
dst.rule = Some(self.parse_rule()?);
Ok(dst)
}
fn parse_abbreviation(&self) -> Result<Box<str>, Error> {
if self.byte() == b'<' {
if !self.bump() {
return Err(err!(
"found opening '<' quote for abbreviation in \
POSIX time zone string, and expected a name \
following it, but found the end of string instead"
));
}
self.parse_quoted_abbreviation()
} else {
self.parse_unquoted_abbreviation()
}
}
fn parse_unquoted_abbreviation(&self) -> Result<Box<str>, Error> {
let start = self.pos();
for i in 0.. {
if !self.byte().is_ascii_alphabetic() {
break;
}
if i >= ABBREVIATION_MAX {
return Err(err!(
"expected abbreviation with at most {} bytes, \
but found a longer abbreviation beginning with '{}'",
ABBREVIATION_MAX,
Bytes(&self.tz[start..i]),
));
}
if !self.bump() {
break;
}
}
let end = self.pos();
let abbrev =
String::from_utf8(self.tz[start..end].to_vec()).map_err(|_| {
err!(
"found abbreviation '{}', but it is not valid UTF-8",
Bytes(&self.tz[start..end]),
)
})?;
if abbrev.len() < 3 {
return Err(err!(
"expected abbreviation with 3 or more bytes, but found \
abbreviation {:?} with {} bytes",
abbrev,
abbrev.len(),
));
}
Ok(abbrev.into())
}
fn parse_quoted_abbreviation(&self) -> Result<Box<str>, Error> {
let start = self.pos();
for i in 0.. {
if !self.byte().is_ascii_alphanumeric()
&& self.byte() != b'+'
&& self.byte() != b'-'
{
break;
}
if i >= ABBREVIATION_MAX {
return Err(err!(
"expected abbreviation with at most {} bytes, \
but found a longer abbreviation beginning with '{}'",
ABBREVIATION_MAX,
Bytes(&self.tz[start..i]),
));
}
if !self.bump() {
break;
}
}
let end = self.pos();
let abbrev =
String::from_utf8(self.tz[start..end].to_vec()).map_err(|_| {
err!(
"found abbreviation '{}', but it is not valid UTF-8",
Bytes(&self.tz[start..end]),
)
})?;
if self.is_done() {
return Err(err!(
"found non-empty quoted abbreviation {abbrev:?}, but \
did not find expected end-of-quoted abbreviation \
'>' character",
));
}
if self.byte() != b'>' {
return Err(err!(
"found non-empty quoted abbreviation {abbrev:?}, but \
found '{}' instead of end-of-quoted abbreviation '>' \
character",
Byte(self.byte()),
));
}
self.bump();
if abbrev.len() < 3 {
return Err(err!(
"expected abbreviation with 3 or more bytes, but found \
abbreviation {abbrev:?} with {} bytes",
abbrev.len(),
));
}
Ok(abbrev.into())
}
fn parse_posix_offset(&self) -> Result<PosixOffset, Error> {
let sign = self.parse_optional_sign().map_err(|e| {
e.context(
"failed to parse sign for time offset \
in POSIX time zone string",
)
})?;
let hour = self.parse_hour_posix()?;
let mut offset =
PosixOffset { sign, hour, minute: None, second: None };
if self.maybe_byte() != Some(b':') {
return Ok(offset);
}
if !self.bump() {
return Err(err!(
"incomplete time in POSIX timezone (missing minutes)",
));
}
let minute = Some(self.parse_minute()?);
if self.maybe_byte() != Some(b':') {
return Ok(PosixOffset { sign, hour, minute, second: None });
}
if !self.bump() {
return Err(err!(
"incomplete time in POSIX timezone (missing seconds)",
));
}
let second = Some(self.parse_second()?);
Ok(PosixOffset { sign, hour, minute, second })
}
fn parse_rule(&self) -> Result<Rule, Error> {
let start = self.parse_posix_datetime_spec().map_err(|e| {
e.context("failed to parse start of DST transition rule")
})?;
if self.maybe_byte() != Some(b',') || !self.bump() {
return Err(err!(
"expected end of DST rule after parsing the start \
of the DST rule"
));
}
let end = self.parse_posix_datetime_spec().map_err(|e| {
e.context("failed to parse end of DST transition rule")
})?;
Ok(Rule { start, end })
}
fn parse_posix_datetime_spec(&self) -> Result<PosixDateTimeSpec, Error> {
let date = self.parse_posix_date_spec()?;
let mut spec = PosixDateTimeSpec { date, time: None };
if self.maybe_byte() != Some(b'/') {
return Ok(spec);
}
if !self.bump() {
return Err(err!(
"expected time specification after '/' following a date
specification in a POSIX time zone DST transition rule",
));
}
spec.time = Some(self.parse_posix_time_spec()?);
Ok(spec)
}
fn parse_posix_date_spec(&self) -> Result<PosixDateSpec, Error> {
match self.byte() {
b'J' => {
if !self.bump() {
return Err(err!(
"expected one-based Julian day after 'J' in date \
specification of a POSIX time zone DST transition \
rule, but got the end of the string instead"
));
}
Ok(PosixDateSpec::JulianOne(
self.parse_posix_julian_day_no_leap()?,
))
}
b'0'..=b'9' => Ok(PosixDateSpec::JulianZero(
self.parse_posix_julian_day_with_leap()?,
)),
b'M' => {
if !self.bump() {
return Err(err!(
"expected month-week-weekday after 'M' in date \
specification of a POSIX time zone DST transition \
rule, but got the end of the string instead"
));
}
Ok(PosixDateSpec::WeekdayOfMonth(
self.parse_weekday_of_month()?,
))
}
_ => Err(err!(
"expected 'J', a digit or 'M' at the beginning of a date \
specification of a POSIX time zone DST transition rule, \
but got '{}' instead",
Byte(self.byte()),
)),
}
}
fn parse_posix_julian_day_no_leap(
&self,
) -> Result<PosixJulianDayNoLeap, Error> {
let number = self
.parse_number_with_upto_n_digits(3)
.map_err(|e| e.context("invalid one based Julian day"))?;
let day = PosixJulianDayNoLeap::new(number).ok_or_else(|| {
err!("invalid one based Julian day (must be in range 1..=365")
})?;
Ok(day)
}
fn parse_posix_julian_day_with_leap(
&self,
) -> Result<PosixJulianDayWithLeap, Error> {
let number = self
.parse_number_with_upto_n_digits(3)
.map_err(|e| e.context("invalid zero based Julian day"))?;
let day = PosixJulianDayWithLeap::new(number).ok_or_else(|| {
err!("invalid zero based Julian day (must be in range 0..=365")
})?;
Ok(day)
}
fn parse_weekday_of_month(&self) -> Result<WeekdayOfMonth, Error> {
let month = self.parse_month()?;
if self.maybe_byte() != Some(b'.') {
return Err(err!(
"expected '.' after month '{month}' in POSIX time zone rule"
));
}
if !self.bump() {
return Err(err!(
"expected week after month '{month}' in POSIX time zone rule"
));
}
let week = self.parse_week()?;
if self.maybe_byte() != Some(b'.') {
return Err(err!(
"expected '.' after week '{week}' in POSIX time zone rule"
));
}
if !self.bump() {
return Err(err!(
"expected day-of-week after week '{week}' in \
POSIX time zone rule"
));
}
let weekday = self.parse_weekday()?;
Ok(WeekdayOfMonth { month, week, weekday })
}
fn parse_posix_time_spec(&self) -> Result<PosixTimeSpec, Error> {
let (sign, hour) = if self.ianav3plus {
let sign = self.parse_optional_sign().map_err(|e| {
e.context(
"failed to parse sign for transition time \
in POSIX time zone string",
)
})?;
let hour = self.parse_hour_ianav3plus()?;
(sign, hour)
} else {
(None, self.parse_hour_posix()?.rinto())
};
let mut spec =
PosixTimeSpec { sign, hour, minute: None, second: None };
if self.maybe_byte() != Some(b':') {
return Ok(spec);
}
if !self.bump() {
return Err(err!(
"incomplete transition time in \
POSIX time zone string (missing minutes)",
));
}
let minute = Some(self.parse_minute()?);
if self.maybe_byte() != Some(b':') {
return Ok(PosixTimeSpec { sign, hour, minute, second: None });
}
if !self.bump() {
return Err(err!(
"incomplete transition time in \
POSIX time zone string (missing seconds)",
));
}
let second = Some(self.parse_second()?);
Ok(PosixTimeSpec { sign, hour, minute, second })
}
fn parse_month(&self) -> Result<Month, Error> {
let number = self.parse_number_with_upto_n_digits(2)?;
let month = Month::new(number).ok_or_else(|| {
err!("month in POSIX time zone must be in range 1..=12")
})?;
Ok(month)
}
fn parse_week(&self) -> Result<PosixWeek, Error> {
let number = self.parse_number_with_exactly_n_digits(1)?;
let week = PosixWeek::new(number).ok_or_else(|| {
err!("week in POSIX time zone must be in range 1..=5")
})?;
Ok(week)
}
fn parse_weekday(&self) -> Result<Weekday, Error> {
let number = self.parse_number_with_exactly_n_digits(1)?;
let number8 = i8::try_from(number).map_err(|_| {
err!(
"weekday '{number}' in POSIX time zone \
does not fit into 8-bit integer"
)
})?;
let weekday =
Weekday::from_sunday_zero_offset(number8).map_err(|_| {
err!(
"weekday in POSIX time zone must be in range 0..=6 \
(with 0 corresponding to Sunday), but got {number8}",
)
})?;
Ok(weekday)
}
fn parse_hour_ianav3plus(&self) -> Result<IanaHour, Error> {
assert!(self.ianav3plus);
let number = self
.parse_number_with_upto_n_digits(3)
.map_err(|e| e.context("invalid hour digits"))?;
let hour = IanaHour::new(number).ok_or_else(|| {
err!(
"hour in POSIX (IANA v3+ style) \
time zone must be in range -167..=167"
)
})?;
Ok(hour)
}
fn parse_hour_posix(&self) -> Result<PosixHour, Error> {
type PosixHour24 = ri8<0, 24>;
let number = self
.parse_number_with_upto_n_digits(2)
.map_err(|e| e.context("invalid hour digits"))?;
let hour = PosixHour24::new(number).ok_or_else(|| {
err!("hour in POSIX time zone must be in range 0..=24")
})?;
Ok(hour.rinto())
}
fn parse_minute(&self) -> Result<Minute, Error> {
let number = self
.parse_number_with_exactly_n_digits(2)
.map_err(|e| e.context("invalid minute digits"))?;
let minute = Minute::new(number).ok_or_else(|| {
err!("minute in POSIX time zone must be in range 0..=59")
})?;
Ok(minute)
}
fn parse_second(&self) -> Result<Second, Error> {
let number = self
.parse_number_with_exactly_n_digits(2)
.map_err(|e| e.context("invalid second digits"))?;
let second = Second::new(number).ok_or_else(|| {
err!("second in POSIX time zone must be in range 0..=59")
})?;
Ok(second)
}
fn parse_number_with_exactly_n_digits(
&self,
n: usize,
) -> Result<i64, Error> {
assert!(n >= 1, "numbers must have at least 1 digit");
let start = self.pos();
for i in 0..n {
if self.is_done() {
return Err(err!("expected {n} digits, but found {i}"));
}
if !self.byte().is_ascii_digit() {
return Err(err!("invalid digit '{}'", Byte(self.byte())));
}
self.bump();
}
let end = self.pos();
parse::i64(&self.tz[start..end])
}
fn parse_number_with_upto_n_digits(&self, n: usize) -> Result<i64, Error> {
assert!(n >= 1, "numbers must have at least 1 digit");
let start = self.pos();
for i in 0..n {
if self.is_done() || !self.byte().is_ascii_digit() {
break;
}
self.bump();
}
let end = self.pos();
parse::i64(&self.tz[start..end])
}
fn parse_optional_sign(&self) -> Result<Option<Sign>, Error> {
if self.is_done() {
return Ok(None);
}
Ok(match self.byte() {
b'-' => {
if !self.bump() {
return Err(err!(
"expected digit after '-' sign, but got end of input",
));
}
Some(Sign::N::<-1>())
}
b'+' => {
if !self.bump() {
return Err(err!(
"expected digit after '+' sign, but got end of input",
));
}
Some(Sign::N::<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()
}
fn remaining(&self) -> &[u8] {
&self.tz[self.pos()..]
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use crate::civil::{date, time};
use super::*;
fn reasonable_posix_time_zone(
input: impl AsRef<[u8]>,
) -> ReasonablePosixTimeZone {
PosixTz::parse(input).unwrap().unwrap_rule().reasonable().unwrap()
}
fn reasonable_iana_time_zone(
input: impl AsRef<[u8]>,
) -> ReasonablePosixTimeZone {
IanaTz::parse_v3plus(input).unwrap().into_tz()
}
#[cfg(feature = "std")]
#[test]
fn debug_posix_tz() -> anyhow::Result<()> {
const ENV: &str = "JIFF_DEBUG_POSIX_TZ";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let val = val
.to_str()
.ok_or_else(|| err!("{ENV} contains invalid UTF-8"))?;
let tz = Parser::new(val).parse()?;
std::eprintln!("{tz:#?}");
Ok(())
}
#[cfg(feature = "std")]
#[test]
fn debug_iana_tz() -> anyhow::Result<()> {
const ENV: &str = "JIFF_DEBUG_IANA_TZ";
let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
let val = val
.to_str()
.ok_or_else(|| err!("{ENV} contains invalid UTF-8"))?;
let tz = Parser { ianav3plus: true, ..Parser::new(val) }.parse()?;
std::eprintln!("{tz:#?}");
Ok(())
}
#[test]
fn reasonable_to_dst_civil_datetime_utc_range() {
let tz = reasonable_iana_time_zone("WART4WARST,J1/-3,J365/20");
let dst_info = DstInfo {
dst: tz.dst.as_ref().unwrap(),
offset: crate::tz::offset(-3),
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(C(2024)), Some(dst_info));
let tz = reasonable_iana_time_zone("WART4WARST,J1/-4,J365/21");
let dst_info = DstInfo {
dst: tz.dst.as_ref().unwrap(),
offset: crate::tz::offset(-3),
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(C(2024)), Some(dst_info));
let tz = reasonable_iana_time_zone("EST5EDT,M3.2.0,M11.1.0");
let dst_info = DstInfo {
dst: tz.dst.as_ref().unwrap(),
offset: crate::tz::offset(-4),
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(C(2024)), Some(dst_info));
}
#[test]
fn reasonable() {
assert!(PosixTz::parse("EST5")
.unwrap()
.unwrap_rule()
.reasonable()
.is_ok());
assert!(PosixTz::parse("EST5EDT")
.unwrap()
.unwrap_rule()
.reasonable()
.is_err());
assert!(PosixTz::parse("EST5EDT,J1,J365")
.unwrap()
.unwrap_rule()
.reasonable()
.is_ok());
let tz = reasonable_posix_time_zone("EST24EDT,J1,J365");
assert_eq!(
tz,
ReasonablePosixTimeZone {
original: "EST24EDT,J1,J365".into(),
std_abbrev: "EST".into(),
std_offset: PosixOffset {
sign: None,
hour: C(24).rinto(),
minute: None,
second: None,
},
dst: Some(ReasonablePosixDst {
abbrev: "EDT".into(),
offset: None,
rule: Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(1).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(365).rinto()),
time: None,
},
},
}),
},
);
let tz = reasonable_posix_time_zone("EST-24EDT,J1,J365");
assert_eq!(
tz,
ReasonablePosixTimeZone {
original: "EST-24EDT,J1,J365".into(),
std_abbrev: "EST".into(),
std_offset: PosixOffset {
sign: Some(C(-1).rinto()),
hour: C(24).rinto(),
minute: None,
second: None,
},
dst: Some(ReasonablePosixDst {
abbrev: "EDT".into(),
offset: None,
rule: Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(1).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(365).rinto()),
time: None,
},
},
}),
},
);
}
#[test]
fn posix_date_time_spec_to_datetime() {
let to_datetime = |spec: &PosixDateTimeSpec, year: i16| {
let year = Year::new(year).unwrap();
spec.to_datetime(year, crate::tz::offset(0))
};
let tz = reasonable_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 = reasonable_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 = reasonable_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 = reasonable_iana_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 = reasonable_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 = reasonable_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 =
reasonable_iana_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 = reasonable_posix_time_zone("EST5EDT,J1,J365/5:12:34");
assert_eq!(
tz.rule().start.time(),
PosixTimeSpec {
sign: None,
hour: C(2).rinto(),
minute: None,
second: None,
},
);
assert_eq!(
tz.rule().end.time(),
PosixTimeSpec {
sign: None,
hour: C(5).rinto(),
minute: Some(C(12).rinto()),
second: Some(C(34).rinto()),
},
);
}
#[test]
fn posix_date_spec_to_date() {
let tz = reasonable_posix_time_zone("EST+5EDT,M3.2.0/2,M11.1.0/2");
let start = tz.rule().start.date.to_civil_date(C(2023));
assert_eq!(start, Some(date(2023, 3, 12)));
let end = tz.rule().end.date.to_civil_date(C(2023));
assert_eq!(end, Some(date(2023, 11, 5)));
let start = tz.rule().start.date.to_civil_date(C(2024));
assert_eq!(start, Some(date(2024, 3, 10)));
let end = tz.rule().end.date.to_civil_date(C(2024));
assert_eq!(end, Some(date(2024, 11, 3)));
let tz = reasonable_posix_time_zone("EST+5EDT,J60,J365");
let start = tz.rule().start.date.to_civil_date(C(2023));
assert_eq!(start, Some(date(2023, 3, 1)));
let end = tz.rule().end.date.to_civil_date(C(2023));
assert_eq!(end, Some(date(2023, 12, 31)));
let start = tz.rule().start.date.to_civil_date(C(2024));
assert_eq!(start, Some(date(2024, 3, 1)));
let end = tz.rule().end.date.to_civil_date(C(2024));
assert_eq!(end, Some(date(2024, 12, 31)));
let tz = reasonable_posix_time_zone("EST+5EDT,59,365");
let start = tz.rule().start.date.to_civil_date(C(2023));
assert_eq!(start, Some(date(2023, 3, 1)));
let end = tz.rule().end.date.to_civil_date(C(2023));
assert_eq!(end, None);
let start = tz.rule().start.date.to_civil_date(C(2024));
assert_eq!(start, Some(date(2024, 2, 29)));
let end = tz.rule().end.date.to_civil_date(C(2024));
assert_eq!(end, Some(date(2024, 12, 31)));
let tz = reasonable_posix_time_zone("EST+5EDT,M1.1.1,M12.5.2");
let start = tz.rule().start.date.to_civil_date(C(2024));
assert_eq!(start, Some(date(2024, 1, 1)));
let end = tz.rule().end.date.to_civil_date(C(2024));
assert_eq!(end, Some(date(2024, 12, 31)));
}
#[test]
fn posix_time_spec_to_civil_time() {
let tz = reasonable_posix_time_zone("EST5EDT,J1,J365/5:12:34");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time().to_civil_time(),
Some(time(2, 0, 0, 0)),
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time().to_civil_time(),
Some(time(5, 12, 34, 0)),
);
let tz =
reasonable_posix_time_zone("EST5EDT,J1/23:59:59,J365/24:00:00");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time().to_civil_time(),
Some(time(23, 59, 59, 0)),
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time().to_civil_time(),
None,
);
let tz = reasonable_iana_time_zone("EST5EDT,J1/-1,J365/167:00:00");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time().to_civil_time(),
None,
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time().to_civil_time(),
None,
);
}
#[test]
fn posix_time_spec_to_span() {
let tz = reasonable_posix_time_zone("EST5EDT,J1,J365/5:12:34");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time().to_duration(),
SignedDuration::from_hours(2),
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time().to_duration(),
SignedDuration::from_secs((5 * 60 * 60) + (12 * 60) + 34),
);
let tz =
reasonable_posix_time_zone("EST5EDT,J1/23:59:59,J365/24:00:00");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time().to_duration(),
SignedDuration::from_secs((23 * 60 * 60) + (59 * 60) + 59),
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time().to_duration(),
SignedDuration::from_hours(24),
);
let tz = reasonable_iana_time_zone("EST5EDT,J1/-1,J365/167:00:00");
assert_eq!(
tz.dst.as_ref().unwrap().rule.start.time().to_duration(),
SignedDuration::from_hours(-1),
);
assert_eq!(
tz.dst.as_ref().unwrap().rule.end.time().to_duration(),
SignedDuration::from_hours(167),
);
}
#[test]
fn parse_posix_tz() {
let tz = PosixTz::parse("EST5EDT").unwrap();
assert_eq!(
tz,
PosixTz::Rule(PosixTimeZone {
original: "EST5EDT".into(),
std_abbrev: "EST".into(),
std_offset: PosixOffset {
sign: None,
hour: C(5).rinto(),
minute: None,
second: None,
},
dst: Some(PosixDst {
abbrev: "EDT".into(),
offset: None,
rule: None,
}),
},)
);
let tz = PosixTz::parse(":EST5EDT").unwrap();
assert_eq!(tz, PosixTz::Implementation("EST5EDT".into()));
assert!(PosixTz::parse(b":EST5\xFFEDT").is_err());
}
#[test]
fn parse_iana() {
let p = IanaTz::parse_v3plus("CRAZY5SHORT,M12.5.0/50,0/2").unwrap();
assert_eq!(
p,
IanaTz(ReasonablePosixTimeZone {
original: "CRAZY5SHORT,M12.5.0/50,0/2".into(),
std_abbrev: "CRAZY".into(),
std_offset: PosixOffset {
sign: None,
hour: C(5).rinto(),
minute: None,
second: None,
},
dst: Some(ReasonablePosixDst {
abbrev: "SHORT".into(),
offset: None,
rule: Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(
WeekdayOfMonth {
month: C(12).rinto(),
week: C(5).rinto(),
weekday: Weekday::Sunday,
},
),
time: Some(PosixTimeSpec {
sign: None,
hour: C(50).rinto(),
minute: None,
second: None,
},),
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianZero(C(0).rinto()),
time: Some(PosixTimeSpec {
sign: None,
hour: C(2).rinto(),
minute: None,
second: None,
},),
},
},
}),
}),
);
let p = Parser::new("America/New_York");
assert!(p.parse().is_err());
let p = Parser::new(":America/New_York");
assert!(p.parse().is_err());
}
#[test]
fn parse() {
let p = Parser::new("NZST-12NZDT,J60,J300");
assert_eq!(
p.parse().unwrap(),
PosixTimeZone {
original: "NZST-12NZDT,J60,J300".into(),
std_abbrev: "NZST".into(),
std_offset: PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(12).rinto(),
minute: None,
second: None,
},
dst: Some(PosixDst {
abbrev: "NZDT".into(),
offset: None,
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(60).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(300).rinto()),
time: None,
},
}),
}),
},
);
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(),
PosixTimeZone {
original: "NZST-12NZDT,M9.5.0,M4.1.0/3".into(),
std_abbrev: "NZST".into(),
std_offset: PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(12).rinto(),
minute: None,
second: None,
},
dst: Some(PosixDst {
abbrev: "NZDT".into(),
offset: None,
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(
WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Sunday,
}
),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(
WeekdayOfMonth {
month: C(4).rinto(),
week: C(1).rinto(),
weekday: Weekday::Sunday,
}
),
time: Some(PosixTimeSpec {
sign: None,
hour: C(3).rinto(),
minute: None,
second: None,
}),
},
})
}),
},
);
let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3WAT");
assert_eq!(
p.parse_posix_time_zone().unwrap(),
PosixTimeZone {
original: "NZST-12NZDT,M9.5.0,M4.1.0/3WAT".into(),
std_abbrev: "NZST".into(),
std_offset: PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(12).rinto(),
minute: None,
second: None,
},
dst: Some(PosixDst {
abbrev: "NZDT".into(),
offset: None,
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(
WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Sunday,
}
),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(
WeekdayOfMonth {
month: C(4).rinto(),
week: C(1).rinto(),
weekday: Weekday::Sunday,
}
),
time: Some(PosixTimeSpec {
sign: None,
hour: C(3).rinto(),
minute: None,
second: None,
}),
},
})
}),
},
);
let p = Parser::new("NZST-12NZDT,J60,J300");
assert_eq!(
p.parse_posix_time_zone().unwrap(),
PosixTimeZone {
original: "NZST-12NZDT,J60,J300".into(),
std_abbrev: "NZST".into(),
std_offset: PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(12).rinto(),
minute: None,
second: None,
},
dst: Some(PosixDst {
abbrev: "NZDT".into(),
offset: None,
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(60).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(300).rinto()),
time: None,
},
}),
}),
},
);
let p = Parser::new("NZST-12NZDT,J60,J300WAT");
assert_eq!(
p.parse_posix_time_zone().unwrap(),
PosixTimeZone {
original: "NZST-12NZDT,J60,J300WAT".into(),
std_abbrev: "NZST".into(),
std_offset: PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(12).rinto(),
minute: None,
second: None,
},
dst: Some(PosixDst {
abbrev: "NZDT".into(),
offset: None,
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(60).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(300).rinto()),
time: None,
},
}),
}),
},
);
}
#[test]
fn parse_posix_dst() {
let p = Parser::new("NZDT,M9.5.0,M4.1.0/3");
assert_eq!(
p.parse_posix_dst().unwrap(),
PosixDst {
abbrev: "NZDT".into(),
offset: None,
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Sunday,
}),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(4).rinto(),
week: C(1).rinto(),
weekday: Weekday::Sunday,
}),
time: Some(PosixTimeSpec {
sign: None,
hour: C(3).rinto(),
minute: None,
second: None,
}),
},
}),
},
);
let p = Parser::new("NZDT,J60,J300");
assert_eq!(
p.parse_posix_dst().unwrap(),
PosixDst {
abbrev: "NZDT".into(),
offset: None,
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(60).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(300).rinto()),
time: None,
},
}),
},
);
let p = Parser::new("NZDT-7,J60,J300");
assert_eq!(
p.parse_posix_dst().unwrap(),
PosixDst {
abbrev: "NZDT".into(),
offset: Some(PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(7).rinto(),
minute: None,
second: None,
}),
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(60).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(300).rinto()),
time: None,
},
}),
},
);
let p = Parser::new("NZDT+7,J60,J300");
assert_eq!(
p.parse_posix_dst().unwrap(),
PosixDst {
abbrev: "NZDT".into(),
offset: Some(PosixOffset {
sign: Some(Sign::N::<1>()),
hour: C(7).rinto(),
minute: None,
second: None,
}),
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(60).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(300).rinto()),
time: None,
},
}),
},
);
let p = Parser::new("NZDT7,J60,J300");
assert_eq!(
p.parse_posix_dst().unwrap(),
PosixDst {
abbrev: "NZDT".into(),
offset: Some(PosixOffset {
sign: None,
hour: C(7).rinto(),
minute: None,
second: None,
}),
rule: Some(Rule {
start: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(60).rinto()),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(300).rinto()),
time: None,
},
}),
},
);
let p = Parser::new("NZDT7,");
assert!(p.parse_posix_dst().is_err());
let p = Parser::new("NZDT7!");
assert!(p.parse_posix_dst().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(255);
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(256);
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(255));
let p = Parser::new(&tz);
assert_eq!(
&*p.parse_quoted_abbreviation().unwrap(),
tz.trim_right_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(256));
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(),
PosixOffset {
sign: None,
hour: C(5).rinto(),
minute: None,
second: None,
},
);
let p = Parser::new("+5");
assert_eq!(
p.parse_posix_offset().unwrap(),
PosixOffset {
sign: Some(Sign::N::<1>()),
hour: C(5).rinto(),
minute: None,
second: None,
},
);
let p = Parser::new("-5");
assert_eq!(
p.parse_posix_offset().unwrap(),
PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(5).rinto(),
minute: None,
second: None,
},
);
let p = Parser::new("-12:34:56");
assert_eq!(
p.parse_posix_offset().unwrap(),
PosixOffset {
sign: Some(Sign::N::<-1>()),
hour: C(12).rinto(),
minute: Some(C(34).rinto()),
second: Some(C(56).rinto()),
},
);
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: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Sunday,
}),
time: None,
},
end: PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(4).rinto(),
week: C(1).rinto(),
weekday: Weekday::Sunday,
}),
time: Some(PosixTimeSpec {
sign: None,
hour: C(3).rinto(),
minute: None,
second: None,
}),
},
},
);
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_spec() {
let p = Parser::new("J1");
assert_eq!(
p.parse_posix_datetime_spec().unwrap(),
PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(1).rinto()),
time: None,
},
);
let p = Parser::new("J1/3");
assert_eq!(
p.parse_posix_datetime_spec().unwrap(),
PosixDateTimeSpec {
date: PosixDateSpec::JulianOne(C(1).rinto()),
time: Some(PosixTimeSpec {
sign: None,
hour: C(3).rinto(),
minute: None,
second: None,
}),
},
);
let p = Parser::new("M4.1.0/3");
assert_eq!(
p.parse_posix_datetime_spec().unwrap(),
PosixDateTimeSpec {
date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(4).rinto(),
week: C(1).rinto(),
weekday: Weekday::Sunday,
}),
time: Some(PosixTimeSpec {
sign: None,
hour: C(3).rinto(),
minute: None,
second: None,
}),
},
);
let p = Parser::new("1/3:45:05");
assert_eq!(
p.parse_posix_datetime_spec().unwrap(),
PosixDateTimeSpec {
date: PosixDateSpec::JulianZero(C(1).rinto()),
time: Some(PosixTimeSpec {
sign: None,
hour: C(3).rinto(),
minute: Some(C(45).rinto()),
second: Some(C(5).rinto()),
}),
},
);
let p = Parser::new("a");
assert!(p.parse_posix_datetime_spec().is_err());
let p = Parser::new("J1/");
assert!(p.parse_posix_datetime_spec().is_err());
let p = Parser::new("1/");
assert!(p.parse_posix_datetime_spec().is_err());
let p = Parser::new("M4.1.0/");
assert!(p.parse_posix_datetime_spec().is_err());
}
#[test]
fn parse_posix_date_spec() {
let p = Parser::new("J1");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::JulianOne(C(1).rinto())
);
let p = Parser::new("J365");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::JulianOne(C(365).rinto())
);
let p = Parser::new("0");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::JulianZero(C(0).rinto())
);
let p = Parser::new("1");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::JulianZero(C(1).rinto())
);
let p = Parser::new("365");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::JulianZero(C(365).rinto())
);
let p = Parser::new("M9.5.0");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Sunday,
}),
);
let p = Parser::new("M9.5.6");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Saturday,
}),
);
let p = Parser::new("M09.5.6");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Saturday,
}),
);
let p = Parser::new("M12.1.1");
assert_eq!(
p.parse_posix_date_spec().unwrap(),
PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
month: C(12).rinto(),
week: C(1).rinto(),
weekday: Weekday::Monday,
}),
);
let p = Parser::new("a");
assert!(p.parse_posix_date_spec().is_err());
let p = Parser::new("j");
assert!(p.parse_posix_date_spec().is_err());
let p = Parser::new("m");
assert!(p.parse_posix_date_spec().is_err());
let p = Parser::new("n");
assert!(p.parse_posix_date_spec().is_err());
let p = Parser::new("J366");
assert!(p.parse_posix_date_spec().is_err());
let p = Parser::new("366");
assert!(p.parse_posix_date_spec().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(),
WeekdayOfMonth {
month: C(9).rinto(),
week: C(5).rinto(),
weekday: Weekday::Sunday,
},
);
let p = Parser::new("9.1.6");
assert_eq!(
p.parse_weekday_of_month().unwrap(),
WeekdayOfMonth {
month: C(9).rinto(),
week: C(1).rinto(),
weekday: Weekday::Saturday,
},
);
let p = Parser::new("09.1.6");
assert_eq!(
p.parse_weekday_of_month().unwrap(),
WeekdayOfMonth {
month: C(9).rinto(),
week: C(1).rinto(),
weekday: 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_spec() {
let p = Parser::new("5");
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: None,
hour: C(5).rinto(),
minute: None,
second: None
}
);
let p = Parser::new("22");
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: None,
hour: C(22).rinto(),
minute: None,
second: None
}
);
let p = Parser::new("02");
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: None,
hour: C(2).rinto(),
minute: None,
second: None
}
);
let p = Parser::new("5:45");
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: None,
hour: C(5).rinto(),
minute: Some(C(45).rinto()),
second: None
}
);
let p = Parser::new("5:45:12");
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: None,
hour: C(5).rinto(),
minute: Some(C(45).rinto()),
second: Some(C(12).rinto()),
}
);
let p = Parser::new("5:45:129");
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: None,
hour: C(5).rinto(),
minute: Some(C(45).rinto()),
second: Some(C(12).rinto()),
}
);
let p = Parser::new("5:45:12:");
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: None,
hour: C(5).rinto(),
minute: Some(C(45).rinto()),
second: Some(C(12).rinto()),
}
);
let p = Parser { ianav3plus: true, ..Parser::new("+5:45:12") };
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: Some(C(1).rinto()),
hour: C(5).rinto(),
minute: Some(C(45).rinto()),
second: Some(C(12).rinto()),
}
);
let p = Parser { ianav3plus: true, ..Parser::new("-5:45:12") };
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: Some(C(-1).rinto()),
hour: C(5).rinto(),
minute: Some(C(45).rinto()),
second: Some(C(12).rinto()),
}
);
let p = Parser { ianav3plus: true, ..Parser::new("-167:45:12") };
assert_eq!(
p.parse_posix_time_spec().unwrap(),
PosixTimeSpec {
sign: Some(C(-1).rinto()),
hour: C(167).rinto(),
minute: Some(C(45).rinto()),
second: Some(C(12).rinto()),
}
);
let p = Parser::new("25");
assert!(p.parse_posix_time_spec().is_err());
let p = Parser::new("12:2");
assert!(p.parse_posix_time_spec().is_err());
let p = Parser::new("12:");
assert!(p.parse_posix_time_spec().is_err());
let p = Parser::new("12:23:5");
assert!(p.parse_posix_time_spec().is_err());
let p = Parser::new("12:23:");
assert!(p.parse_posix_time_spec().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("168") };
assert!(p.parse_posix_time_spec().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("-168") };
assert!(p.parse_posix_time_spec().is_err());
let p = Parser { ianav3plus: true, ..Parser::new("+168") };
assert!(p.parse_posix_time_spec().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());
}
}