use std::cmp::Ordering;
use std::hash::Hash;
use std::ops::{Add, Sub};
use ecow::{EcoString, EcoVec, eco_format};
use time::error::{Format, InvalidFormatDescription};
use time::macros::format_description;
use time::{Month, PrimitiveDateTime, format_description};
use crate::World;
use crate::diag::{StrResult, bail};
use crate::engine::Engine;
use crate::foundations::{
Dict, Duration, Repr, Smart, Str, Value, cast, func, repr, scope, ty,
};
#[ty(scope, cast)]
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
pub enum Datetime {
Date(time::Date),
Time(time::Time),
Datetime(time::PrimitiveDateTime),
}
impl Datetime {
pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
Some(Datetime::Date(
time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
.ok()?,
))
}
pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> {
Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?))
}
pub fn from_ymd_hms(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> Option<Self> {
let date =
time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
.ok()?;
let time = time::Time::from_hms(hour, minute, second).ok()?;
Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
}
pub fn from_toml_dict(dict: &Dict) -> Option<Self> {
if dict.len() != 1 {
return None;
}
let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else {
return None;
};
if let Ok(d) = time::PrimitiveDateTime::parse(
string,
&format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"),
) {
Self::from_ymd_hms(
d.year(),
d.month() as u8,
d.day(),
d.hour(),
d.minute(),
d.second(),
)
} else if let Ok(d) = time::PrimitiveDateTime::parse(
string,
&format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"),
) {
Self::from_ymd_hms(
d.year(),
d.month() as u8,
d.day(),
d.hour(),
d.minute(),
d.second(),
)
} else if let Ok(d) =
time::Date::parse(string, &format_description!("[year]-[month]-[day]"))
{
Self::from_ymd(d.year(), d.month() as u8, d.day())
} else if let Ok(d) =
time::Time::parse(string, &format_description!("[hour]:[minute]:[second]"))
{
Self::from_hms(d.hour(), d.minute(), d.second())
} else {
None
}
}
pub fn kind(&self) -> &'static str {
match self {
Datetime::Datetime(_) => "datetime",
Datetime::Date(_) => "date",
Datetime::Time(_) => "time",
}
}
}
#[scope]
impl Datetime {
#[func(constructor)]
pub fn construct(
#[named]
year: Option<i32>,
#[named]
month: Option<Month>,
#[named]
day: Option<u8>,
#[named]
hour: Option<u8>,
#[named]
minute: Option<u8>,
#[named]
second: Option<u8>,
) -> StrResult<Datetime> {
let time = match (hour, minute, second) {
(Some(hour), Some(minute), Some(second)) => {
match time::Time::from_hms(hour, minute, second) {
Ok(time) => Some(time),
Err(_) => bail!("time is invalid"),
}
}
(None, None, None) => None,
_ => bail!("time is incomplete"),
};
let date = match (year, month, day) {
(Some(year), Some(month), Some(day)) => {
match time::Date::from_calendar_date(year, month, day) {
Ok(date) => Some(date),
Err(_) => bail!("date is invalid"),
}
}
(None, None, None) => None,
_ => bail!("date is incomplete"),
};
Ok(match (date, time) {
(Some(date), Some(time)) => {
Datetime::Datetime(PrimitiveDateTime::new(date, time))
}
(Some(date), None) => Datetime::Date(date),
(None, Some(time)) => Datetime::Time(time),
(None, None) => {
bail!("at least one of date or time must be fully specified")
}
})
}
#[func]
pub fn today(
engine: &mut Engine,
#[named]
#[default]
offset: Smart<i64>,
) -> StrResult<Datetime> {
Ok(engine
.world
.today(offset.custom())
.ok_or("unable to get the current date")?)
}
#[func]
pub fn display(
&self,
#[default]
pattern: Smart<DisplayPattern>,
) -> StrResult<EcoString> {
let pat = |s| format_description::parse_borrowed::<2>(s).unwrap();
let result = match pattern {
Smart::Auto => match self {
Self::Date(date) => date.format(&pat("[year]-[month]-[day]")),
Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")),
Self::Datetime(datetime) => {
datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]"))
}
},
Smart::Custom(DisplayPattern(_, format)) => match self {
Self::Date(date) => date.format(&format),
Self::Time(time) => time.format(&format),
Self::Datetime(datetime) => datetime.format(&format),
},
};
result.map(EcoString::from).map_err(format_time_format_error)
}
#[func]
pub fn year(&self) -> Option<i32> {
match self {
Self::Date(date) => Some(date.year()),
Self::Time(_) => None,
Self::Datetime(datetime) => Some(datetime.year()),
}
}
#[func]
pub fn month(&self) -> Option<u8> {
match self {
Self::Date(date) => Some(date.month().into()),
Self::Time(_) => None,
Self::Datetime(datetime) => Some(datetime.month().into()),
}
}
#[func]
pub fn weekday(&self) -> Option<u8> {
match self {
Self::Date(date) => Some(date.weekday().number_from_monday()),
Self::Time(_) => None,
Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
}
}
#[func]
pub fn day(&self) -> Option<u8> {
match self {
Self::Date(date) => Some(date.day()),
Self::Time(_) => None,
Self::Datetime(datetime) => Some(datetime.day()),
}
}
#[func]
pub fn hour(&self) -> Option<u8> {
match self {
Self::Date(_) => None,
Self::Time(time) => Some(time.hour()),
Self::Datetime(datetime) => Some(datetime.hour()),
}
}
#[func]
pub fn minute(&self) -> Option<u8> {
match self {
Self::Date(_) => None,
Self::Time(time) => Some(time.minute()),
Self::Datetime(datetime) => Some(datetime.minute()),
}
}
#[func]
pub fn second(&self) -> Option<u8> {
match self {
Self::Date(_) => None,
Self::Time(time) => Some(time.second()),
Self::Datetime(datetime) => Some(datetime.second()),
}
}
#[func]
pub fn ordinal(&self) -> Option<u16> {
match self {
Self::Datetime(datetime) => Some(datetime.ordinal()),
Self::Date(date) => Some(date.ordinal()),
Self::Time(_) => None,
}
}
}
impl Repr for Datetime {
fn repr(&self) -> EcoString {
let year = self.year().map(|y| eco_format!("year: {}", (y as i64).repr()));
let month = self.month().map(|m| eco_format!("month: {}", (m as i64).repr()));
let day = self.day().map(|d| eco_format!("day: {}", (d as i64).repr()));
let hour = self.hour().map(|h| eco_format!("hour: {}", (h as i64).repr()));
let minute = self.minute().map(|m| eco_format!("minute: {}", (m as i64).repr()));
let second = self.second().map(|s| eco_format!("second: {}", (s as i64).repr()));
let filtered = [year, month, day, hour, minute, second]
.into_iter()
.flatten()
.collect::<EcoVec<_>>();
eco_format!("datetime{}", &repr::pretty_array_like(&filtered, false))
}
}
impl PartialOrd for Datetime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b),
(Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
(Self::Time(a), Self::Time(b)) => a.partial_cmp(b),
_ => None,
}
}
}
impl Add<Duration> for Datetime {
type Output = Self;
fn add(self, rhs: Duration) -> Self::Output {
let rhs: time::Duration = rhs.into();
match self {
Self::Datetime(datetime) => Self::Datetime(datetime + rhs),
Self::Date(date) => Self::Date(date + rhs),
Self::Time(time) => Self::Time(time + rhs),
}
}
}
impl Sub<Duration> for Datetime {
type Output = Self;
fn sub(self, rhs: Duration) -> Self::Output {
let rhs: time::Duration = rhs.into();
match self {
Self::Datetime(datetime) => Self::Datetime(datetime - rhs),
Self::Date(date) => Self::Date(date - rhs),
Self::Time(time) => Self::Time(time - rhs),
}
}
}
impl Sub for Datetime {
type Output = StrResult<Duration>;
fn sub(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()),
(Self::Date(a), Self::Date(b)) => Ok((a - b).into()),
(Self::Time(a), Self::Time(b)) => Ok((a - b).into()),
(a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
}
}
}
pub struct DisplayPattern(Str, format_description::OwnedFormatItem);
cast! {
DisplayPattern,
self => self.0.into_value(),
v: Str => {
let item = format_description::parse_owned::<2>(&v)
.map_err(format_time_invalid_format_description_error)?;
Self(v, item)
}
}
cast! {
Month,
v: u8 => Self::try_from(v).map_err(|_| "month is invalid")?
}
fn format_time_format_error(error: Format) -> EcoString {
match error {
Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name),
Format::InsufficientTypeInformation { .. } => {
"failed to format datetime (insufficient information)".into()
}
err => eco_format!("failed to format datetime in the requested format ({err})"),
}
}
fn format_time_invalid_format_description_error(
error: InvalidFormatDescription,
) -> EcoString {
match error {
InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => {
eco_format!("missing closing bracket for bracket at index {}", index)
}
InvalidFormatDescription::InvalidComponentName { name, index, .. } => {
eco_format!("invalid component name '{}' at index {}", name, index)
}
InvalidFormatDescription::InvalidModifier { value, index, .. } => {
eco_format!("invalid modifier '{}' at index {}", value, index)
}
InvalidFormatDescription::Expected { what, index, .. } => {
eco_format!("expected {} at index {}", what, index)
}
InvalidFormatDescription::MissingComponentName { index, .. } => {
eco_format!("expected component name at index {}", index)
}
InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => {
eco_format!(
"missing required modifier {} for component at index {}",
name,
index
)
}
InvalidFormatDescription::NotSupported { context, what, index, .. } => {
eco_format!("{} is not supported in {} at index {}", what, context, index)
}
err => eco_format!("failed to parse datetime format ({err})"),
}
}