use chrono::{offset::LocalResult, prelude::*};
use mlua::{FromLua, MetaMethod, UserData};
#[derive(Debug, Clone, FromLua)]
pub struct AstraDateTime {
dt: DateTime<FixedOffset>,
}
impl AstraDateTime {
pub fn register_to_lua(lua: &mlua::Lua) -> mlua::Result<()> {
lua.globals().set(
"astra_internal__datetime_sleep",
lua.create_async_function(|_, amount: u64| async move {
tokio::time::sleep(std::time::Duration::from_millis(amount)).await;
Ok(())
})?,
)?;
lua.globals().set(
"astra_internal__datetime_new_now",
lua.create_function(|_, is_utc: bool| {
let dt = if is_utc {
Utc::now().fixed_offset()
} else {
Local::now().fixed_offset()
};
Ok(Self { dt })
})?,
)?;
lua.globals().set(
"astra_internal__datetime_new_parse",
lua.create_function(|_, date_str: String| {
match DateTime::parse_from_rfc2822(&date_str) {
Ok(dt) => Ok(Self { dt }),
Err(err1) => match DateTime::parse_from_rfc3339(&date_str) {
Ok(dt) => Ok(Self { dt }),
Err(err2) => Err(mlua::Error::runtime(format!(
"\nRFC 2822 ERR: {:?}\nRFC 3339 ERR: {:?}",
err1.to_string(),
err2.to_string()
))),
},
}
})?,
)?;
lua.globals().set(
"astra_internal__datetime_new_from",
lua.create_function(
#[allow(clippy::type_complexity)]
|_,
(year, month, day, hour, min, sec, milli, is_utc): (
i32,
Option<u32>,
Option<u32>,
Option<u32>,
Option<u32>,
Option<u32>,
Option<u32>,
bool,
)| {
match NaiveDate::from_ymd_opt(year, month.unwrap_or(1), day.unwrap_or(1))
.and_then(|naive_date| {
naive_date.and_hms_milli_opt(
hour.unwrap_or_default(),
min.unwrap_or_default(),
sec.unwrap_or_default(),
milli.unwrap_or_default(),
)
}) {
Some(naive_datetime) => {
if is_utc {
Ok(Self {
dt: naive_datetime.and_utc().fixed_offset(),
})
} else {
match naive_datetime.and_local_timezone(Local) {
LocalResult::Single(dt) => Ok(Self {
dt: dt.fixed_offset(),
}),
LocalResult::Ambiguous(earliest, _latest) => Ok(Self {
dt: earliest.fixed_offset(),
}),
LocalResult::None => Err(mlua::Error::runtime(
"Error while resolving local time!",
)),
}
}
}
None => Err(mlua::Error::runtime("Invalid time!")),
}
},
)?,
)?;
Ok(())
}
}
impl UserData for AstraDateTime {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
macro_rules! add_getter_method {
($method_name:expr, $field:ident) => {
methods.add_method($method_name, |_, this, ()| Ok(this.dt.$field()));
};
($method_name:expr, $field:ident, $conversion:expr) => {
methods.add_method($method_name, |_, this, ()| {
Ok($conversion(this.dt.$field()))
});
};
}
macro_rules! add_setter_method {
($method_name:expr,$method:ident, $field_type:ty, $error_msg:expr) => {
methods.add_method_mut($method_name, |_, this, field: $field_type| {
match this.dt.$method(field) {
Some(n) => {
this.dt = n;
Ok(())
}
None => Err(mlua::Error::runtime($error_msg)),
}
});
};
}
macro_rules! add_formatted_method {
($method_name:expr, $format_str:expr) => {
methods.add_method($method_name, |_, this, ()| {
Ok(format!("{}", this.dt.format($format_str)))
});
};
($method_name:expr, $operation:expr) => {
methods.add_method($method_name, |_, this, ()| Ok($operation));
};
}
macro_rules! add_to_datetime {
($method_name:expr, $method:ident) => {
methods.add_method($method_name, |_, this, millis: i64| {
match this
.dt
.checked_add_signed(chrono::TimeDelta::$method(millis))
{
Some(delta) => Ok(Self { dt: delta }),
None => Err(mlua::Error::runtime("Invalid value")),
}
});
};
}
macro_rules! sub_from_datetime {
($method_name:expr, $method:ident) => {
methods.add_method($method_name, |_, this, millis: i64| {
match this
.dt
.checked_sub_signed(chrono::TimeDelta::$method(millis))
{
Some(delta) => Ok(Self { dt: delta }),
None => Err(mlua::Error::runtime("Invalid value")),
}
});
};
}
add_getter_method!("get_year", year);
add_getter_method!("get_month", month);
add_getter_method!("get_day", day);
add_getter_method!("get_weekday", weekday, |w: Weekday| w
.num_days_from_sunday());
add_getter_method!("get_hour", hour);
add_getter_method!("get_minute", minute);
add_getter_method!("get_second", second);
add_getter_method!("get_millisecond", timestamp_subsec_millis);
add_getter_method!("get_epoch_milliseconds", timestamp_millis);
add_getter_method!("get_timezone_offset", offset, |offset: &FixedOffset| offset
.local_minus_utc()
/ 60);
add_setter_method!("set_year", with_year, i32, "Invalid year!");
add_setter_method!("set_month", with_month, u32, "Invalid month!");
add_setter_method!("set_day", with_day, u32, "Invalid day!");
add_setter_method!("set_hour", with_hour, u32, "Invalid hour!");
add_setter_method!("set_minute", with_minute, u32, "Invalid minute!");
add_setter_method!("set_second", with_second, u32, "Invalid second!");
methods.add_method_mut("set_millisecond", |_, this, field: u32| {
match this.dt.with_nanosecond(field * 1_000_000) {
Some(n) => {
this.dt = n;
Ok(())
}
None => Err(mlua::Error::runtime("Invalid millisecond!")),
}
});
methods.add_method_mut("set_epoch_milliseconds", |_, this, milli: i64| {
match DateTime::from_timestamp_millis(milli) {
Some(dt) => {
this.dt = dt.with_timezone(&this.dt.timezone().fix());
Ok(())
}
None => Err(mlua::Error::runtime("Invalid millisecond!")),
}
});
add_to_datetime!("add_milliseconds", milliseconds);
add_to_datetime!("add_seconds", seconds);
add_to_datetime!("add_minutes", minutes);
add_to_datetime!("add_hours", hours);
add_to_datetime!("add_days", days);
add_to_datetime!("add_weeks", weeks);
methods.add_method("add_months", |_, this, months: i32| {
let current_month = this.dt.month() as i32;
let new_month = current_month + months;
if !(1..=12).contains(&new_month) {
return Err(mlua::Error::runtime("Month must be between 1-12"));
}
match this.dt.with_month(new_month as u32) {
Some(n) => Ok(Self { dt: n }),
None => Err(mlua::Error::runtime("Invalid month value")),
}
});
methods.add_method("add_years", |_, this, years: i32| {
let current_year = this.dt.year() + years;
match this.dt.with_year(current_year) {
Some(n) => Ok(Self { dt: n }),
None => Err(mlua::Error::runtime("Invalid year value")),
}
});
sub_from_datetime!("sub_milliseconds", milliseconds);
sub_from_datetime!("sub_seconds", seconds);
sub_from_datetime!("sub_minutes", minutes);
sub_from_datetime!("sub_hours", hours);
sub_from_datetime!("sub_days", days);
sub_from_datetime!("sub_weeks", weeks);
methods.add_method("sub_months", |_, this, months: i32| {
let current_month = this.dt.month() as i32;
let new_month = current_month - months;
if !(1..=12).contains(&new_month) {
return Err(mlua::Error::runtime("Month must be between 1-12"));
}
match this.dt.with_month(new_month as u32) {
Some(n) => Ok(Self { dt: n }),
None => Err(mlua::Error::runtime("Invalid month value")),
}
});
methods.add_method("sub_years", |_, this, years: i32| {
let current_year = this.dt.year() - years;
match this.dt.with_year(current_year) {
Some(n) => Ok(Self { dt: n }),
None => Err(mlua::Error::runtime("Invalid year value")),
}
});
methods.add_method(
"set_time",
|_, this, (hour, minute, second, millis): (u32, u32, u32, u32)| {
let dt = this
.dt
.with_hour(hour)
.and_then(|dt| dt.with_minute(minute))
.and_then(|dt| dt.with_second(second))
.and_then(|dt| dt.with_nanosecond(millis * 1_000_000));
match dt {
Some(n) => Ok(Self { dt: n }),
None => Err(mlua::Error::runtime("Invalid time values")),
}
},
);
methods.add_method(
"set_date",
|_, this, (year, month, day): (i32, u32, u32)| {
let dt = this
.dt
.with_year(year)
.and_then(|dt| dt.with_month(month))
.and_then(|dt| dt.with_day(day));
match dt {
Some(n) => Ok(Self { dt: n }),
None => Err(mlua::Error::runtime("Invalid date values")),
}
},
);
methods.add_method("to_utc", |_, this, _: ()| {
Ok(Self {
dt: this.dt.to_utc().fixed_offset(),
})
});
methods.add_method("to_local", |_, this, _: ()| {
let dt: DateTime<chrono::Local> = chrono::DateTime::from(this.dt);
Ok(Self {
dt: dt.fixed_offset(),
})
});
add_getter_method!("to_rfc2822", to_rfc2822);
add_getter_method!("to_rfc3339", to_rfc3339);
add_getter_method!("to_datetime_string", to_rfc3339);
add_formatted_method!("to_date_string", "%Y-%m-%d");
add_formatted_method!("to_time_string", "%H:%M:%S%.3f%:z");
add_formatted_method!("to_locale_date_string", "%x");
add_formatted_method!("to_locale_time_string", "%X");
add_formatted_method!("to_locale_datetime_string", "%c");
methods.add_method("to_iso_string", |_, this, ()| {
Ok(this.dt.to_rfc3339_opts(SecondsFormat::Millis, false))
});
methods.add_method("to_format", |_, this, format: String| {
Ok(this.dt.format(&format).to_string())
});
methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
Ok(this.dt.to_rfc3339())
});
methods.add_meta_method(
MetaMethod::Concat,
|_, _, (a, b): (mlua::Value, mlua::Value)| Ok(a.to_string()? + &b.to_string()?),
);
methods.add_meta_method(MetaMethod::Eq, |_, this, other: Self| {
Ok(this.dt == other.dt)
});
methods.add_meta_method(MetaMethod::Le, |_, this, other: Self| {
Ok(this.dt <= other.dt)
});
methods.add_meta_method(
MetaMethod::Lt,
|_, this, other: Self| Ok(this.dt < other.dt),
);
}
}