use jiff::{civil, Span, ToSpan, Zoned};
use super::{date, epoch, error, offset, relative, time, weekday, year, Item};
use crate::extended::{DateParts, TimeParts};
use crate::{ExtendedDateTime, ParsedDateTime};
#[derive(Debug, Default, Clone)]
pub(crate) struct DateTimeBuilder {
base: Option<Zoned>,
timestamp: Option<epoch::Timestamp>,
date: Option<date::Date>,
time: Option<time::Time>,
weekday: Option<weekday::Weekday>,
offset: Option<offset::Offset>,
timezone: Option<jiff::tz::TimeZone>,
relative: Vec<relative::Relative>,
}
impl DateTimeBuilder {
pub(super) fn new() -> Self {
Self::default()
}
pub(super) fn set_base(mut self, base: Zoned) -> Self {
self.base = Some(base);
self
}
fn set_timezone(mut self, tz: jiff::tz::TimeZone) -> Result<Self, &'static str> {
if self.timezone.is_some() {
return Err("timezone rule cannot appear more than once");
}
self.timezone = Some(tz);
Ok(self)
}
pub(super) fn set_timestamp(mut self, ts: epoch::Timestamp) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot appear more than once");
} else if self.date.is_some()
|| self.time.is_some()
|| self.weekday.is_some()
|| self.offset.is_some()
|| !self.relative.is_empty()
{
return Err("timestamp cannot be combined with other date/time items");
}
self.timestamp = Some(ts);
Ok(self)
}
fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.date.is_some() {
return Err("date cannot appear more than once");
}
self.date = Some(date);
Ok(self)
}
fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.time.is_some() {
return Err("time cannot appear more than once");
} else if self.offset.is_some() && time.offset.is_some() {
return Err("time offset and timezone are mutually exclusive");
}
self.time = Some(time);
Ok(self)
}
fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.weekday.is_some() {
return Err("weekday cannot appear more than once");
}
self.weekday = Some(weekday);
Ok(self)
}
fn set_offset(mut self, timezone: offset::Offset) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.offset.is_some()
|| self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some()
{
return Err("time offset cannot appear more than once");
}
self.offset = Some(timezone);
Ok(self)
}
fn push_relative(mut self, relative: relative::Relative) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
}
self.relative.push(relative);
Ok(self)
}
fn set_pure(mut self, pure: String) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
}
if let Some(date) = self.date.as_mut() {
if date.year.is_none() {
date.year = Some(year::year_from_str(&pure)?);
return Ok(self);
}
}
let (mut hour_str, mut minute_str) = match pure.len() {
1..=2 => (pure.as_str(), "0"),
3..=4 => pure.split_at(pure.len() - 2),
_ => {
return Err("pure number must be 1-4 digits when interpreted as time");
}
};
let hour = time::hour24(&mut hour_str).map_err(|_| "invalid hour in pure number")?;
let minute = time::minute(&mut minute_str).map_err(|_| "invalid minute in pure number")?;
let time = time::Time {
hour,
minute,
..Default::default()
};
self.set_time(time)
}
pub(super) fn build(self) -> Result<ParsedDateTime, error::Error> {
if let Some(date) = self.date.as_ref() {
if date.year.unwrap_or(0) > 9999 {
return self.build_extended();
}
}
if !self.should_try_extended_fallback() {
return self.build_in_range().map(ParsedDateTime::InRange);
}
match self.clone().build_in_range() {
Ok(dt) => Ok(ParsedDateTime::InRange(dt)),
Err(in_range_err) => self.build_extended().or(Err(in_range_err)),
}
}
fn should_try_extended_fallback(&self) -> bool {
if self.timestamp.is_some() {
return false;
}
self.date
.as_ref()
.is_some_and(|d| d.year.unwrap_or(0) >= 9999)
|| self.base.as_ref().is_some_and(|b| b.year() >= 9999)
}
fn build_in_range(self) -> Result<Zoned, error::Error> {
let has_timezone = self.timezone.is_some();
let base = match (self.base, self.timezone) {
(Some(b), Some(tz)) => b.timestamp().to_zoned(tz),
(Some(b), None) => b,
(None, Some(tz)) => jiff::Timestamp::now().to_zoned(tz),
(None, None) => Zoned::now(),
};
if let Some(ts) = self.timestamp {
let ts = jiff::Timestamp::try_from(ts)?;
return Ok(ts.to_zoned(base.offset().to_time_zone()));
}
let need_midnight = self.date.is_some()
|| self.time.is_some()
|| self.weekday.is_some()
|| self.offset.is_some()
|| has_timezone;
let mut dt = if need_midnight {
base.with().time(civil::time(0, 0, 0, 0)).build()?
} else {
base
};
if let Some(date) = self.date {
let d: civil::Date = if date.year.is_some() {
date.try_into()?
} else {
let base_year = u32::try_from(dt.date().year())
.map_err(|_| "base year must be non-negative")?;
date.with_year(base_year).try_into()?
};
dt = dt.with().date(d).build()?;
}
if let Some(time) = self.time.clone() {
if let Some(offset) = &time.offset {
dt = dt.datetime().to_zoned(offset.try_into()?)?;
}
let t: civil::Time = time.try_into()?;
dt = dt.with().time(t).build()?;
}
if let Some(weekday::Weekday { mut offset, day }) = self.weekday {
if self.time.is_none() {
dt = dt.with().time(civil::time(0, 0, 0, 0)).build()?;
}
let target = day.into();
if dt.date().weekday() != target && offset > 0 {
offset -= 1;
}
let delta = (target.since(civil::Weekday::Monday) as i32
- dt.date().weekday().since(civil::Weekday::Monday) as i32)
.rem_euclid(7)
+ offset.checked_mul(7).ok_or("multiplication overflow")?;
dt = dt.checked_add(Span::new().try_days(delta)?)?;
}
if let Some(offset) = self.offset {
let (offset, hour_adjustment) = offset.normalize();
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
}
for rel in self.relative {
dt = match rel {
relative::Relative::Years(_) | relative::Relative::Months(_) => {
let original_day_of_month = dt.day();
dt = dt.checked_add::<Span>(rel.try_into()?)?;
if original_day_of_month != dt.day() {
dt = dt.checked_add(
(original_day_of_month.checked_sub(dt.day()).unwrap_or(0)).days(),
)?;
}
dt
}
_ => dt.checked_add::<Span>(rel.try_into()?)?,
};
}
Ok(dt)
}
fn build_extended(self) -> Result<ParsedDateTime, error::Error> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with large years".into());
}
let DateTimeBuilder {
base,
timestamp: _,
date,
time,
weekday,
offset,
timezone,
relative,
} = self;
let has_timezone = timezone.is_some();
let base = match (base, timezone) {
(Some(b), Some(tz)) => b.timestamp().to_zoned(tz),
(Some(b), None) => b,
(None, Some(tz)) => jiff::Timestamp::now().to_zoned(tz),
(None, None) => Zoned::now(),
};
let rule_tz = base.time_zone().clone();
let need_midnight = date.is_some()
|| time.is_some()
|| weekday.is_some()
|| offset.is_some()
|| has_timezone;
let mut dt = ExtendedDateTime::new(
DateParts {
year: u32::try_from(base.year()).map_err(|_| "year must be non-negative")?,
month: base.month() as u8,
day: base.day() as u8,
},
TimeParts {
hour: if need_midnight { 0 } else { base.hour() as u8 },
minute: if need_midnight {
0
} else {
base.minute() as u8
},
second: if need_midnight {
0
} else {
base.second() as u8
},
nanosecond: if need_midnight {
0
} else {
base.subsec_nanosecond() as u32
},
},
base.offset().seconds(),
)?;
if let Some(date) = date {
let year = date.year.unwrap_or(dt.year);
dt = dt.with_date(year, date.month, date.day)?;
}
let had_time_item = time.is_some();
let has_time_offset = time.as_ref().and_then(|t| t.offset.as_ref()).is_some();
if let Some(time) = time {
if let Some(offset) = time.offset {
dt = dt.with_offset(offset.total_seconds())?;
}
dt = dt.with_time(time.hour, time.minute, time.second, time.nanosecond)?;
}
if let Some(weekday::Weekday {
mut offset,
day: target_day,
}) = weekday
{
if !had_time_item {
dt = dt.with_time(0, 0, 0, 0)?;
}
let target = weekday_monday0(target_day);
if dt.weekday_monday0() != target && offset > 0 {
offset -= 1;
}
let delta = (target as i32 - dt.weekday_monday0() as i32).rem_euclid(7)
+ offset.checked_mul(7).ok_or("multiplication overflow")?;
dt = dt.checked_add_days(delta as i64)?;
}
for rel in relative {
dt = match rel {
relative::Relative::Years(years) => dt.checked_add_years(years)?,
relative::Relative::Months(months) => dt.checked_add_months(months)?,
relative::Relative::Days(days) => dt.checked_add_days(days as i64)?,
relative::Relative::Hours(hours) => dt.checked_add_hours(hours as i64)?,
relative::Relative::Minutes(minutes) => dt.checked_add_minutes(minutes as i64)?,
relative::Relative::Seconds(seconds, nanos) => {
dt.checked_add_seconds(seconds, nanos)?
}
};
}
if !has_time_offset && offset.is_none() {
let offset_seconds = resolve_rule_offset_for_extended(&rule_tz, &dt)?;
dt = dt.with_offset(offset_seconds)?;
}
if let Some(offset) = offset {
let (offset, hour_adjustment) = offset.normalize();
dt = dt.checked_add_hours(hour_adjustment as i64)?;
dt = dt.with_offset(offset.total_seconds())?;
}
if dt.year <= 9999 {
if let (Ok(ts), Ok(offset)) = (
jiff::Timestamp::new(dt.unix_seconds(), dt.nanosecond as i32),
jiff::tz::Offset::from_seconds(dt.offset_seconds),
) {
return Ok(ParsedDateTime::InRange(ts.to_zoned(offset.to_time_zone())));
}
}
Ok(ParsedDateTime::Extended(dt))
}
}
fn surrogate_year_for_rules(year: u32) -> i16 {
const BASE: i64 = 9_599;
let mapped = BASE + (i64::from(year) - BASE).rem_euclid(400);
mapped as i16
}
fn resolve_rule_offset_for_extended(
tz: &jiff::tz::TimeZone,
dt: &ExtendedDateTime,
) -> Result<i32, error::Error> {
let surrogate_year = surrogate_year_for_rules(dt.year);
let surrogate_dt = civil::DateTime::new(
surrogate_year,
dt.month as i8,
dt.day as i8,
dt.hour as i8,
dt.minute as i8,
dt.second as i8,
dt.nanosecond as i32,
)?;
let zoned = tz.to_ambiguous_zoned(surrogate_dt).compatible()?;
Ok(zoned.offset().seconds())
}
fn weekday_monday0(day: weekday::Day) -> u8 {
match day {
weekday::Day::Monday => 0,
weekday::Day::Tuesday => 1,
weekday::Day::Wednesday => 2,
weekday::Day::Thursday => 3,
weekday::Day::Friday => 4,
weekday::Day::Saturday => 5,
weekday::Day::Sunday => 6,
}
}
impl TryFrom<Vec<Item>> for DateTimeBuilder {
type Error = &'static str;
fn try_from(items: Vec<Item>) -> Result<Self, Self::Error> {
let mut builder = DateTimeBuilder::new();
for item in items {
builder = match item {
Item::DateTime(dt) => builder.set_date(dt.date)?.set_time(dt.time)?,
Item::Date(d) => builder.set_date(d)?,
Item::Time(t) => builder.set_time(t)?,
Item::Weekday(weekday) => builder.set_weekday(weekday)?,
Item::Offset(offset) => builder.set_offset(offset)?,
Item::Relative(rel) => builder.push_relative(rel)?,
Item::TimeZone(tz) => builder.set_timezone(tz)?,
Item::Pure(pure) => builder.set_pure(pure)?,
}
}
Ok(builder)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jiff::{civil::DateTime, tz::TimeZone};
fn timestamp() -> epoch::Timestamp {
let mut input = "@1234567890";
epoch::parse(&mut input).unwrap()
}
fn date() -> date::Date {
let mut input = "2023-06-15";
date::parse(&mut input).unwrap()
}
fn date_large(mut input: &str) -> date::Date {
date::parse(&mut input).unwrap()
}
fn time() -> time::Time {
let mut input = "12:30:00";
time::parse(&mut input).unwrap()
}
fn time_with_offset() -> time::Time {
let mut input = "12:30:00+05:00";
time::parse(&mut input).unwrap()
}
fn time_with_small_offset() -> time::Time {
let mut input = "12:00:00+01:00";
time::parse(&mut input).unwrap()
}
fn weekday() -> weekday::Weekday {
let mut input = "monday";
weekday::parse(&mut input).unwrap()
}
fn offset() -> offset::Offset {
let mut input = "+05:00";
offset::timezone_offset(&mut input).unwrap()
}
fn offset_large() -> offset::Offset {
let mut input = "m+24";
offset::parse(&mut input).unwrap()
}
fn relative_day() -> relative::Relative {
let mut input = "1 day";
relative::parse(&mut input).unwrap()
}
fn relative_month() -> relative::Relative {
let mut input = "1 month";
relative::parse(&mut input).unwrap()
}
fn relative_hours() -> relative::Relative {
let mut input = "2 hours";
relative::parse(&mut input).unwrap()
}
fn relative_minutes() -> relative::Relative {
let mut input = "3 minutes";
relative::parse(&mut input).unwrap()
}
fn relative_seconds() -> relative::Relative {
let mut input = "4 seconds";
relative::parse(&mut input).unwrap()
}
fn relative_day_ago() -> relative::Relative {
let mut input = "1 day ago";
relative::parse(&mut input).unwrap()
}
fn weekday_next_monday() -> weekday::Weekday {
let mut input = "next monday";
weekday::parse(&mut input).unwrap()
}
fn timezone() -> jiff::tz::TimeZone {
jiff::tz::TimeZone::UTC
}
fn expect_extended_datetime(parsed: ParsedDateTime) -> ExtendedDateTime {
match parsed {
ParsedDateTime::Extended(dt) => dt,
ParsedDateTime::InRange(z) => panic!("expected extended datetime, got in-range: {z}"),
}
}
#[test]
fn duplicate_items_error() {
let test_cases = vec![
(
vec![Item::TimeZone(timezone()), Item::TimeZone(timezone())],
"timezone rule cannot appear more than once",
),
(
vec![Item::Date(date()), Item::Date(date())],
"date cannot appear more than once",
),
(
vec![Item::Time(time()), Item::Time(time())],
"time cannot appear more than once",
),
(
vec![Item::Weekday(weekday()), Item::Weekday(weekday())],
"weekday cannot appear more than once",
),
(
vec![Item::Offset(offset()), Item::Offset(offset())],
"time offset cannot appear more than once",
),
];
for (items, expected_err) in test_cases {
let result = DateTimeBuilder::try_from(items);
assert_eq!(result.unwrap_err(), expected_err);
}
}
#[test]
fn duplicate_timestamp_error() {
let builder = DateTimeBuilder::new().set_timestamp(timestamp()).unwrap();
assert_eq!(
builder.set_timestamp(timestamp()).unwrap_err(),
"timestamp cannot appear more than once"
);
}
#[test]
fn timestamp_cannot_be_combined_with_other_items() {
let builders_with_items: Vec<DateTimeBuilder> = vec![
DateTimeBuilder::new().set_date(date()).unwrap(),
DateTimeBuilder::new().set_time(time()).unwrap(),
DateTimeBuilder::new().set_weekday(weekday()).unwrap(),
DateTimeBuilder::new().set_offset(offset()).unwrap(),
DateTimeBuilder::new()
.push_relative(relative_day())
.unwrap(),
];
for builder in builders_with_items {
assert_eq!(
builder.set_timestamp(timestamp()).unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
}
let ts_builder = || DateTimeBuilder::new().set_timestamp(timestamp()).unwrap();
assert_eq!(
ts_builder().set_date(date()).unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
assert_eq!(
ts_builder().set_time(time()).unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
assert_eq!(
ts_builder().set_weekday(weekday()).unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
assert_eq!(
ts_builder().push_relative(relative_day()).unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
assert_eq!(
ts_builder().set_offset(offset()).unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
assert_eq!(
ts_builder().set_pure("2023".to_string()).unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
}
#[test]
fn time_offset_conflicts() {
let items1 = vec![Item::Time(time_with_offset()), Item::Offset(offset())];
assert_eq!(
DateTimeBuilder::try_from(items1).unwrap_err(),
"time offset cannot appear more than once"
);
let items2 = vec![Item::Offset(offset()), Item::Time(time_with_offset())];
assert_eq!(
DateTimeBuilder::try_from(items2).unwrap_err(),
"time offset and timezone are mutually exclusive"
);
}
#[test]
fn helper_mappings() {
let mapped = surrogate_year_for_rules(2024);
assert!((9599..=9998).contains(&mapped));
assert_eq!((mapped as i32).rem_euclid(400), (2024_i32).rem_euclid(400));
assert_eq!(surrogate_year_for_rules(10000), 9600);
assert_eq!(weekday_monday0(weekday::Day::Monday), 0);
assert_eq!(weekday_monday0(weekday::Day::Tuesday), 1);
assert_eq!(weekday_monday0(weekday::Day::Wednesday), 2);
assert_eq!(weekday_monday0(weekday::Day::Thursday), 3);
assert_eq!(weekday_monday0(weekday::Day::Friday), 4);
assert_eq!(weekday_monday0(weekday::Day::Saturday), 5);
assert_eq!(weekday_monday0(weekday::Day::Sunday), 6);
}
#[test]
fn build_rejects_timestamp_in_extended_path() {
let mut builder = DateTimeBuilder::new();
builder.timestamp = Some(timestamp());
assert!(builder.build_extended().is_err());
}
#[test]
fn build_falls_back_to_extended_for_9999_rollover() {
let base = "2000-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("9999-12-31")),
Item::Relative(relative_day()),
])
.unwrap();
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
assert_eq!((dt.year, dt.month, dt.day), (10000, 1, 1));
}
#[test]
fn build_extended_keeps_9999_values_when_timestamp_overflows() {
let base = "2000-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("10000-01-01")),
Item::Relative(relative_day_ago()),
])
.unwrap();
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
assert_eq!((dt.year, dt.month, dt.day), (9999, 12, 31));
}
#[test]
fn resolve_rule_offset_for_extended_uses_surrogate_year() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 7,
day: 1,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
let offset = resolve_rule_offset_for_extended(&jiff::tz::TimeZone::UTC, &dt).unwrap();
assert_eq!(offset, 0);
}
#[test]
fn resolve_rule_offset_for_extended_non_utc_rule() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 7,
day: 1,
},
TimeParts {
hour: 12,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
let tz = jiff::tz::TimeZone::get("Europe/Paris").unwrap();
let _ = resolve_rule_offset_for_extended(&tz, &dt).unwrap();
}
#[test]
fn resolve_rule_offset_for_extended_handles_upper_bound_dates() {
let dt = ExtendedDateTime::new(
DateParts {
year: 9999,
month: 12,
day: 31,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert!(resolve_rule_offset_for_extended(&jiff::tz::TimeZone::UTC, &dt).is_ok());
}
#[test]
fn should_try_extended_fallback_conditions() {
let mut builder = DateTimeBuilder::new();
assert!(!builder.should_try_extended_fallback());
builder.relative.push(relative_day());
builder.date = Some(date_large("9999-12-31"));
assert!(builder.should_try_extended_fallback());
builder.timestamp = Some(timestamp());
assert!(!builder.should_try_extended_fallback());
}
#[test]
fn build_extended_applies_relative_units_and_weekday() {
let base = "2000-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("10000-01-01")),
Item::Weekday(weekday_next_monday()),
Item::Relative(relative_day()),
Item::Relative(relative_hours()),
Item::Relative(relative_minutes()),
Item::Relative(relative_seconds()),
])
.unwrap();
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
assert_eq!((dt.year, dt.month, dt.day), (10000, 1, 4));
assert_eq!((dt.hour, dt.minute, dt.second), (2, 3, 4));
}
#[test]
fn build_extended_applies_normalized_final_offset() {
let base = "2000-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("10000-06-01")),
Item::Time(time()),
Item::Offset(offset_large()),
])
.unwrap();
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
assert_eq!(dt.offset_seconds, 23 * 3600);
}
#[test]
fn build_prefers_in_range_when_fallback_succeeds() {
let base = "2000-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("9999-01-31")),
Item::Relative(relative_day()),
])
.unwrap();
let z = builder.set_base(base).build().unwrap().expect_in_range();
assert_eq!(z.strftime("%Y-%m-%d").to_string(), "9999-02-01");
}
#[test]
fn build_falls_back_to_extended_for_non_relative_boundary_time_offset() {
let base = "2000-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("9999-12-31")),
Item::Time(time_with_small_offset()),
])
.unwrap();
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
assert_eq!((dt.year, dt.month, dt.day), (9999, 12, 31));
assert_eq!((dt.hour, dt.minute, dt.second), (12, 0, 0));
assert_eq!(dt.offset_seconds, 3600);
}
#[test]
fn build_in_range_month_overflow_adjustment_path() {
let base = "2000-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("2021-01-31")),
Item::Relative(relative_month()),
])
.unwrap();
let z = builder.set_base(base).build().unwrap().expect_in_range();
assert_eq!(z.strftime("%Y-%m-%d").to_string(), "2021-03-03");
}
#[test]
fn build_extended_uses_base_and_timezone_rule() {
let base = "2000-01-01 10:11:12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let tz = jiff::tz::TimeZone::get("Europe/Paris").unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("10000-01-01")),
Item::TimeZone(tz),
])
.unwrap();
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
assert_eq!(dt.year, 10000);
}
#[test]
fn build_extended_supports_timezone_without_base() {
let tz = jiff::tz::TimeZone::get("Europe/Paris").unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("10000-01-01")),
Item::TimeZone(tz),
])
.unwrap();
let dt = expect_extended_datetime(builder.build().unwrap());
assert_eq!(dt.year, 10000);
}
#[test]
fn build_extended_preserves_base_time_when_not_truncated() {
let base = "2000-01-01 10:11:12.123456789"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let mut builder = DateTimeBuilder::new();
builder.base = Some(base);
let z = builder.build_extended().unwrap().expect_in_range();
assert_eq!(
z.strftime("%Y-%m-%d %H:%M:%S%.9f").to_string(),
"2000-01-01 10:11:12.123456789"
);
}
#[test]
fn build_extended_applies_date_and_weekday_without_time_item() {
let base = "2000-01-01 10:11:12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
let builder = DateTimeBuilder::try_from(vec![
Item::Date(date_large("10000-01-01")),
Item::Weekday(weekday_next_monday()),
])
.unwrap();
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
assert_eq!((dt.hour, dt.minute, dt.second), (0, 0, 0));
}
#[test]
#[should_panic(expected = "ParsedDateTime is not representable as jiff::Zoned")]
fn expect_in_range_panics_for_extended_input() {
let parsed = DateTimeBuilder::try_from(vec![Item::Date(date_large("10000-01-01"))])
.unwrap()
.build()
.unwrap();
let _ = parsed.expect_in_range();
}
}