use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
use jiff::civil;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UtcOffset {
pub minutes: i16,
}
impl UtcOffset {
pub const UTC: Self = Self { minutes: 0 };
pub fn from_hhmm(sign: i8, hours: u8, minutes: u8) -> Option<Self> {
if !matches!(sign, 1 | -1) || hours > 23 || minutes > 59 {
return None;
}
let total = (hours as i16 * 60 + minutes as i16) * sign as i16;
Some(Self { minutes: total })
}
pub fn to_seconds(self) -> i32 {
self.minutes as i32 * 60
}
}
impl fmt::Display for UtcOffset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.minutes == 0 {
write!(f, "Z")
} else {
let sign = if self.minutes >= 0 { '+' } else { '-' };
let abs = self.minutes.unsigned_abs();
write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IsmDate {
Year(i32),
YearMonth(i32, u8),
Date(i32, u8, u8),
DateHourMin {
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
offset: Option<UtcOffset>,
},
DateTime {
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanosecond: u32,
offset: Option<UtcOffset>,
},
}
impl IsmDate {
#[inline]
pub fn year(&self) -> i32 {
match self {
IsmDate::Year(y) => *y,
IsmDate::YearMonth(y, _) => *y,
IsmDate::Date(y, _, _) => *y,
IsmDate::DateHourMin { year, .. } => *year,
IsmDate::DateTime { year, .. } => *year,
}
}
#[inline]
pub fn month(&self) -> Option<u8> {
match self {
IsmDate::Year(_) => None,
IsmDate::YearMonth(_, m) => Some(*m),
IsmDate::Date(_, m, _) => Some(*m),
IsmDate::DateHourMin { month, .. } => Some(*month),
IsmDate::DateTime { month, .. } => Some(*month),
}
}
#[inline]
pub fn day(&self) -> Option<u8> {
match self {
IsmDate::Year(_) | IsmDate::YearMonth(_, _) => None,
IsmDate::Date(_, _, d) => Some(*d),
IsmDate::DateHourMin { day, .. } => Some(*day),
IsmDate::DateTime { day, .. } => Some(*day),
}
}
pub fn contains(&self, point: &IsmDate) -> bool {
if self.year() != point.year() {
return false;
}
match self {
IsmDate::Year(_) => {
true
}
IsmDate::YearMonth(_, sm) => {
match point {
IsmDate::Year(_) => false,
_ => point.month() == Some(*sm),
}
}
IsmDate::Date(_, sm, sd) => {
match point {
IsmDate::Year(_) | IsmDate::YearMonth(_, _) => false,
_ => point.month() == Some(*sm) && point.day() == Some(*sd),
}
}
IsmDate::DateHourMin {
month: sm,
day: sd,
hour: sh,
minute: smin,
offset: soff,
..
} => {
match point {
IsmDate::DateHourMin {
month,
day,
hour,
minute,
offset,
..
} => month == sm && day == sd && hour == sh && minute == smin && offset == soff,
IsmDate::DateTime {
month,
day,
hour,
minute,
offset,
..
} => month == sm && day == sd && hour == sh && minute == smin && offset == soff,
_ => false,
}
}
IsmDate::DateTime {
month: sm,
day: sd,
hour: sh,
minute: smin,
second: ss,
nanosecond: sns,
offset: soff,
..
} => {
if let IsmDate::DateTime {
month,
day,
hour,
minute,
second,
nanosecond,
offset,
..
} = point
{
month == sm
&& day == sd
&& hour == sh
&& minute == smin
&& second == ss
&& nanosecond == sns
&& offset == soff
} else {
false
}
}
}
}
pub fn end_cmp(&self, other: &IsmDate) -> Ordering {
let a = self.end_components();
let b = other.end_components();
a.cmp(&b)
}
pub fn to_maxdate_str(&self) -> Box<str> {
let (y, m, d, _, _, _, _, _) = self.end_components();
format!("{:04}{:02}{:02}", y, m, d).into_boxed_str()
}
fn end_components(&self) -> (i32, u8, u8, u8, u8, u8, u32, i16) {
match self {
IsmDate::Year(y) => (*y, 12, 31, 23, 59, 59, 999_999_999, 0),
IsmDate::YearMonth(y, m) => {
let d = days_in_month(*y, *m);
(*y, *m, d, 23, 59, 59, 999_999_999, 0)
}
IsmDate::Date(y, m, d) => (*y, *m, *d, 23, 59, 59, 999_999_999, 0),
IsmDate::DateHourMin {
year,
month,
day,
hour,
minute,
offset,
} => {
let utc_tb = offset.map_or(0_i16, |o| -o.minutes);
(*year, *month, *day, *hour, *minute, 59, 999_999_999, utc_tb)
}
IsmDate::DateTime {
year,
month,
day,
hour,
minute,
second,
nanosecond,
offset,
} => {
let utc_tb = offset.map_or(0_i16, |o| -o.minutes);
(
*year,
*month,
*day,
*hour,
*minute,
*second,
*nanosecond,
utc_tb,
)
}
}
}
}
impl fmt::Display for IsmDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IsmDate::Year(y) => write!(f, "{:04}", y),
IsmDate::YearMonth(y, m) => write!(f, "{:04}-{:02}", y, m),
IsmDate::Date(y, m, d) => write!(f, "{:04}-{:02}-{:02}", y, m, d),
IsmDate::DateHourMin {
year,
month,
day,
hour,
minute,
offset,
} => {
write!(
f,
"{:04}-{:02}-{:02}T{:02}:{:02}",
year, month, day, hour, minute
)?;
if let Some(o) = offset {
write!(f, "{o}")?;
}
Ok(())
}
IsmDate::DateTime {
year,
month,
day,
hour,
minute,
second,
nanosecond,
offset,
} => {
write!(
f,
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
year, month, day, hour, minute, second
)?;
if *nanosecond > 0 {
let s = format!("{:09}", nanosecond);
let trimmed = s.trim_end_matches('0');
write!(f, ".{trimmed}")?;
}
if let Some(o) = offset {
write!(f, "{o}")?;
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseIsmDateError {
msg: &'static str,
}
impl fmt::Display for ParseIsmDateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid ISM date: {}", self.msg)
}
}
impl std::error::Error for ParseIsmDateError {}
impl ParseIsmDateError {
const fn new(msg: &'static str) -> Self {
Self { msg }
}
}
impl FromStr for IsmDate {
type Err = ParseIsmDateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_ism_date(s)
}
}
impl FromStr for UtcOffset {
type Err = ParseIsmDateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Z" => Ok(UtcOffset::UTC),
_ if (s.starts_with('+') || s.starts_with('-')) && s.len() == 6 => {
let b = s.as_bytes();
if b[3] != b':' {
return Err(ParseIsmDateError::new(
"UTC offset missing ':' separator (expected ±HH:MM)",
));
}
let sign: i8 = if s.starts_with('+') { 1 } else { -1 };
let oh =
parse_2digits(&b[1..3]).ok_or(ParseIsmDateError::new("invalid offset hour"))?;
let om = parse_2digits(&b[4..6])
.ok_or(ParseIsmDateError::new("invalid offset minute"))?;
UtcOffset::from_hhmm(sign, oh, om)
.ok_or(ParseIsmDateError::new("UTC offset out of range"))
}
_ => Err(ParseIsmDateError::new(
"unrecognized UTC offset (expected Z or ±HH:MM)",
)),
}
}
}
fn parse_ism_date(s: &str) -> Result<IsmDate, ParseIsmDateError> {
let bytes = s.as_bytes();
match bytes.len() {
4 if all_ascii_digits(bytes) => {
let y = parse_4digit_year(bytes)?;
Ok(IsmDate::Year(y))
}
8 if all_ascii_digits(bytes) => {
let y = parse_4digit_year(&bytes[0..4])?;
let m = parse_2digits(&bytes[4..6])
.ok_or(ParseIsmDateError::new("invalid month digits"))?;
let d =
parse_2digits(&bytes[6..8]).ok_or(ParseIsmDateError::new("invalid day digits"))?;
validate_date(y, m, d)?;
Ok(IsmDate::Date(y, m, d))
}
7 if bytes[4] == b'-' => {
let y = parse_4digit_year(&bytes[0..4])?;
let m = parse_2digits(&bytes[5..7])
.ok_or(ParseIsmDateError::new("invalid month digits"))?;
validate_year_month(y, m)?;
Ok(IsmDate::YearMonth(y, m))
}
10 if bytes[4] == b'-' && bytes[7] == b'-' => {
let y = parse_4digit_year(&bytes[0..4])?;
let m = parse_2digits(&bytes[5..7])
.ok_or(ParseIsmDateError::new("invalid month digits"))?;
let d =
parse_2digits(&bytes[8..10]).ok_or(ParseIsmDateError::new("invalid day digits"))?;
validate_date(y, m, d)?;
Ok(IsmDate::Date(y, m, d))
}
_ if bytes.len() >= 16
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes[10] == b'T'
&& bytes[13] == b':' =>
{
parse_datetime_or_hourmind(s)
}
_ => Err(ParseIsmDateError::new("unrecognized date format")),
}
}
fn parse_datetime_or_hourmind(s: &str) -> Result<IsmDate, ParseIsmDateError> {
if !s.is_ascii() {
return Err(ParseIsmDateError::new(
"date string contains non-ASCII characters",
));
}
let bytes = s.as_bytes();
let y = parse_4digit_year(&bytes[0..4])?;
let m = parse_2digits(&bytes[5..7]).ok_or(ParseIsmDateError::new("invalid month digits"))?;
let d = parse_2digits(&bytes[8..10]).ok_or(ParseIsmDateError::new("invalid day digits"))?;
validate_date(y, m, d)?;
let h = parse_2digits(&bytes[11..13]).ok_or(ParseIsmDateError::new("invalid hour digits"))?;
let min =
parse_2digits(&bytes[14..16]).ok_or(ParseIsmDateError::new("invalid minute digits"))?;
if h > 23 {
return Err(ParseIsmDateError::new("hour out of range"));
}
if min > 59 {
return Err(ParseIsmDateError::new("minute out of range"));
}
let rest = &s[16..];
if rest.is_empty() || rest.starts_with('Z') || rest.starts_with('+') || rest.starts_with('-') {
let offset = parse_offset(rest)?;
return Ok(IsmDate::DateHourMin {
year: y,
month: m,
day: d,
hour: h,
minute: min,
offset,
});
}
if !rest.starts_with(':') || rest.len() < 3 {
return Err(ParseIsmDateError::new("expected ':SS' in dateTime"));
}
let sec_bytes = &rest.as_bytes()[1..3];
let sec = parse_2digits(sec_bytes).ok_or(ParseIsmDateError::new("invalid second digits"))?;
if sec > 59 {
return Err(ParseIsmDateError::new("second out of range"));
}
let after_sec = &rest[3..];
let (nanosecond, after_frac) = if let Some(frac_str) = after_sec.strip_prefix('.') {
let digit_end = frac_str.bytes().take_while(|b| b.is_ascii_digit()).count();
if digit_end == 0 {
return Err(ParseIsmDateError::new("empty fractional seconds"));
}
let frac_digits = &frac_str[..digit_end];
let ns = parse_frac_as_nanoseconds(frac_digits)?;
(ns, &frac_str[digit_end..])
} else {
(0u32, after_sec)
};
let offset = parse_offset(after_frac)?;
Ok(IsmDate::DateTime {
year: y,
month: m,
day: d,
hour: h,
minute: min,
second: sec,
nanosecond,
offset,
})
}
fn parse_offset(s: &str) -> Result<Option<UtcOffset>, ParseIsmDateError> {
match s {
"" => Ok(None),
"Z" => Ok(Some(UtcOffset::UTC)),
_ if (s.starts_with('+') || s.starts_with('-')) && s.len() == 6 => {
let b = s.as_bytes();
if b[3] != b':' {
return Err(ParseIsmDateError::new(
"UTC offset missing ':' separator (expected ±HH:MM)",
));
}
let sign: i8 = if s.starts_with('+') { 1 } else { -1 };
let oh =
parse_2digits(&b[1..3]).ok_or(ParseIsmDateError::new("invalid offset hour"))?;
let om =
parse_2digits(&b[4..6]).ok_or(ParseIsmDateError::new("invalid offset minute"))?;
UtcOffset::from_hhmm(sign, oh, om)
.ok_or(ParseIsmDateError::new("UTC offset out of range"))
.map(Some)
}
_ => Err(ParseIsmDateError::new("unrecognized timezone suffix")),
}
}
fn parse_frac_as_nanoseconds(frac: &str) -> Result<u32, ParseIsmDateError> {
if frac.len() > 9 {
return Err(ParseIsmDateError::new(
"fractional seconds: more than 9 digits",
));
}
let mut padded = [b'0'; 9];
padded[..frac.len()].copy_from_slice(frac.as_bytes());
let ns: u32 = std::str::from_utf8(&padded)
.ok()
.and_then(|s| s.parse().ok())
.ok_or(ParseIsmDateError::new("fractional seconds not numeric"))?;
Ok(ns)
}
fn validate_date(year: i32, month: u8, day: u8) -> Result<(), ParseIsmDateError> {
let y = i16::try_from(year).map_err(|_| ParseIsmDateError::new("year out of i16 range"))?;
civil::Date::new(y, month as i8, day as i8)
.map_err(|_| ParseIsmDateError::new("invalid calendar date"))?;
Ok(())
}
fn validate_year_month(year: i32, month: u8) -> Result<(), ParseIsmDateError> {
if !(1..=12).contains(&month) {
return Err(ParseIsmDateError::new("month out of range 1–12"));
}
let _y = i16::try_from(year).map_err(|_| ParseIsmDateError::new("year out of i16 range"))?;
Ok(())
}
fn days_in_month(year: i32, month: u8) -> u8 {
let y = i16::try_from(year).unwrap_or(2000); civil::Date::new(y, month as i8, 1)
.map(|d| d.days_in_month() as u8)
.unwrap_or(30) }
fn parse_4digit_year(bytes: &[u8]) -> Result<i32, ParseIsmDateError> {
if bytes.len() != 4 || !all_ascii_digits(bytes) {
return Err(ParseIsmDateError::new("year must be exactly 4 digits"));
}
Ok(parse_digits_as_i32(bytes))
}
#[inline]
fn parse_2digits(bytes: &[u8]) -> Option<u8> {
if bytes.len() == 2 && bytes[0].is_ascii_digit() && bytes[1].is_ascii_digit() {
Some((bytes[0] - b'0') * 10 + (bytes[1] - b'0'))
} else {
None
}
}
#[inline]
fn parse_digits_as_i32(bytes: &[u8]) -> i32 {
bytes
.iter()
.fold(0i32, |acc, b| acc * 10 + (*b - b'0') as i32)
}
#[inline]
fn all_ascii_digits(bytes: &[u8]) -> bool {
bytes.iter().all(|b| b.is_ascii_digit())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ApproxQualifier {
FirstQtr,
SecondQtr,
ThirdQtr,
FourthQtr,
Circa,
Early,
Mid,
Late,
}
impl fmt::Display for ApproxQualifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ApproxQualifier::FirstQtr => "1st qtr",
ApproxQualifier::SecondQtr => "2nd qtr",
ApproxQualifier::ThirdQtr => "3rd qtr",
ApproxQualifier::FourthQtr => "4th qtr",
ApproxQualifier::Circa => "circa",
ApproxQualifier::Early => "early",
ApproxQualifier::Mid => "mid",
ApproxQualifier::Late => "late",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseApproxQualifierError;
impl fmt::Display for ParseApproxQualifierError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid approx qualifier")
}
}
impl std::error::Error for ParseApproxQualifierError {}
impl FromStr for ApproxQualifier {
type Err = ParseApproxQualifierError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"1st qtr" => Ok(ApproxQualifier::FirstQtr),
"2nd qtr" => Ok(ApproxQualifier::SecondQtr),
"3rd qtr" => Ok(ApproxQualifier::ThirdQtr),
"4th qtr" => Ok(ApproxQualifier::FourthQtr),
"circa" => Ok(ApproxQualifier::Circa),
"early" => Ok(ApproxQualifier::Early),
"mid" => Ok(ApproxQualifier::Mid),
"late" => Ok(ApproxQualifier::Late),
_ => Err(ParseApproxQualifierError),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ApproxIsmDate {
pub date: IsmDate,
pub qualifier: Option<ApproxQualifier>,
}
impl fmt::Display for ApproxIsmDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(q) = self.qualifier {
write!(f, "{} {}", q, self.date)
} else {
write!(f, "{}", self.date)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn round_trip(s: &str) -> bool {
IsmDate::from_str(s)
.map(|d| d.to_string() == s)
.unwrap_or(false)
}
#[test]
fn round_trip_year() {
assert!(round_trip("2003"));
assert!(round_trip("1900"));
assert!(round_trip("9999"));
}
#[test]
fn round_trip_year_month() {
assert!(round_trip("2003-04"));
assert!(round_trip("2003-12"));
assert!(round_trip("2003-01"));
}
#[test]
fn round_trip_date() {
assert!(round_trip("2003-04-15"));
assert!(round_trip("2000-02-29")); }
#[test]
fn round_trip_date_hour_min_utc() {
assert!(round_trip("2003-04-15T14:30Z"));
}
#[test]
fn round_trip_date_hour_min_offset() {
assert!(round_trip("2003-04-15T14:30-05:00"));
assert!(round_trip("2003-04-15T14:30+05:30"));
}
#[test]
fn round_trip_date_hour_min_floating() {
assert!(round_trip("2003-04-15T14:30"));
}
#[test]
fn round_trip_datetime_utc() {
assert!(round_trip("2003-04-15T14:30:00Z"));
}
#[test]
fn round_trip_datetime_with_millis() {
assert!(round_trip("2003-04-15T14:30:00.123Z"));
}
#[test]
fn round_trip_datetime_with_micros() {
assert!(round_trip("2003-04-15T14:30:00.123456Z"));
}
#[test]
fn round_trip_datetime_floating() {
assert!(round_trip("2003-04-15T14:30:00"));
}
#[test]
fn capco_yyyymmdd_parses_to_date() {
let d = IsmDate::from_str("20030415").unwrap();
assert_eq!(d, IsmDate::Date(2003, 4, 15));
}
#[test]
fn capco_year_only_parses_to_year() {
let d = IsmDate::from_str("2035").unwrap();
assert_eq!(d, IsmDate::Year(2035));
}
#[test]
fn capco_display_uses_iso_form() {
let d = IsmDate::from_str("20030415").unwrap();
assert_eq!(d.to_string(), "2003-04-15");
}
#[test]
fn rejects_invalid_month() {
assert!(IsmDate::from_str("2003-13").is_err());
assert!(IsmDate::from_str("2003-00").is_err());
}
#[test]
fn rejects_invalid_day() {
assert!(IsmDate::from_str("2003-02-29").is_err()); assert!(IsmDate::from_str("2003-04-31").is_err()); }
#[test]
fn accepts_leap_day_in_leap_year() {
assert!(IsmDate::from_str("2000-02-29").is_ok()); assert!(IsmDate::from_str("2004-02-29").is_ok()); }
#[test]
fn year_contains_same_year() {
let y = IsmDate::Year(2003);
assert!(y.contains(&IsmDate::Year(2003)));
}
#[test]
fn year_contains_year_month() {
let y = IsmDate::Year(2003);
assert!(y.contains(&IsmDate::YearMonth(2003, 4)));
assert!(!y.contains(&IsmDate::YearMonth(2004, 1)));
}
#[test]
fn year_contains_date() {
let y = IsmDate::Year(2003);
assert!(y.contains(&IsmDate::Date(2003, 12, 31)));
assert!(!y.contains(&IsmDate::Date(2004, 1, 1)));
}
#[test]
fn year_month_does_not_contain_year() {
let ym = IsmDate::YearMonth(2003, 4);
assert!(!ym.contains(&IsmDate::Year(2003)));
}
#[test]
fn year_month_contains_same_month_date() {
let ym = IsmDate::YearMonth(2003, 4);
assert!(ym.contains(&IsmDate::Date(2003, 4, 1)));
assert!(ym.contains(&IsmDate::Date(2003, 4, 30)));
assert!(!ym.contains(&IsmDate::Date(2003, 5, 1)));
}
#[test]
fn date_does_not_contain_coarser() {
let d = IsmDate::Date(2003, 4, 15);
assert!(!d.contains(&IsmDate::Year(2003)));
assert!(!d.contains(&IsmDate::YearMonth(2003, 4)));
}
#[test]
fn date_contains_self() {
let d = IsmDate::Date(2003, 4, 15);
assert!(d.contains(&IsmDate::Date(2003, 4, 15)));
}
#[test]
fn date_contains_hour_min_on_same_day() {
let d = IsmDate::Date(2003, 4, 15);
assert!(d.contains(&IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
}));
}
#[test]
fn date_does_not_contain_hour_min_different_day() {
let d = IsmDate::Date(2003, 4, 15);
assert!(!d.contains(&IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 16,
hour: 0,
minute: 0,
offset: None,
}));
}
#[test]
fn year_end_cmp_is_greater_than_mid_year_date() {
let year = IsmDate::Year(2003);
let mid = IsmDate::Date(2003, 6, 15);
assert_eq!(year.end_cmp(&mid), Ordering::Greater);
}
#[test]
fn year_month_end_cmp_greater_than_early_date_in_month() {
let ym = IsmDate::YearMonth(2003, 4); let d = IsmDate::Date(2003, 4, 1); assert_eq!(ym.end_cmp(&d), Ordering::Greater);
}
#[test]
fn date_end_cmp_greater_than_date_hour_min_same_day() {
let day = IsmDate::Date(2003, 4, 15);
let t = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 22,
minute: 30,
offset: None,
};
assert_eq!(day.end_cmp(&t), Ordering::Greater);
}
#[test]
fn date_hour_min_end_cmp_later_time_is_greater() {
let earlier = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 0,
offset: None,
};
let later = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
};
assert_eq!(later.end_cmp(&earlier), Ordering::Greater);
assert_eq!(earlier.end_cmp(&later), Ordering::Less);
}
#[test]
fn date_hour_min_end_cmp_equal_times_is_equal() {
let a = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
};
let b = a.clone();
assert_eq!(a.end_cmp(&b), Ordering::Equal);
}
#[test]
fn date_hour_min_end_cmp_same_civil_negative_offset_is_greater() {
let utc = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: Some(UtcOffset::UTC),
};
let eastern = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: Some(UtcOffset::from_hhmm(-1, 5, 0).unwrap()), };
assert_eq!(eastern.end_cmp(&utc), Ordering::Greater);
assert_eq!(utc.end_cmp(&eastern), Ordering::Less);
}
#[test]
fn date_hour_min_end_cmp_same_civil_positive_offset_is_less() {
let utc = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: Some(UtcOffset::UTC),
};
let india = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: Some(UtcOffset::from_hhmm(1, 5, 30).unwrap()), };
assert_eq!(india.end_cmp(&utc), Ordering::Less);
assert_eq!(utc.end_cmp(&india), Ordering::Greater);
}
#[test]
fn to_maxdate_str_year() {
assert_eq!(&*IsmDate::Year(2003).to_maxdate_str(), "20031231");
}
#[test]
fn to_maxdate_str_year_month_april() {
assert_eq!(&*IsmDate::YearMonth(2003, 4).to_maxdate_str(), "20030430");
}
#[test]
fn to_maxdate_str_year_month_february_non_leap() {
assert_eq!(&*IsmDate::YearMonth(2003, 2).to_maxdate_str(), "20030228");
}
#[test]
fn to_maxdate_str_year_month_february_leap() {
assert_eq!(&*IsmDate::YearMonth(2000, 2).to_maxdate_str(), "20000229");
}
#[test]
fn to_maxdate_str_date() {
assert_eq!(&*IsmDate::Date(2003, 4, 15).to_maxdate_str(), "20030415");
}
#[test]
fn approx_qualifier_round_trip() {
for q in [
ApproxQualifier::FirstQtr,
ApproxQualifier::SecondQtr,
ApproxQualifier::ThirdQtr,
ApproxQualifier::FourthQtr,
ApproxQualifier::Circa,
ApproxQualifier::Early,
ApproxQualifier::Mid,
ApproxQualifier::Late,
] {
let s = q.to_string();
assert_eq!(ApproxQualifier::from_str(&s).unwrap(), q);
}
}
#[test]
fn utc_offset_display_utc() {
assert_eq!(UtcOffset::UTC.to_string(), "Z");
}
#[test]
fn utc_offset_display_positive() {
let o = UtcOffset::from_hhmm(1, 5, 30).unwrap();
assert_eq!(o.to_string(), "+05:30");
}
#[test]
fn utc_offset_display_negative() {
let o = UtcOffset::from_hhmm(-1, 5, 0).unwrap();
assert_eq!(o.to_string(), "-05:00");
}
#[test]
fn utc_offset_rejects_invalid() {
assert!(UtcOffset::from_hhmm(1, 24, 0).is_none()); assert!(UtcOffset::from_hhmm(1, 0, 60).is_none()); }
#[test]
fn utc_offset_from_str_z_is_utc() {
assert_eq!("Z".parse::<UtcOffset>().unwrap(), UtcOffset::UTC);
}
#[test]
fn utc_offset_from_str_positive() {
let o = "+05:30".parse::<UtcOffset>().unwrap();
assert_eq!(o, UtcOffset::from_hhmm(1, 5, 30).unwrap());
}
#[test]
fn utc_offset_from_str_negative() {
let o = "-05:00".parse::<UtcOffset>().unwrap();
assert_eq!(o, UtcOffset::from_hhmm(-1, 5, 0).unwrap());
}
#[test]
fn utc_offset_from_str_round_trip() {
for s in ["Z", "+05:30", "-05:00", "+23:59", "-23:59"] {
let parsed: UtcOffset = s.parse().unwrap();
assert_eq!(parsed.to_string(), s, "round-trip failed for {s:?}");
}
let zero: UtcOffset = "+00:00".parse().unwrap();
assert_eq!(zero, UtcOffset::UTC);
assert_eq!(zero.to_string(), "Z");
}
#[test]
fn utc_offset_from_str_rejects_invalid() {
for bad in [
"EST", "UTC", "utc", "+0530", "+05-30", "05:30", "", "+24:00",
] {
assert!(bad.parse::<UtcOffset>().is_err(), "should reject {bad:?}");
}
}
#[test]
fn parse_offset_rejects_wrong_separator() {
let err = IsmDate::from_str("2003-04-15T10:30+05-30");
assert!(
err.is_err(),
"offset with wrong separator should be Err, got {err:?}"
);
let err2 = IsmDate::from_str("2003-04-15T10:30+0530");
assert!(
err2.is_err(),
"offset without separator should be Err, got {err2:?}"
);
}
#[test]
fn parse_datetime_rejects_non_ascii() {
let result = IsmDate::from_str("2003-04-15T10:30\u{00E9}");
assert!(result.is_err(), "non-ASCII should be Err, got {result:?}");
}
#[test]
fn utc_offset_from_hhmm_invalid_sign_zero() {
assert!(
UtcOffset::from_hhmm(0, 5, 0).is_none(),
"sign=0 must be rejected"
);
}
#[test]
fn utc_offset_from_hhmm_invalid_sign_two() {
assert!(
UtcOffset::from_hhmm(2, 5, 0).is_none(),
"sign=2 must be rejected"
);
}
#[test]
fn utc_offset_from_hhmm_invalid_sign_minus_two() {
assert!(
UtcOffset::from_hhmm(-2, 5, 0).is_none(),
"sign=-2 must be rejected"
);
}
#[test]
fn utc_offset_from_hhmm_max_valid_boundary() {
let pos = UtcOffset::from_hhmm(1, 23, 59).unwrap();
assert_eq!(pos.minutes, 23 * 60 + 59);
let neg = UtcOffset::from_hhmm(-1, 23, 59).unwrap();
assert_eq!(neg.minutes, -(23 * 60 + 59));
}
#[test]
fn utc_offset_from_hhmm_rejects_hours_24() {
assert!(
UtcOffset::from_hhmm(1, 24, 0).is_none(),
"hours=24 must be rejected"
);
}
#[test]
fn utc_offset_from_hhmm_rejects_minutes_60() {
assert!(
UtcOffset::from_hhmm(1, 0, 60).is_none(),
"minutes=60 must be rejected"
);
}
#[test]
fn utc_offset_to_seconds_utc_is_zero() {
assert_eq!(UtcOffset::UTC.to_seconds(), 0);
}
#[test]
fn utc_offset_to_seconds_positive() {
let o = UtcOffset::from_hhmm(1, 5, 30).unwrap();
assert_eq!(o.to_seconds(), 5 * 3600 + 30 * 60);
}
#[test]
fn utc_offset_to_seconds_negative() {
let o = UtcOffset::from_hhmm(-1, 5, 0).unwrap();
assert_eq!(o.to_seconds(), -5 * 3600);
}
#[test]
fn utc_offset_from_hhmm_zero_positive_sign() {
let pos = UtcOffset::from_hhmm(1, 0, 0).unwrap();
let neg = UtcOffset::from_hhmm(-1, 0, 0).unwrap();
assert_eq!(pos, UtcOffset::UTC);
assert_eq!(neg, UtcOffset::UTC);
}
#[test]
fn year_accessor_all_variants() {
assert_eq!(IsmDate::Year(2003).year(), 2003);
assert_eq!(IsmDate::YearMonth(2003, 4).year(), 2003);
assert_eq!(IsmDate::Date(2003, 4, 15).year(), 2003);
assert_eq!(
IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: None,
}
.year(),
2003
);
assert_eq!(
IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 0,
nanosecond: 0,
offset: None,
}
.year(),
2003
);
}
#[test]
fn month_accessor_all_variants() {
assert_eq!(IsmDate::Year(2003).month(), None);
assert_eq!(IsmDate::YearMonth(2003, 4).month(), Some(4));
assert_eq!(IsmDate::Date(2003, 4, 15).month(), Some(4));
assert_eq!(
IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: None,
}
.month(),
Some(4)
);
assert_eq!(
IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 0,
nanosecond: 0,
offset: None,
}
.month(),
Some(4)
);
}
#[test]
fn day_accessor_all_variants() {
assert_eq!(IsmDate::Year(2003).day(), None);
assert_eq!(IsmDate::YearMonth(2003, 4).day(), None);
assert_eq!(IsmDate::Date(2003, 4, 15).day(), Some(15));
assert_eq!(
IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: None,
}
.day(),
Some(15)
);
assert_eq!(
IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 0,
nanosecond: 0,
offset: None,
}
.day(),
Some(15)
);
}
#[test]
fn date_hour_min_contains_itself() {
let t = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: Some(UtcOffset::UTC),
};
assert!(t.contains(&t.clone()));
}
#[test]
fn date_hour_min_does_not_contain_coarser() {
let t = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
};
assert!(!t.contains(&IsmDate::Year(2003)));
assert!(!t.contains(&IsmDate::YearMonth(2003, 4)));
assert!(!t.contains(&IsmDate::Date(2003, 4, 15)));
}
#[test]
fn date_hour_min_contains_datetime_same_minute() {
let dhm = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
};
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
second: 45,
nanosecond: 0,
offset: None,
};
assert!(dhm.contains(&dt));
}
#[test]
fn date_hour_min_does_not_contain_datetime_different_minute() {
let dhm = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
};
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 31,
second: 0,
nanosecond: 0,
offset: None,
};
assert!(!dhm.contains(&dt));
}
#[test]
fn date_hour_min_does_not_contain_datetime_different_offset() {
let dhm = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: Some(UtcOffset::UTC),
};
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
second: 0,
nanosecond: 0,
offset: Some(UtcOffset::from_hhmm(-1, 5, 0).unwrap()),
};
assert!(!dhm.contains(&dt));
}
#[test]
fn datetime_contains_itself() {
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
second: 45,
nanosecond: 123_456_789,
offset: Some(UtcOffset::UTC),
};
assert!(dt.contains(&dt.clone()));
}
#[test]
fn datetime_does_not_contain_coarser() {
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
second: 45,
nanosecond: 0,
offset: None,
};
assert!(!dt.contains(&IsmDate::Year(2003)));
assert!(!dt.contains(&IsmDate::YearMonth(2003, 4)));
assert!(!dt.contains(&IsmDate::Date(2003, 4, 15)));
}
#[test]
fn datetime_does_not_contain_datehourmin() {
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
second: 45,
nanosecond: 0,
offset: None,
};
let dhm = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
};
assert!(!dt.contains(&dhm));
}
#[test]
fn year_contains_datehourmin_same_year() {
let y = IsmDate::Year(2003);
let t = IsmDate::DateHourMin {
year: 2003,
month: 6,
day: 15,
hour: 10,
minute: 0,
offset: None,
};
assert!(y.contains(&t));
}
#[test]
fn year_contains_datetime_same_year() {
let y = IsmDate::Year(2003);
let dt = IsmDate::DateTime {
year: 2003,
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 59,
nanosecond: 0,
offset: None,
};
assert!(y.contains(&dt));
}
#[test]
fn year_does_not_contain_datehourmin_different_year() {
let y = IsmDate::Year(2003);
let t = IsmDate::DateHourMin {
year: 2004,
month: 1,
day: 1,
hour: 0,
minute: 0,
offset: None,
};
assert!(!y.contains(&t));
}
#[test]
fn year_month_contains_datehourmin_same_month() {
let ym = IsmDate::YearMonth(2003, 4);
let t = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 0,
offset: None,
};
assert!(ym.contains(&t));
}
#[test]
fn year_month_does_not_contain_datehourmin_different_month() {
let ym = IsmDate::YearMonth(2003, 4);
let t = IsmDate::DateHourMin {
year: 2003,
month: 5,
day: 1,
hour: 0,
minute: 0,
offset: None,
};
assert!(!ym.contains(&t));
}
#[test]
fn date_contains_datetime_same_day() {
let d = IsmDate::Date(2003, 4, 15);
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 23,
minute: 59,
second: 59,
nanosecond: 999_999_999,
offset: None,
};
assert!(d.contains(&dt));
}
#[test]
fn date_does_not_contain_datetime_different_day() {
let d = IsmDate::Date(2003, 4, 15);
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 16,
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
};
assert!(!d.contains(&dt));
}
#[test]
fn year_end_cmp_same_year_is_equal() {
assert_eq!(
IsmDate::Year(2003).end_cmp(&IsmDate::Year(2003)),
Ordering::Equal
);
}
#[test]
fn year_end_cmp_different_years() {
assert_eq!(
IsmDate::Year(2004).end_cmp(&IsmDate::Year(2003)),
Ordering::Greater
);
assert_eq!(
IsmDate::Year(2003).end_cmp(&IsmDate::Year(2004)),
Ordering::Less
);
}
#[test]
fn year_month_end_cmp_same_month_is_equal() {
assert_eq!(
IsmDate::YearMonth(2003, 4).end_cmp(&IsmDate::YearMonth(2003, 4)),
Ordering::Equal
);
}
#[test]
fn year_month_end_cmp_different_months_same_year() {
assert_eq!(
IsmDate::YearMonth(2003, 5).end_cmp(&IsmDate::YearMonth(2003, 4)),
Ordering::Greater
);
assert_eq!(
IsmDate::YearMonth(2003, 4).end_cmp(&IsmDate::YearMonth(2003, 5)),
Ordering::Less
);
}
#[test]
fn date_end_cmp_same_date_is_equal() {
assert_eq!(
IsmDate::Date(2003, 4, 15).end_cmp(&IsmDate::Date(2003, 4, 15)),
Ordering::Equal
);
}
#[test]
fn date_end_cmp_later_date_is_greater() {
assert_eq!(
IsmDate::Date(2003, 4, 16).end_cmp(&IsmDate::Date(2003, 4, 15)),
Ordering::Greater
);
}
#[test]
fn datetime_end_cmp_same_instant_is_equal() {
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 45,
nanosecond: 0,
offset: None,
};
assert_eq!(dt.end_cmp(&dt.clone()), Ordering::Equal);
}
#[test]
fn datetime_end_cmp_later_second_is_greater() {
let earlier = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 44,
nanosecond: 0,
offset: None,
};
let later = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 45,
nanosecond: 0,
offset: None,
};
assert_eq!(later.end_cmp(&earlier), Ordering::Greater);
assert_eq!(earlier.end_cmp(&later), Ordering::Less);
}
#[test]
fn datetime_end_cmp_nanosecond_tiebreak() {
let a = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 45,
nanosecond: 0,
offset: None,
};
let b = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
second: 45,
nanosecond: 1,
offset: None,
};
assert_eq!(b.end_cmp(&a), Ordering::Greater);
}
#[test]
fn date_hour_min_floating_is_treated_as_offset_zero() {
let floating = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: None,
};
let utc = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 10,
minute: 30,
offset: Some(UtcOffset::UTC),
};
assert_eq!(floating.end_cmp(&utc), Ordering::Equal);
}
#[test]
fn year_end_cmp_vs_year_month_same_year() {
assert_eq!(
IsmDate::Year(2003).end_cmp(&IsmDate::YearMonth(2003, 6)),
Ordering::Greater
);
assert_eq!(
IsmDate::YearMonth(2003, 12).end_cmp(&IsmDate::Year(2003)),
Ordering::Equal );
}
#[test]
fn to_maxdate_str_date_hour_min() {
let t = IsmDate::DateHourMin {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
offset: None,
};
assert_eq!(&*t.to_maxdate_str(), "20030415");
}
#[test]
fn to_maxdate_str_datetime() {
let dt = IsmDate::DateTime {
year: 2003,
month: 4,
day: 15,
hour: 14,
minute: 30,
second: 45,
nanosecond: 0,
offset: Some(UtcOffset::UTC),
};
assert_eq!(&*dt.to_maxdate_str(), "20030415");
}
#[test]
fn to_maxdate_str_all_months_days_in_month() {
let expected = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for (i, &days) in expected.iter().enumerate() {
let month = (i + 1) as u8;
let ym = IsmDate::YearMonth(2003, month);
let s = ym.to_maxdate_str();
let day_part: u8 = s[6..].parse().unwrap();
assert_eq!(
day_part, days,
"2003-{month:02} should end on day {days}, got {day_part}"
);
}
}
#[test]
fn to_maxdate_str_february_leap_year() {
assert_eq!(&*IsmDate::YearMonth(2000, 2).to_maxdate_str(), "20000229");
assert_eq!(&*IsmDate::YearMonth(1900, 2).to_maxdate_str(), "19000228");
}
#[test]
fn approx_ism_date_display_without_qualifier() {
let a = ApproxIsmDate {
date: IsmDate::Year(2003),
qualifier: None,
};
assert_eq!(a.to_string(), "2003");
}
#[test]
fn approx_ism_date_display_with_qualifier() {
let a = ApproxIsmDate {
date: IsmDate::Year(1995),
qualifier: Some(ApproxQualifier::Circa),
};
assert_eq!(a.to_string(), "circa 1995");
}
#[test]
fn approx_ism_date_display_all_qualifiers() {
let pairs = [
(ApproxQualifier::FirstQtr, "1st qtr 2003"),
(ApproxQualifier::SecondQtr, "2nd qtr 2003"),
(ApproxQualifier::ThirdQtr, "3rd qtr 2003"),
(ApproxQualifier::FourthQtr, "4th qtr 2003"),
(ApproxQualifier::Circa, "circa 2003"),
(ApproxQualifier::Early, "early 2003"),
(ApproxQualifier::Mid, "mid 2003"),
(ApproxQualifier::Late, "late 2003"),
];
for (qualifier, expected) in pairs {
let a = ApproxIsmDate {
date: IsmDate::Year(2003),
qualifier: Some(qualifier),
};
assert_eq!(a.to_string(), expected, "qualifier={qualifier:?}");
}
}
#[test]
fn parse_ism_date_error_display() {
let err = IsmDate::from_str("not-a-date").unwrap_err();
let s = err.to_string();
assert!(
s.contains("invalid ISM date"),
"error display should mention 'invalid ISM date', got: {s:?}"
);
}
#[test]
fn parse_approx_qualifier_error_display() {
let err = ApproxQualifier::from_str("bogus").unwrap_err();
let s = err.to_string();
assert!(
s.contains("invalid approx qualifier"),
"error display should mention 'invalid approx qualifier', got: {s:?}"
);
}
#[test]
fn rejects_short_strings() {
for s in ["", "2", "20", "200", "20030"] {
assert!(
IsmDate::from_str(s).is_err(),
"should reject short string {s:?}"
);
}
}
#[test]
fn rejects_nine_char_string() {
assert!(IsmDate::from_str("200304150").is_err());
}
#[test]
fn rejects_day_zero_in_date() {
assert!(IsmDate::from_str("2003-04-00").is_err());
}
#[test]
fn rejects_day_32_in_date() {
assert!(IsmDate::from_str("2003-01-32").is_err());
}
#[test]
fn rejects_yyyymmdd_month_13() {
assert!(IsmDate::from_str("20031301").is_err());
}
#[test]
fn rejects_yyyymmdd_day_00() {
assert!(IsmDate::from_str("20030400").is_err());
}
#[test]
fn rejects_datehourmin_hour_out_of_range() {
assert!(IsmDate::from_str("2003-04-15T24:00").is_err());
assert!(IsmDate::from_str("2003-04-15T25:00Z").is_err());
}
#[test]
fn rejects_datehourmin_minute_out_of_range() {
assert!(IsmDate::from_str("2003-04-15T10:60").is_err());
assert!(IsmDate::from_str("2003-04-15T10:99Z").is_err());
}
#[test]
fn rejects_datetime_second_out_of_range() {
assert!(IsmDate::from_str("2003-04-15T10:30:60Z").is_err());
assert!(IsmDate::from_str("2003-04-15T10:30:99").is_err());
}
#[test]
fn rejects_fractional_seconds_empty() {
assert!(IsmDate::from_str("2003-04-15T10:30:00.Z").is_err());
assert!(IsmDate::from_str("2003-04-15T10:30:00.").is_err());
}
#[test]
fn rejects_fractional_seconds_too_many_digits() {
assert!(IsmDate::from_str("2003-04-15T10:30:00.1234567890Z").is_err());
}
#[test]
fn accepts_fractional_seconds_9_digits() {
assert!(IsmDate::from_str("2003-04-15T10:30:00.123456789Z").is_ok());
}
#[test]
fn rejects_bad_offset_in_datetime() {
assert!(IsmDate::from_str("2003-04-15T10:30:00+99:99").is_err());
assert!(IsmDate::from_str("2003-04-15T10:30:00+24:00").is_err());
}
#[test]
fn rejects_bad_offset_in_datehourmin() {
assert!(IsmDate::from_str("2003-04-15T10:30+99:99").is_err());
assert!(IsmDate::from_str("2003-04-15T10:30+24:00").is_err());
}
#[test]
fn rejects_unknown_suffix_after_datehourmin() {
assert!(IsmDate::from_str("2003-04-15T10:30:garbage").is_err());
}
#[test]
fn rejects_year_with_non_digit_separator() {
assert!(IsmDate::from_str("2003X04").is_err());
}
#[test]
fn rejects_date_with_wrong_separator() {
assert!(IsmDate::from_str("2003/04/15").is_err());
}
#[test]
fn round_trip_datetime_with_nanos() {
assert!(round_trip("2003-04-15T14:30:00.123456789Z"));
}
#[test]
fn round_trip_datetime_with_negative_offset() {
assert!(round_trip("2003-04-15T14:30:00-05:00"));
}
#[test]
fn round_trip_date_hour_min_negative_offset() {
assert!(round_trip("2003-04-15T14:30-07:00"));
}
#[test]
fn round_trip_year_month_january() {
assert!(round_trip("2003-01"));
}
#[test]
fn round_trip_year_month_december() {
assert!(round_trip("2003-12"));
}
#[test]
fn capco_yyyymmdd_rejects_invalid_calendar_date() {
assert!(IsmDate::from_str("20031301").is_err());
assert!(IsmDate::from_str("20030400").is_err());
assert!(IsmDate::from_str("20030229").is_err());
}
#[test]
fn utc_offset_from_str_all_canonical_forms() {
let o: UtcOffset = "+12:00".parse().unwrap();
assert_eq!(o.minutes, 720);
assert_eq!(o.to_string(), "+12:00");
let o: UtcOffset = "-12:00".parse().unwrap();
assert_eq!(o.minutes, -720);
assert_eq!(o.to_string(), "-12:00");
}
}