use jiff::{civil, Span, ToSpan, Zoned};
use super::{date, epoch, error, offset, relative, time, weekday, year, Item};
#[derive(Debug, Default)]
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<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 {
date.with_year(dt.date().year() as u16).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)?)?;
}
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()?)?,
}
}
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()?)?;
}
Ok(dt)
}
}
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::Timestamp(ts) => builder.set_timestamp(ts)?,
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::*;
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 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 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 relative() -> relative::Relative {
let mut input = "1 day";
relative::parse(&mut input).unwrap()
}
fn timezone() -> jiff::tz::TimeZone {
jiff::tz::TimeZone::UTC
}
#[test]
fn test_duplicate_items_errors() {
let test_cases = vec![
(
vec![Item::TimeZone(timezone()), Item::TimeZone(timezone())],
"timezone rule cannot appear more than once",
),
(
vec![Item::Timestamp(timestamp()), Item::Timestamp(timestamp())],
"timestamp 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 test_timestamp_cannot_be_combined_with_other_items() {
let test_cases = vec![
vec![Item::Date(date()), Item::Timestamp(timestamp())],
vec![Item::Time(time()), Item::Timestamp(timestamp())],
vec![Item::Weekday(weekday()), Item::Timestamp(timestamp())],
vec![Item::Offset(offset()), Item::Timestamp(timestamp())],
vec![Item::Relative(relative()), Item::Timestamp(timestamp())],
vec![Item::Timestamp(timestamp()), Item::Date(date())],
vec![Item::Timestamp(timestamp()), Item::Time(time())],
vec![Item::Timestamp(timestamp()), Item::Weekday(weekday())],
vec![Item::Timestamp(timestamp()), Item::Relative(relative())],
vec![Item::Timestamp(timestamp()), Item::Offset(offset())],
vec![Item::Timestamp(timestamp()), Item::Pure("2023".to_string())],
];
for items in test_cases {
let result = DateTimeBuilder::try_from(items);
assert_eq!(
result.unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
}
}
#[test]
fn test_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 test_valid_combination_date_time() {
let items = vec![Item::Date(date()), Item::Time(time())];
let result = DateTimeBuilder::try_from(items);
assert!(result.is_ok());
}
#[test]
fn test_valid_combination_date_weekday() {
let items = vec![Item::Date(date()), Item::Weekday(weekday())];
let result = DateTimeBuilder::try_from(items);
assert!(result.is_ok());
}
#[test]
fn test_valid_timestamp_alone() {
let items = vec![Item::Timestamp(timestamp())];
let result = DateTimeBuilder::try_from(items);
assert!(result.is_ok());
}
}