mirror of
https://github.com/lune-org/lune.git
synced 2025-04-07 20:10:55 +01:00
Migrate datetime builtin to lune-std-datetime crate
This commit is contained in:
parent
5d941889f3
commit
7fff3cbec5
6 changed files with 458 additions and 1 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1561,8 +1561,11 @@ dependencies = [
|
|||
name = "lune-std-datetime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono_lc",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -13,4 +13,8 @@ workspace = true
|
|||
[dependencies]
|
||||
mlua = { version = "0.9.7", features = ["luau"] }
|
||||
|
||||
thiserror = "1.0"
|
||||
chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc
|
||||
chrono_lc = "0.1"
|
||||
|
||||
lune-utils = { version = "0.1.0", path = "../lune-utils" }
|
||||
|
|
228
crates/lune-std-datetime/src/date_time.rs
Normal file
228
crates/lune-std-datetime/src/date_time.rs
Normal file
|
@ -0,0 +1,228 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use chrono::DateTime as ChronoDateTime;
|
||||
use chrono_lc::LocaleDate;
|
||||
|
||||
use crate::result::{DateTimeError, DateTimeResult};
|
||||
use crate::values::DateTimeValues;
|
||||
|
||||
const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
|
||||
const DEFAULT_LOCALE: &str = "en";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct DateTime {
|
||||
// NOTE: We store this as the UTC time zone since it is the most commonly
|
||||
// used and getting the generics right for TimeZone is somewhat tricky,
|
||||
// but none of the method implementations below should rely on this tz
|
||||
inner: ChronoDateTime<Utc>,
|
||||
}
|
||||
|
||||
impl DateTime {
|
||||
/**
|
||||
Creates a new `DateTime` struct representing the current moment in time.
|
||||
|
||||
See [`chrono::DateTime::now`] for additional details.
|
||||
*/
|
||||
pub fn now() -> Self {
|
||||
Self { inner: Utc::now() }
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new `DateTime` struct from the given `unix_timestamp`,
|
||||
which is a float of seconds passed since the UNIX epoch.
|
||||
|
||||
This is somewhat unconventional, but fits our Luau interface and dynamic types quite well.
|
||||
To use this method the same way you would use a more traditional `from_unix_timestamp`
|
||||
that takes a `u64` of seconds or similar type, casting the value is sufficient:
|
||||
|
||||
```rust ignore
|
||||
DateTime::from_unix_timestamp_float(123456789u64 as f64)
|
||||
```
|
||||
|
||||
See [`chrono::DateTime::from_timestamp`] for additional details.
|
||||
*/
|
||||
pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult<Self> {
|
||||
let whole = unix_timestamp.trunc() as i64;
|
||||
let fract = unix_timestamp.fract();
|
||||
let nanos = (fract * 1_000_000_000f64)
|
||||
.round()
|
||||
.clamp(u32::MIN as f64, u32::MAX as f64) as u32;
|
||||
let inner = ChronoDateTime::<Utc>::from_timestamp(whole, nanos)
|
||||
.ok_or(DateTimeError::OutOfRangeUnspecified)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms individual date & time values into a new
|
||||
`DateTime` struct, using the universal (UTC) time zone.
|
||||
|
||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
||||
for additional details and cases where this constructor may return an error.
|
||||
*/
|
||||
pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
||||
.ok_or(DateTimeError::InvalidDate)?;
|
||||
|
||||
let time = NaiveTime::from_hms_milli_opt(
|
||||
values.hour,
|
||||
values.minute,
|
||||
values.second,
|
||||
values.millisecond,
|
||||
)
|
||||
.ok_or(DateTimeError::InvalidTime)?;
|
||||
|
||||
let inner = Utc.from_utc_datetime(&NaiveDateTime::new(date, time));
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms individual date & time values into a new
|
||||
`DateTime` struct, using the current local time zone.
|
||||
|
||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
||||
for additional details and cases where this constructor may return an error.
|
||||
*/
|
||||
pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
||||
.ok_or(DateTimeError::InvalidDate)?;
|
||||
|
||||
let time = NaiveTime::from_hms_milli_opt(
|
||||
values.hour,
|
||||
values.minute,
|
||||
values.second,
|
||||
values.millisecond,
|
||||
)
|
||||
.ok_or(DateTimeError::InvalidTime)?;
|
||||
|
||||
let inner = Local
|
||||
.from_local_datetime(&NaiveDateTime::new(date, time))
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Formats the `DateTime` using the universal (UTC) time
|
||||
zone, the given format string, and the given locale.
|
||||
|
||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
||||
|
||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
||||
*/
|
||||
pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
||||
self.inner
|
||||
.with_timezone(&Local)
|
||||
.formatl(
|
||||
format.unwrap_or(DEFAULT_FORMAT),
|
||||
locale.unwrap_or(DEFAULT_LOCALE),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Formats the `DateTime` using the universal (UTC) time
|
||||
zone, the given format string, and the given locale.
|
||||
|
||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
||||
|
||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
||||
*/
|
||||
pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
||||
self.inner
|
||||
.with_timezone(&Utc)
|
||||
.formatl(
|
||||
format.unwrap_or(DEFAULT_FORMAT),
|
||||
locale.unwrap_or(DEFAULT_LOCALE),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Parses a time string in the ISO 8601 format, such as
|
||||
`1996-12-19T16:39:57-08:00`, into a new `DateTime` struct.
|
||||
|
||||
See [`chrono::DateTime::parse_from_rfc3339`] for additional details.
|
||||
*/
|
||||
pub fn from_iso_date(iso_date: impl AsRef<str>) -> DateTimeResult<Self> {
|
||||
let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc);
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts individual date & time values from this
|
||||
`DateTime`, using the current local time zone.
|
||||
*/
|
||||
pub fn to_local_time(self) -> DateTimeValues {
|
||||
DateTimeValues::from(self.inner.with_timezone(&Local))
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts individual date & time values from this
|
||||
`DateTime`, using the universal (UTC) time zone.
|
||||
*/
|
||||
pub fn to_universal_time(self) -> DateTimeValues {
|
||||
DateTimeValues::from(self.inner.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
/**
|
||||
Formats a time string in the ISO 8601 format, such as `1996-12-19T16:39:57-08:00`.
|
||||
|
||||
See [`chrono::DateTime::to_rfc3339`] for additional details.
|
||||
*/
|
||||
pub fn to_iso_date(self) -> String {
|
||||
self.inner.to_rfc3339()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DateTime {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp()));
|
||||
fields.add_field_method_get("unixTimestampMillis", |_, this| {
|
||||
Ok(this.inner.timestamp_millis())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Metamethods to compare DateTime as instants in time
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Eq,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| Ok(this.eq(&other)),
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Lt,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
||||
Ok(matches!(this.cmp(&other), Ordering::Less))
|
||||
},
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Le,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
||||
Ok(matches!(this.cmp(&other), Ordering::Less | Ordering::Equal))
|
||||
},
|
||||
);
|
||||
// Normal methods
|
||||
methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_iso_date()));
|
||||
methods.add_method(
|
||||
"formatUniversalTime",
|
||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
||||
Ok(this.format_string_universal(format.as_deref(), locale.as_deref()))
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"formatLocalTime",
|
||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
||||
Ok(this.format_string_local(format.as_deref(), locale.as_deref()))
|
||||
},
|
||||
);
|
||||
methods.add_method("toUniversalTime", |_, this: &Self, ()| {
|
||||
Ok(this.to_universal_time())
|
||||
});
|
||||
methods.add_method("toLocalTime", |_, this: &Self, ()| Ok(this.to_local_time()));
|
||||
}
|
||||
}
|
|
@ -4,6 +4,12 @@ use mlua::prelude::*;
|
|||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
mod date_time;
|
||||
mod result;
|
||||
mod values;
|
||||
|
||||
use self::date_time::DateTime;
|
||||
|
||||
/**
|
||||
Creates the `datetime` standard library module.
|
||||
|
||||
|
@ -12,5 +18,19 @@ use lune_utils::TableBuilder;
|
|||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?.build_readonly()
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("fromIsoDate", |_, iso_date: String| {
|
||||
Ok(DateTime::from_iso_date(iso_date)?)
|
||||
})?
|
||||
.with_function("fromLocalTime", |_, values| {
|
||||
Ok(DateTime::from_local_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUniversalTime", |_, values| {
|
||||
Ok(DateTime::from_universal_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUnixTimestamp", |_, timestamp| {
|
||||
Ok(DateTime::from_unix_timestamp_float(timestamp)?)
|
||||
})?
|
||||
.with_function("now", |_, ()| Ok(DateTime::now()))?
|
||||
.build_readonly()
|
||||
}
|
||||
|
|
32
crates/lune-std-datetime/src/result.rs
Normal file
32
crates/lune-std-datetime/src/result.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub type DateTimeResult<T, E = DateTimeError> = Result<T, E>;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum DateTimeError {
|
||||
#[error("invalid date")]
|
||||
InvalidDate,
|
||||
#[error("invalid time")]
|
||||
InvalidTime,
|
||||
#[error("ambiguous date or time")]
|
||||
Ambiguous,
|
||||
#[error("date or time is outside allowed range")]
|
||||
OutOfRangeUnspecified,
|
||||
#[error("{name} must be within range {min} -> {max}, got {value}")]
|
||||
OutOfRange {
|
||||
name: &'static str,
|
||||
value: String,
|
||||
min: String,
|
||||
max: String,
|
||||
},
|
||||
#[error(transparent)]
|
||||
ParseError(#[from] chrono::ParseError),
|
||||
}
|
||||
|
||||
impl From<DateTimeError> for LuaError {
|
||||
fn from(value: DateTimeError) -> Self {
|
||||
LuaError::runtime(value.to_string())
|
||||
}
|
||||
}
|
170
crates/lune-std-datetime/src/values.rs
Normal file
170
crates/lune-std-datetime/src/values.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
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<Self> {
|
||||
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<T>(name: &'static str, value: T, min: T, max: T) -> DateTimeResult<T>
|
||||
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<Self> {
|
||||
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<LuaValue> {
|
||||
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<T: TimeZone> From<DateTime<T>> for DateTimeValues {
|
||||
fn from(value: DateTime<T>) -> 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<DateTimeValues> for DateTime<Utc> {
|
||||
type Error = DateTimeError;
|
||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
||||
Utc.with_ymd_and_hms(
|
||||
value.year,
|
||||
value.month,
|
||||
value.day,
|
||||
value.hour,
|
||||
value.minute,
|
||||
value.second,
|
||||
)
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DateTimeValues> for DateTime<Local> {
|
||||
type Error = DateTimeError;
|
||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
||||
Local
|
||||
.with_ymd_and_hms(
|
||||
value.year,
|
||||
value.month,
|
||||
value.day,
|
||||
value.hour,
|
||||
value.minute,
|
||||
value.second,
|
||||
)
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue