use mlua::prelude::*; use chrono::prelude::*; use lune_utils::TableBuilder; use super::result::{DateTimeError, DateTimeResult}; #[derive(Debug, Clone, Copy)] pub struct DateTimeValues { pub year: i32, pub month: u32, pub day: u32, pub hour: u32, pub minute: u32, pub second: u32, pub millisecond: u32, } impl DateTimeValues { /** Verifies that all of the date & time values are within allowed ranges: | Name | Range | |---------------|----------------| | `year` | `1400 -> 9999` | | `month` | `1 -> 12` | | `day` | `1 -> 31` | | `hour` | `0 -> 23` | | `minute` | `0 -> 59` | | `second` | `0 -> 60` | | `millisecond` | `0 -> 999` | */ pub fn verify(self) -> DateTimeResult { verify_in_range("year", self.year, 1400, 9999)?; verify_in_range("month", self.month, 1, 12)?; verify_in_range("day", self.day, 1, 31)?; verify_in_range("hour", self.hour, 0, 23)?; verify_in_range("minute", self.minute, 0, 59)?; verify_in_range("second", self.second, 0, 60)?; verify_in_range("millisecond", self.millisecond, 0, 999)?; Ok(self) } } fn verify_in_range(name: &'static str, value: T, min: T, max: T) -> DateTimeResult where T: PartialOrd + std::fmt::Display, { assert!(max > min); if value < min || value > max { Err(DateTimeError::OutOfRange { name, min: min.to_string(), max: max.to_string(), value: value.to_string(), }) } else { Ok(value) } } /** Conversion methods between `DateTimeValues` and plain lua tables Note that the `IntoLua` implementation here uses a read-only table, since we generally want to convert into lua when we know we have a fixed point in time, and we guarantee that it doesn't change */ impl FromLua<'_> for DateTimeValues { fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { if !value.is_table() { return Err(LuaError::FromLuaConversionError { from: value.type_name(), to: "DateTimeValues", message: Some("value must be a table".to_string()), }); }; let value = value.as_table().unwrap(); let values = Self { year: value.get("year")?, month: value.get("month")?, day: value.get("day")?, hour: value.get("hour")?, minute: value.get("minute")?, second: value.get("second")?, millisecond: value.get("millisecond").unwrap_or(0), }; match values.verify() { Ok(dt) => Ok(dt), Err(e) => Err(LuaError::FromLuaConversionError { from: "table", to: "DateTimeValues", message: Some(e.to_string()), }), } } } impl IntoLua<'_> for DateTimeValues { fn into_lua(self, lua: &Lua) -> LuaResult { let tab = TableBuilder::new(lua)? .with_value("year", self.year)? .with_values(vec![ ("month", self.month), ("day", self.day), ("hour", self.hour), ("minute", self.minute), ("second", self.second), ("millisecond", self.millisecond), ])? .build_readonly()?; Ok(LuaValue::Table(tab)) } } /** Conversion methods between chrono's timezone-aware `DateTime` to and from our non-timezone-aware `DateTimeValues` values struct */ impl From> for DateTimeValues { fn from(value: DateTime) -> Self { Self { year: value.year(), month: value.month(), day: value.day(), hour: value.hour(), minute: value.minute(), second: value.second(), millisecond: value.timestamp_subsec_millis(), } } } impl TryFrom for DateTime { type Error = DateTimeError; fn try_from(value: DateTimeValues) -> Result { Utc.with_ymd_and_hms( value.year, value.month, value.day, value.hour, value.minute, value.second, ) .single() .ok_or(DateTimeError::Ambiguous) } } impl TryFrom for DateTime { type Error = DateTimeError; fn try_from(value: DateTimeValues) -> Result { Local .with_ymd_and_hms( value.year, value.month, value.day, value.hour, value.minute, value.second, ) .single() .ok_or(DateTimeError::Ambiguous) } }