Refactor DateTime builtin to be a wrapper around chrono, improve error handling, misc smaller changes and consistency improvements

This commit is contained in:
Filip Tibell 2023-09-15 23:24:05 -05:00
parent 7a05563f0f
commit fad48b9603
No known key found for this signature in database
19 changed files with 900 additions and 895 deletions

View file

@ -26,6 +26,7 @@ cli = [
"dep:clap",
"dep:include_dir",
"dep:regex",
"dep:rustyline",
]
roblox = [
"dep:glam",
@ -116,7 +117,6 @@ num-traits = "0.2"
anyhow = { optional = true, version = "1.0" }
env_logger = { optional = true, version = "0.10" }
itertools = { optional = true, version = "0.11" }
clap = { optional = true, version = "4.1", features = ["derive"] }
include_dir = { optional = true, version = "0.7", features = ["glob"] }
regex = { optional = true, version = "1.7", default-features = false, features = [

View file

@ -1,177 +0,0 @@
use crate::lune::builtins::datetime::date_time::Timezone;
use chrono::prelude::*;
use chrono_lc::LocaleDate;
#[derive(Copy, Clone, Debug)]
pub struct DateTimeBuilder {
/// The year. In the range 1400 - 9999.
pub year: i32,
/// The month. In the range 1 - 12.
pub month: u32,
/// The day. In the range 1 - 31.
pub day: u32,
/// The hour. In the range 0 - 23.
pub hour: u32,
/// The minute. In the range 0 - 59.
pub minute: u32,
/// The second. In the range usually 0 - 59, but sometimes 0 - 60 to accommodate leap seconds in certain systems.
pub second: u32,
/// The milliseconds. In the range 0 - 999.
pub millisecond: u32,
}
impl Default for DateTimeBuilder {
/// Constructs the default state for DateTimeBuilder, which is the Unix Epoch.
fn default() -> Self {
Self {
year: 1970,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
}
}
}
impl DateTimeBuilder {
/// Builder method to set the `Year`.
pub fn with_year(&mut self, year: i32) -> &mut Self {
self.year = year;
self
}
/// Builder method to set the `Month`.
pub fn with_month(&mut self, month: Month) -> &mut Self {
// THe Month enum casts to u32 starting at zero, so we add one to it
self.month = month as u32 + 1;
self
}
/// Builder method to set the `Month`.
pub fn with_day(&mut self, day: u32) -> &mut Self {
self.day = day;
self
}
/// Builder method to set the `Hour`.
pub fn with_hour(&mut self, hour: u32) -> &mut Self {
self.hour = hour;
self
}
/// Builder method to set the `Minute`.
pub fn with_minute(&mut self, minute: u32) -> &mut Self {
self.minute = minute;
self
}
/// Builder method to set the `Second`.
pub fn with_second(&mut self, second: u32) -> &mut Self {
self.second = second;
self
}
/// Builder method to set the `Millisecond`.
pub fn with_millisecond(&mut self, millisecond: u32) -> &mut Self {
self.millisecond = millisecond;
self
}
/// Converts the `DateTimeBuilder` to a string with a specified format and locale.
pub fn to_string<T>(
self,
timezone: Timezone,
format: Option<T>,
locale: Option<T>,
) -> Result<String, ()>
where
T: ToString,
{
let format = match format {
Some(fmt) => fmt.to_string(),
None => "%Y-%m-%dT%H:%M:%SZ".to_string(),
};
let locale = match locale {
Some(locale) => locale.to_string(),
None => "en".to_string(),
};
let time = Utc
.with_ymd_and_hms(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
)
.single()
.ok_or(())?;
// dbg!(
// "{}",
// match timezone {
// Timezone::Utc => time.to_rfc3339(), //.formatl((format).as_str(), locale.as_str()),
// Timezone::Local => time.with_timezone(&Local).to_rfc3339(), // .formatl((format).as_str(), locale.as_str()),
// }
// );
Ok(match timezone {
Timezone::Utc => time.formatl((format).as_str(), locale.as_str()),
Timezone::Local => time
.with_timezone(&Local)
.formatl((format).as_str(), locale.as_str()),
}
.to_string())
// .formatl((format).as_str(), locale.as_str())
// .to_string())
// Ok(match timezone {
// Timezone::Utc => Utc
// .with_ymd_and_hms(
// self.year,
// self.month,
// self.day,
// self.hour,
// self.minute,
// self.second,
// )
// .single()
// .ok_or(())?
// .with_timezone(&match timezone {
// Timezone::Utc => Utc,
// Timezone::Local => Local
// })
// .formatl((format).as_str(), locale.as_str())
// .to_string(),
// Timezone::Local => Local
// .with_ymd_and_hms(
// self.year,
// self.month,
// self.day,
// self.hour,
// self.minute,
// self.second,
// )
// .single()
// .ok_or(())?
// .formatl((format).as_str(), locale.as_str())
// .to_string(),
// })
}
pub fn build(self) -> Self {
self
}
}

View file

@ -1,243 +0,0 @@
use crate::lune::builtins::datetime::builder::DateTimeBuilder;
use chrono::prelude::*;
use chrono::DateTime as ChronoDateTime;
use num_traits::FromPrimitive;
/// Possible types of timestamps accepted by `DateTime`.
pub enum TimestampType {
Seconds,
Millis,
}
/// General timezone types accepted by `DateTime` methods.
#[derive(Eq, PartialEq)]
pub enum Timezone {
Utc,
Local,
}
#[derive(Clone)]
pub struct DateTime {
/// The number of **seconds** since January 1st, 1970
/// at 00:00 UTC (the Unix epoch). Range is
/// -17,987,443,200 to 253,402,300,799, approximately
/// years 14009999.
pub unix_timestamp: i64,
/// The number of **milliseconds* since January 1st, 1970
/// at 00:00 UTC (the Unix epoch). Range is -17,987,443,200,000
/// to 253,402,300,799,999, approximately years 14009999.
pub unix_timestamp_millis: i64,
}
impl DateTime {
/// Returns a `DateTime` representing the current moment in time.
pub fn now() -> Self {
let time = Utc::now();
Self {
unix_timestamp: time.timestamp(),
unix_timestamp_millis: time.timestamp_millis(),
}
}
/// Returns a new `DateTime` object from the given unix timestamp, in either seconds on
/// milliseconds. In case of failure, defaults to the (seconds or
/// milliseconds) since January 1st, 1970 at 00:00 (UTC).
pub fn from_unix_timestamp(timestamp_kind: TimestampType, unix_timestamp: i64) -> Self {
let time_chrono = match timestamp_kind {
TimestampType::Seconds => NaiveDateTime::from_timestamp_opt(unix_timestamp, 0),
TimestampType::Millis => NaiveDateTime::from_timestamp_millis(unix_timestamp),
};
if let Some(time) = time_chrono {
Self {
unix_timestamp: time.timestamp(),
unix_timestamp_millis: time.timestamp_millis(),
}
} else {
Self::now()
}
}
/// Returns a new `DateTime` using the given units from a UTC time. The
/// values accepted are similar to those found in the time value table
/// returned by `to_universal_time`.
///
/// - Date units (year, month, day) that produce an invalid date will raise an error. For example, January 32nd or February 29th on a non-leap year.
/// - Time units (hour, minute, second, millisecond) that are outside their normal range are valid. For example, 90 minutes will cause the hour to roll over by 1; -10 seconds will cause the minute value to roll back by 1.
/// - Non-integer values are rounded down. For example, providing 2.5 hours will be equivalent to providing 2 hours, not 2 hours 30 minutes.
/// - Omitted values are assumed to be their lowest value in their normal range, except for year which defaults to 1970.
pub fn from_universal_time(date_time: Option<DateTimeBuilder>) -> Result<Self, ()> {
Ok(match date_time {
Some(date_time) => {
let utc_time: ChronoDateTime<Utc> = Utc.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(date_time.year, date_time.month, date_time.day)
.ok_or(())?,
NaiveTime::from_hms_milli_opt(
date_time.hour,
date_time.minute,
date_time.second,
date_time.millisecond,
)
.ok_or(())?,
));
Self {
unix_timestamp: utc_time.timestamp(),
unix_timestamp_millis: utc_time.timestamp_millis(),
}
}
None => Self::now(),
})
}
/// Returns a new `DateTime` using the given units from a local time. The
/// values accepted are similar to those found in the time value table
/// returned by `to_local_time`.
///
/// - Date units (year, month, day) that produce an invalid date will raise an error. For example, January 32nd or February 29th on a non-leap year.
/// - Time units (hour, minute, second, millisecond) that are outside their normal range are valid. For example, 90 minutes will cause the hour to roll over by 1; -10 seconds will cause the minute value to roll back by 1.
/// - Non-integer values are rounded down. For example, providing 2.5 hours will be equivalent to providing 2 hours, not 2 hours 30 minutes.
/// - Omitted values are assumed to be their lowest value in their normal range, except for year which defaults to 1970.
pub fn from_local_time(date_time: Option<DateTimeBuilder>) -> Result<Self, ()> {
Ok(match date_time {
Some(date_time) => {
let local_time: ChronoDateTime<Local> = Local
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(date_time.year, date_time.month, date_time.day)
.ok_or(())?,
NaiveTime::from_hms_milli_opt(
date_time.hour,
date_time.minute,
date_time.second,
date_time.millisecond,
)
.ok_or(())?,
))
.single()
.ok_or(())?;
Self {
unix_timestamp: local_time.timestamp(),
unix_timestamp_millis: local_time.timestamp_millis(),
}
}
None => {
let local_time = Local::now();
Self {
unix_timestamp: local_time.timestamp(),
unix_timestamp_millis: local_time.timestamp_millis(),
}
}
})
}
/// Returns a `DateTime` from an ISO 8601 date-time string in UTC
/// time, such as those returned by `to_iso_date`. If the
/// string parsing fails, the function returns `None`.
///
/// An example ISO 8601 date-time string would be `2020-01-02T10:30:45Z`,
/// which represents January 2nd 2020 at 10:30 AM, 45 seconds.
pub fn from_iso_date<T>(iso_date: T) -> Option<Self>
where
T: ToString,
{
let time = ChronoDateTime::parse_from_str(
format!("{}{}", iso_date.to_string(), "UTC+0000").as_str(),
"%Y-%m-%dT%H:%M:%SZUTC%z",
)
.ok()?;
Some(Self {
unix_timestamp: time.timestamp(),
unix_timestamp_millis: time.timestamp_millis(),
})
}
/// Converts the value of this `DateTime` object to local time. The returned table
/// contains the following keys: `Year`, `Month`, `Day`, `Hour`, `Minute`, `Second`,
/// `Millisecond`. For more details, see the time value table in this data type's
/// description. The values within this table could be passed to `from_local_time`
/// to produce the original `DateTime` object.
pub fn to_datetime_builder<T>(date_time: ChronoDateTime<T>) -> Result<DateTimeBuilder, ()>
where
T: TimeZone,
{
let mut date_time_constructor = DateTimeBuilder::default();
date_time_constructor
.with_year(date_time.year())
.with_month(Month::from_u32(date_time.month()).ok_or(())?)
.with_day(date_time.day())
.with_hour(date_time.hour())
.with_minute(date_time.minute())
.with_second(date_time.second());
Ok(date_time_constructor)
}
/// Converts the value of this `DateTime` object to local time. The returned table
/// contains the following keys: `Year`, `Month`, `Day`, `Hour`, `Minute`, `Second`,
/// `Millisecond`. For more details, see the time value table in this data type's
/// description. The values within this table could be passed to `from_local_time`
/// to produce the original `DateTime` object.
pub fn to_local_time(&self) -> Result<DateTimeBuilder, ()> {
Self::to_datetime_builder(
Local
.timestamp_opt(self.unix_timestamp, 0)
.single()
.ok_or(())?,
)
}
/// Converts the value of this `DateTime` object to Universal Coordinated Time (UTC).
/// The returned table contains the following keys: `Year`, `Month`, `Day`, `Hour`,
/// `Minute`, `Second`, `Millisecond`. For more details, see the time value table
/// in this data type's description. The values within this table could be passed
/// to `from_universal_time` to produce the original `DateTime` object.
pub fn to_universal_time(&self) -> Result<DateTimeBuilder, ()> {
Self::to_datetime_builder(
Utc.timestamp_opt(self.unix_timestamp, 0)
.single()
.ok_or(())?,
)
// dbg!("{:#?}", m?);
// m
}
/// Formats a date as a ISO 8601 date-time string, returns None if the DateTime object is invalid.
/// The value returned by this function could be passed to `from_local_time` to produce the
/// original `DateTime` object.
pub fn to_iso_date(&self) -> Result<String, ()> {
self.to_universal_time()?
.to_string::<&str>(Timezone::Utc, None, None)
}
// There seems to be only one localization crate for chrono,
// which has been committed to last 5 years ago. Thus, this crate doesn't
// work with the version of chrono we're using. I've forked the crate
// and have made it compatible with the latest version of chrono. ~ DevComp
// TODO: Implement more locales for chrono-locale.
/// Generates a string from the `DateTime` value interpreted as the specified timezone
/// and a format string. The format string should contain tokens, which will
/// replace to certain date/time values described by the `DateTime` object.
/// For more details, see the [accepted formatter tokens](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
pub fn format_time<T>(&self, timezone: Timezone, fmt_str: T, locale: T) -> Result<String, ()>
where
T: ToString,
{
self.to_universal_time()?.to_string(
timezone,
Some(fmt_str.to_string()),
Some(locale.to_string()),
)
}
}

View 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())
}
}

View file

@ -1,176 +1,251 @@
use chrono::Month;
use std::cmp::Ordering;
use mlua::prelude::*;
pub(crate) mod builder;
pub(crate) mod date_time;
use chrono::prelude::*;
use chrono::DateTime as ChronoDateTime;
use chrono_lc::LocaleDate;
use self::{
builder::DateTimeBuilder,
date_time::{DateTime, TimestampType, Timezone},
};
use crate::lune::util::TableBuilder;
// TODO: Proper error handling and stuff
mod error;
mod values;
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
use error::*;
use values::*;
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("now", |_, ()| Ok(DateTime::now()))?
.with_function("fromUnixTimestamp", |lua, timestamp: LuaValue| {
let timestamp_cloned = timestamp.clone();
let timestamp_kind = TimestampType::from_lua(timestamp, lua)?;
let timestamp = match timestamp_kind {
TimestampType::Seconds => timestamp_cloned.as_i64().ok_or(LuaError::external("invalid float integer timestamp supplied"))?,
TimestampType::Millis => {
let timestamp = timestamp_cloned.as_f64().ok_or(LuaError::external("invalid float timestamp with millis component supplied"))?;
((((timestamp - timestamp.fract()) as u64) * 1000_u64) // converting the whole seconds part to millis
// the ..3 gets a &str of the first 3 chars of the digits after the decimals, ignoring
// additional floating point accuracy digits
+ (timestamp.fract() * (10_u64.pow(timestamp.fract().to_string().split('.').collect::<Vec<&str>>()[1][..3].len() as u32)) as f64) as u64) as i64
// adding the millis to the fract as a whole number
// HACK: 10 ** (timestamp.fract().to_string().len() - 2) gives us the number of digits
// after the decimal
}
};
Ok(DateTime::from_unix_timestamp(timestamp_kind, timestamp))
})?
.with_function("fromUniversalTime", |lua, date_time: LuaValue| {
Ok(DateTime::from_universal_time(DateTimeBuilder::from_lua(date_time, lua).ok()).or(Err(LuaError::external("invalid DateTimeValues provided to fromUniversalTime"))))
})?
.with_function("fromLocalTime", |lua, date_time: LuaValue| {
Ok(DateTime::from_local_time(DateTimeBuilder::from_lua(date_time, lua).ok()).or(Err(LuaError::external("invalid DateTimeValues provided to fromLocalTime"))))
})?
.with_function("fromIsoDate", |_, iso_date: String| {
Ok(DateTime::from_iso_date(iso_date))
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()
}
impl<'lua> FromLua<'lua> for TimestampType {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::Integer(_) => Ok(TimestampType::Seconds),
LuaValue::Number(num) => Ok(if num.fract() == 0.0 {
TimestampType::Seconds
} else {
TimestampType::Millis
}),
_ => Err(LuaError::external(
"Invalid enum type, number or integer expected",
)),
}
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
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.unix_timestamp));
fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp()));
fields.add_field_method_get("unixTimestampMillis", |_, this| {
Ok(this.unix_timestamp_millis)
Ok(this.inner.timestamp_millis())
});
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("toIsoDate", |_, this, ()| {
Ok(this
.to_iso_date()
.map_err(|()| LuaError::external("failed to parse DateTime object, invalid")))
});
methods.add_method(
"formatTime",
|_, this, (timezone, fmt_str, locale): (LuaValue, String, String)| {
Ok(this
.format_time(Timezone::from_lua(timezone, &Lua::new())?, fmt_str, locale)
.map_err(|()| LuaError::external("failed to parse DateTime object, invalid")))
// 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_method("toUniversalTime", |_, this: &DateTime, ()| {
Ok(this.to_universal_time().or(Err(LuaError::external(
"invalid DateTime self argument provided to toUniversalTime",
))))
});
methods.add_method("toLocalTime", |_, this: &DateTime, ()| {
Ok(this.to_local_time().or(Err(LuaError::external(
"invalid DateTime self argument provided to toLocalTime",
))))
});
}
}
impl<'lua> FromLua<'lua> for DateTime {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::Nil => Err(LuaError::external(
"expected self of type DateTime, found nil",
)),
LuaValue::Table(t) => Ok(DateTime::from_unix_timestamp(
TimestampType::Seconds,
t.get("unixTimestamp")?,
)),
_ => Err(LuaError::external("invalid type for DateTime self arg")),
}
}
}
impl LuaUserData for DateTimeBuilder {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("year", |_, this| Ok(this.year));
fields.add_field_method_get("month", |_, this| Ok(this.month));
fields.add_field_method_get("day", |_, this| Ok(this.day));
fields.add_field_method_get("hour", |_, this| Ok(this.hour));
fields.add_field_method_get("minute", |_, this| Ok(this.minute));
fields.add_field_method_get("second", |_, this| Ok(this.second));
fields.add_field_method_get("millisecond", |_, this| Ok(this.millisecond));
}
}
impl<'lua> FromLua<'lua> for DateTimeBuilder {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::Table(t) => Ok(Self::default()
.with_year(t.get("year")?)
.with_month(
(match t.get("month")? {
LuaValue::String(str) => Ok(str.to_str()?.parse::<Month>().or(Err(
LuaError::external("could not cast month string to Month"),
))?),
LuaValue::Nil => {
Err(LuaError::external("cannot find mandatory month argument"))
}
LuaValue::Number(num) => Ok(Month::try_from(num as u8).or(Err(
LuaError::external("could not cast month number to Month"),
))?),
LuaValue::Integer(int) => Ok(Month::try_from(int as u8).or(Err(
LuaError::external("could not cast month integer to Month"),
))?),
_ => Err(LuaError::external("unexpected month field type")),
})?,
)
.with_day(t.get("day")?)
.with_hour(t.get("hour")?)
.with_minute(t.get("minute")?)
.with_second(t.get("second")?)
.with_millisecond(t.get("millisecond").or(LuaResult::Ok(0))?)
.build()),
_ => Err(LuaError::external(
"expected type table for DateTimeBuilder",
)),
}
}
}
impl<'lua> FromLua<'lua> for Timezone {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::String(str) => match str.to_str()?.to_lowercase().as_str() {
"utc" => Ok(Timezone::Utc),
"local" => Ok(Timezone::Local),
&_ => Err(LuaError::external("Invalid enum member!")),
methods.add_meta_method(
LuaMetaMethod::Le,
|_, this: &Self, other: LuaUserDataRef<Self>| {
Ok(matches!(this.cmp(&other), Ordering::Less | Ordering::Equal))
},
_ => Err(LuaError::external("Invalid enum type, string expected")),
}
);
// 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()));
}
}

View file

@ -0,0 +1,170 @@
use mlua::prelude::*;
use chrono::prelude::*;
use crate::lune::util::TableBuilder;
use super::error::{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)
}
}

View file

@ -38,6 +38,17 @@ macro_rules! create_tests {
}
create_tests! {
datetime_format_local_time: "datetime/formatLocalTime",
datetime_format_universal_time: "datetime/formatUniversalTime",
datetime_from_iso_date: "datetime/fromIsoDate",
datetime_from_local_time: "datetime/fromLocalTime",
datetime_from_universal_time: "datetime/fromUniversalTime",
datetime_from_unix_timestamp: "datetime/fromUnixTimestamp",
datetime_now: "datetime/now",
datetime_to_iso_date: "datetime/toIsoDate",
datetime_to_local_time: "datetime/toLocalTime",
datetime_to_universal_time: "datetime/toUniversalTime",
fs_files: "fs/files",
fs_copy: "fs/copy",
fs_dirs: "fs/dirs",
@ -106,16 +117,6 @@ create_tests! {
task_delay: "task/delay",
task_spawn: "task/spawn",
task_wait: "task/wait",
datetime_now: "datetime/now",
datetime_from_unix_timestamp: "datetime/fromUnixTimestamp",
datetime_from_universal_time: "datetime/fromUniversalTime",
datetime_to_universal_time: "datetime/toUniversalTime",
datetime_from_local_time: "datetime/fromLocalTime",
datetime_to_local_time: "datetime/toLocalTime",
datetime_from_iso_date: "datetime/fromIsoDate",
datetime_to_iso_date: "datetime/toIsoDate",
datetime_format_time: "datetime/formatTime",
}
#[cfg(feature = "roblox")]

View file

@ -0,0 +1,53 @@
local DateTime = require("@lune/datetime")
local process = require("@lune/process")
local expectedTimeString = os.date("%Y-%m-%dT%H:%M:%S", 1694078954)
assert(
DateTime.fromUnixTimestamp(1694078954):formatLocalTime("%Y-%m-%dT%H:%M:%S", "en")
== expectedTimeString,
"invalid ISO 8601 formatting for DateTime.formatLocalTime()"
)
--[[
The rest of this test requires 'fr_FR.UTF-8 UTF-8' to be in /etc/locale.gen to pass
Locale should be set up by a script, or by the user,
or in CI, test runner takes no responsibility for this
To run tests related to locales, one must
explicitly provide the `--test-locales` flag
]]
local runLocaleTests = false
for _, arg in process.args do
if arg == "--test-locales" then
runLocaleTests = true
break
end
end
if not runLocaleTests then
return
end
local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
env = {
LC_ALL = "fr_FR.UTF-8 ",
},
})
assert(dateCmd.ok, "Failed to execute date command")
local expectedLocalizedString = string.gsub(dateCmd.stdout, "\n", "")
assert(
DateTime.fromUnixTimestamp(1693068988):formatLocalTime("%A, %d %B %Y", "fr")
== expectedLocalizedString,
`expected format specifier '%A, %d %B %Y' to return '{expectedLocalizedString}' for locale 'fr' (local)`
)
assert(
DateTime.fromUnixTimestamp(1693068988):formatUniversalTime("%A, %d %B %Y", "fr")
== "samedi, 26 août 2023",
"expected format specifier '%A, %d %B %Y' to return 'samedi, 26 août 2023' for locale 'fr' (UTC)"
)

View file

@ -1,70 +0,0 @@
local DateTime = require("@lune/DateTime")
local process = require("@lune/Process")
-- UTC Timezone
assert(
DateTime.fromUnixTimestamp(1693068988):formatTime("utc", "%Y-%m-%dT%H:%M:%SZ", "en")
== "2023-08-26T16:56:28Z",
"invalid ISO 8601 formatting for DateTime.formatTime() (UTC)"
)
local expectedTimeString = os.date("%Y-%m-%dT%H:%M:%SZ", 1694078954)
assert(
DateTime.fromUnixTimestamp(1694078954):formatTime("local", "%Y-%m-%dT%H:%M:%SZ", "en")
== expectedTimeString,
"invalid ISO 8601 formatting for DateTime.formatTime() (local)"
)
-- This test requires 'fr_FR.UTF-8 UTF-8' to be in /etc/locale.gen to pass
-- Locale should be set up by a script, or by the user, or in CI, test runner
-- takes no responsibility for this
-- Local Timezone
assert(
DateTime.fromUnixTimestamp(1694078954):formatTime("local", "%Y-%m-%dT%H:%M:%SZ", "en")
== expectedTimeString,
"invalid ISO 8601 formatting for DateTime.formatTime() (local)"
)
-- To run tests related to locales, one must explicitly
-- provide the `--test-locales` flag
local toTestLocales = false
for _, arg in process.args do
if arg == "--test-locales" then
toTestLocales = true
break
end
end
if not toTestLocales then
return
end
local expectedLocalizedString
local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
env = {
LC_ALL = "fr_FR.UTF-8 ",
},
})
if dateCmd.ok then
expectedLocalizedString = dateCmd.stdout:gsub("\n", "")
else
error("Failed to execute date command")
end
assert(
DateTime.fromUnixTimestamp(1693068988):formatTime("local", "%A, %d %B %Y", "fr")
== expectedLocalizedString,
`expected format specifier '%A, %d %B %Y' to return '{expectedLocalizedString}' for locale 'fr' (local)`
)
assert(
DateTime.fromUnixTimestamp(1693068988):formatTime("utc", "%A, %d %B %Y", "fr")
== "samedi, 26 août 2023",
"expected format specifier '%A, %d %B %Y' to return 'samedi, 26 août 2023' for locale 'fr' (UTC)"
)

View file

@ -0,0 +1,15 @@
local DateTime = require("@lune/datetime")
assert(
DateTime.fromUnixTimestamp(1693068988):formatUniversalTime("%Y-%m-%dT%H:%M:%SZ", "en")
== "2023-08-26T16:56:28Z",
"invalid ISO 8601 formatting for DateTime.formatTime() (UTC)"
)
local expectedTimeString = os.date("%Y-%m-%dT%H:%M:%SZ", 1694078954)
assert(
DateTime.fromUnixTimestamp(1694078954):formatLocalTime("%Y-%m-%dT%H:%M:%SZ", "en")
== expectedTimeString,
"invalid ISO 8601 formatting for DateTime.formatTime()"
)

View file

@ -1,4 +1,4 @@
local DateTime = require("@lune/DateTime")
local DateTime = require("@lune/datetime")
assert(
DateTime.fromIsoDate("2023-08-26T16:56:28Z") ~= nil,

View file

@ -1,45 +1,41 @@
local DateTime = require("@lune/DateTime")
assert(
DateTime.fromLocalTime()["unixTimestamp"] == os.time(),
"expected DateTime.fromLocalTime() with no args to return DateTime at current moment"
)
local DateTime = require("@lune/datetime")
local timeValues1 = os.date("*t", 1693049188)
assert(
DateTime.fromLocalTime({
["year"] = timeValues1.year,
["month"] = timeValues1.month,
["day"] = timeValues1.day,
["hour"] = timeValues1.hour,
["minute"] = timeValues1.min,
["second"] = timeValues1.sec,
["millisecond"] = 0,
})["unixTimestamp"] == 1693049188,
year = timeValues1.year,
month = timeValues1.month,
day = timeValues1.day,
hour = timeValues1.hour,
minute = timeValues1.min,
second = timeValues1.sec,
millisecond = 0,
}).unixTimestamp == 1693049188,
"expected DateTime.fromLocalTime() with DateTimeValues arg to return 1693049188s"
)
print(DateTime.fromLocalTime({
["year"] = 2023,
["month"] = "aug",
["day"] = 26,
["hour"] = 16,
["minute"] = 56,
["second"] = 28,
["millisecond"] = 892,
})["unixTimestamp"])
year = 2023,
month = 8,
day = 26,
hour = 16,
minute = 56,
second = 28,
millisecond = 892,
}).unixTimestamp)
local timeValues2 = os.date("*t", 1693049188.892)
assert(
DateTime.fromLocalTime({
["year"] = timeValues2.year,
["month"] = timeValues2.month,
["day"] = timeValues2.day,
["hour"] = timeValues2.hour,
["minute"] = timeValues2.min,
["second"] = timeValues2.sec,
["millisecond"] = 892,
})["unixTimestampMillis"] == 1693049188892,
year = timeValues2.year,
month = timeValues2.month,
day = timeValues2.day,
hour = timeValues2.hour,
minute = timeValues2.min,
second = timeValues2.sec,
millisecond = 892,
}).unixTimestampMillis == 1693049188892,
"expected DateTime.fromLocalTime() with DateTimeValues arg with millis to return 1693049188892ms"
)

View file

@ -1,32 +1,27 @@
local DateTime = require("@lune/DateTime")
assert(
math.abs(DateTime.fromUniversalTime()["unixTimestamp"] - os.time()) <= 3,
"expected DateTime.fromLocalTime() with no args to return DateTime at the current moment"
)
local DateTime = require("@lune/datetime")
assert(
DateTime.fromUniversalTime({
["year"] = 2023,
["month"] = "aug",
["day"] = 26,
["hour"] = 16,
["minute"] = 56,
["second"] = 28,
["millisecond"] = 0,
})["unixTimestamp"] == 1693068988,
year = 2023,
month = 8,
day = 26,
hour = 16,
minute = 56,
second = 28,
millisecond = 0,
}).unixTimestamp == 1693068988,
"expected DateTime.fromUniversalTime() with DateTimeValues arg to return 1693068988s"
)
assert(
DateTime.fromUniversalTime({
["year"] = 2023,
["month"] = "aug",
["day"] = 26,
["hour"] = 16,
["minute"] = 56,
["second"] = 28,
["millisecond"] = 892,
})["unixTimestampMillis"] == 1693068988892,
year = 2023,
month = 8,
day = 26,
hour = 16,
minute = 56,
second = 28,
millisecond = 892,
}).unixTimestampMillis == 1693068988892,
"expected DateTime.fromUniversalTime() with DateTimeValues arg with millis to return 1693068988892ms"
)

View file

@ -1,16 +1,16 @@
local DateTime = require("@lune/DateTime")
local DateTime = require("@lune/datetime")
-- Bug in rust side implementation for fromUnixTimestamp, calculation for conversion there is wonky,
-- a difference of few millis causes differences as whole seconds for some reason
assert(
DateTime.fromUnixTimestamp(0000.892)["unixTimestampMillis"] == (0 * 1000) + 892,
DateTime.fromUnixTimestamp(0000.892).unixTimestampMillis == (0 * 1000) + 892,
"expected DateTime.fromUnixTimestamp() with millis float to return correct millis timestamp"
)
-- We subtract one due to the floating point accuracy... Need to fix later
assert(
DateTime.fromUnixTimestamp(1693114921.632)["unixTimestampMillis"]
DateTime.fromUnixTimestamp(1693114921.632).unixTimestampMillis
== ((1693114921 * 1000) + 632) - 1,
"expected DateTime.fromUnixTimestamp() with millis and seconds float to return correct millis timestamp"
)

View file

@ -1,8 +1,7 @@
local DateTime = require("@lune/DateTime")
local DateTime = require("@lune/datetime")
local TYPE = "DateTime"
assert(
typeof(DateTime.now()) == TYPE,
`dateTime.now() should return a {TYPE}, returned {tostring(typeof(DateTime.now()))}`
)
local now = DateTime.now()
assert(typeof(now) == TYPE, `dateTime.now() should return a {TYPE}, returned {typeof(now)}`)

View file

@ -1,9 +1,72 @@
local DateTime = require("@lune/DateTime")
local DateTime = require("@lune/datetime")
local now = DateTime.now()
local nowIso = now:toIsoDate()
-- Make sure we have separator characters, T to separate date & time, + or Z to separate timezone
local dateTimeSplitIdx = string.find(nowIso, "T")
local timezoneSplitIdx = string.find(nowIso, "+")
local timezoneZeroedIdx = string.find(nowIso, "Z")
assert(dateTimeSplitIdx ~= nil, "Missing date & time separator 'T' in iso 8601 string")
assert(
string.match(
DateTime.now():toIsoDate(),
"(%d%d%d%d)-?(%d?%d?)-?(%d?%d?)T(%d?%d?):(%d?%d?):(%d?%d?)Z$"
),
"invalid ISO 8601 date returned by dateTime.toIsoDate()"
timezoneSplitIdx ~= nil or timezoneZeroedIdx ~= nil,
"Missing timezone separator '+' or 'Z' in iso date string"
)
-- Split date (before T) by dashes, split time (after T, before + or Z)
-- by colons, we should then get 3 substrings for each of date & time
local dateParts = string.split(string.sub(nowIso, 1, dateTimeSplitIdx - 1), "-")
local timeParts = string.split(
string.sub(
nowIso,
dateTimeSplitIdx + 1,
((timezoneSplitIdx or timezoneZeroedIdx) :: number) - 1
),
":"
)
assert(#dateParts == 3, "Date partial of iso 8601 should consist of 3 substrings, separated by '-'")
assert(#timeParts == 3, "Time partial of iso 8601 should consist of 3 substrings, separated by ':'")
-- date should be in format YYYY:MM::DD
-- time should be in format HH:MM:SS with optional fraction for seconds
assert(string.match(dateParts[1], "^%d%d%d%d$"), "Date partial should have 4 digits for year")
assert(string.match(dateParts[2], "^%d%d$"), "Date partial should have 2 digits for month")
assert(string.match(dateParts[3], "^%d%d$"), "Date partial should have 2 digits for day")
assert(string.match(timeParts[1], "^%d%d$"), "Time partial should have 2 digits for hour")
assert(string.match(timeParts[2], "^%d%d$"), "Time partial should have 2 digits for minute")
assert(
string.match(timeParts[3], "^%d%d%.?%d*$") and tonumber(timeParts[3]) ~= nil,
"Time partial should have minimum 2 digits with optional fraction for seconds"
)
-- Timezone specifier is either 'Z' for zeroed out timezone (no offset),
-- in which case we don't need to check anything other than it being the
-- last character, or it can be a timezone offset in the format HH::MM
if timezoneZeroedIdx ~= nil then
-- No timezone offset
assert(
timezoneZeroedIdx == #nowIso,
"Timezone specifier 'Z' must be at the last character in iso 8601 string"
)
elseif timezoneSplitIdx ~= nil then
-- Timezone offset
local timezoneParts = string.split(string.sub(nowIso, timezoneSplitIdx + 1), ":")
assert(#timezoneParts == 2, "Timezone partial should consist of 2 substings, separated by ':'")
assert(
string.match(timezoneParts[1], "^%d%d$"),
"Timezone partial should have 2 digits for hour"
)
assert(
string.match(timezoneParts[2], "^%d%d$"),
"Timezone partial should have 2 digits for minute"
)
else
error("unreachable")
end

View file

@ -1,30 +1,30 @@
local DateTime = require("@lune/DateTime")
local DateTime = require("@lune/datetime")
local dateTime = (DateTime.fromIsoDate("2023-08-27T05:54:19Z") :: DateTime.DateTime):toLocalTime()
local values = DateTime.fromIsoDate("2023-08-27T05:54:19Z"):toLocalTime()
local expectedDateTimeValues = os.date("*t", 1693115659)
assert(
dateTime.year == expectedDateTimeValues.year,
`expected {dateTime.year} == {expectedDateTimeValues.year}`
values.year == expectedDateTimeValues.year,
`expected {values.year} == {expectedDateTimeValues.year}`
)
assert(
dateTime.month == expectedDateTimeValues.month,
`expected {dateTime.month} == {expectedDateTimeValues.month}`
values.month == expectedDateTimeValues.month,
`expected {values.month} == {expectedDateTimeValues.month}`
)
assert(
dateTime.day == expectedDateTimeValues.day,
`expected {dateTime.day} == {expectedDateTimeValues.day}`
values.day == expectedDateTimeValues.day,
`expected {values.day} == {expectedDateTimeValues.day}`
)
assert(
dateTime.hour == expectedDateTimeValues.hour,
`expected {dateTime.hour} == {expectedDateTimeValues.hour}`
values.hour == expectedDateTimeValues.hour,
`expected {values.hour} == {expectedDateTimeValues.hour}`
)
assert(
dateTime.minute == expectedDateTimeValues.min,
`expected {dateTime.minute} == {expectedDateTimeValues.min}`
values.minute == expectedDateTimeValues.min,
`expected {values.minute} == {expectedDateTimeValues.min}`
)
assert(
dateTime.second == expectedDateTimeValues.sec,
`expected {dateTime.second} == {expectedDateTimeValues.sec}`
values.second == expectedDateTimeValues.sec,
`expected {values.second} == {expectedDateTimeValues.sec}`
)

View file

@ -1,30 +1,30 @@
local DateTime = require("@lune/DateTime")
local dateTime = (DateTime.fromIsoDate("2023-08-27T05:54:19Z") :: DateTime.DateTime):toLocalTime()
local values = DateTime.fromIsoDate("2023-08-27T05:54:19Z"):toLocalTime()
local expectedDateTimeValues = os.date("*t", 1693115659)
assert(
dateTime.year == expectedDateTimeValues.year,
`expected {dateTime.year} == {expectedDateTimeValues.year}`
values.year == expectedDateTimeValues.year,
`expected {values.year} == {expectedDateTimeValues.year}`
)
assert(
dateTime.month == expectedDateTimeValues.month,
`expected {dateTime.month} == {expectedDateTimeValues.month}`
values.month == expectedDateTimeValues.month,
`expected {values.month} == {expectedDateTimeValues.month}`
)
assert(
dateTime.day == expectedDateTimeValues.day,
`expected {dateTime.day} == {expectedDateTimeValues.day}`
values.day == expectedDateTimeValues.day,
`expected {values.day} == {expectedDateTimeValues.day}`
)
assert(
dateTime.hour == expectedDateTimeValues.hour,
`expected {dateTime.hour} == {expectedDateTimeValues.hour}`
values.hour == expectedDateTimeValues.hour,
`expected {values.hour} == {expectedDateTimeValues.hour}`
)
assert(
dateTime.minute == expectedDateTimeValues.min,
`expected {dateTime.minute} == {expectedDateTimeValues.min}`
values.minute == expectedDateTimeValues.min,
`expected {values.minute} == {expectedDateTimeValues.min}`
)
assert(
dateTime.second == expectedDateTimeValues.sec,
`expected {dateTime.second} == {expectedDateTimeValues.sec}`
values.second == expectedDateTimeValues.sec,
`expected {values.second} == {expectedDateTimeValues.sec}`
)

View file

@ -1,67 +1,184 @@
-- TODO: Add more docs
export type Locale = "en" | "de" | "es" | "fr" | "it" | "ja" | "pl" | "pt-br" | "pt" | "tr"
export type Timezone = "utc" | "local"
export type ShortMonth =
"jan"
| "feb"
| "mar"
| "apr"
| "may"
| "jun"
| "jul"
| "aug"
| "sep"
| "oct"
| "nov"
| "dec"
export type Month =
"january"
| "february"
| "march"
| "april"
| "may"
| "june"
| "july"
| "august"
| "september"
| "october"
| "november"
| "december"
export type DateTimeValues = {
--- Year(s), in the range 1400 -> 9999
year: number,
month: number | ShortMonth | Month,
--- Month(s), in the range 1 -> 12
month: number,
--- Day(s), in the range 1 -> 31
day: number,
--- Hour(s), in the range 0 -> 23
hour: number,
--- Minute(s), in the range 0 -> 59
minute: number,
--- Second(s), in the range 0 -> 60, where 60 is a leap second
second: number,
millisecond: number,
--- Millisecond(s), in the range 0 -> 999
millisecond: number?,
}
local DateTime = {
--- Number of seconds passed since the UNIX epoch.
unixTimestamp = 0,
--- Number of milliseconds passed since the UNIX epoch.
unixTimestampMillis = 0,
}
--[=[
@within DateTime
Formats this `DateTime` as an ISO 8601 date-time string.
Some examples of ISO 8601 date-time strings:
- `2020-02-22T18:12:08Z`
- `2000-01-31T12:34:56+05:00`
- `1970-01-01T00:00:00.055Z`
@return string -- The ISO 8601 formatted string
]=]
function DateTime.toIsoDate(self: DateTime): string
return nil :: any
end
--[=[
@within DateTime
Extracts local time values from this `DateTime`.
The returned table contains the following values:
| Key | Type | Range |
|---------------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
| `millisecond` | `number` | `0 -> 999` |
@return DateTimeValues -- A table of DateTime values
]=]
function DateTime.toLocalTime(self: DateTime): DateTimeValues
return nil :: any
end
--[=[
@within DateTime
Extracts UTC (universal) time values from this `DateTime`.
The returned table contains the following values:
| Key | Type | Range |
|---------------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
| `millisecond` | `number` | `0 -> 999` |
@return DateTimeValues -- A table of DateTime values
]=]
function DateTime.toUniversalTime(self: DateTime): DateTimeValues
return nil :: any
end
--[=[
@within DateTime
Formats this `DateTime` using the given `formatString` and `locale`, as local time.
The given `formatString` is parsed using a `strftime`/`strptime`-inspired
date and time formatting syntax, allowing tokens such as the following:
| Token | Example | Description |
|-------|----------|---------------|
| `%Y` | `1998` | Year number |
| `%m` | `04` | Month number |
| `%d` | `29` | Day number |
| `%A` | `Monday` | Weekday name |
| `%M` | `59` | Minute number |
| `%S` | `10` | Second number |
For a full reference of all available tokens, see the
[chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
If not provided, `formatString` and `locale` will default
to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively.
@param formatString -- A string containing formatting tokens
@param locale -- The locale the time should be formatted in
@return string -- The formatting string
]=]
function DateTime.formatLocalTime(self: DateTime, formatString: string?, locale: Locale?): string
return nil :: any
end
--[=[
@within DateTime
Formats this `DateTime` using the given `formatString` and `locale`, as UTC (universal) time.
The given `formatString` is parsed using a `strftime`/`strptime`-inspired
date and time formatting syntax, allowing tokens such as the following:
| Token | Example | Description |
|-------|----------|---------------|
| `%Y` | `1998` | Year number |
| `%m` | `04` | Month number |
| `%d` | `29` | Day number |
| `%A` | `Monday` | Weekday name |
| `%M` | `59` | Minute number |
| `%S` | `10` | Second number |
For a full reference of all available tokens, see the
[chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
If not provided, `formatString` and `locale` will default
to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively.
@param formatString -- A string containing formatting tokens
@param locale -- The locale the time should be formatted in
@return string -- The formatting string
]=]
function DateTime.formatUniversalTime(
self: DateTime,
formatString: string?,
locale: Locale?
): string
return nil :: any
end
export type DateTime = typeof(DateTime)
--[=[
@class DateTime
Built-in library for date & time manipulation
Built-in library for date & time
### Example usage
```lua
local DateTime = require("@lune/DateTime")
local DateTime = require("@lune/datetime")
-- Returns the current moment in time as a ISO 8601 string
DateTime.now():toIsoDate()
-- Creates a DateTime for the current exact moment in time
local now = DateTime.now()
-- Returns the current moment in time as per the format template in French
DateTime.now():formatTime("utc", "%A, %d %B %Y", "fr")
-- Formats the current moment in time as an ISO 8601 string
print(now:toIsoDate())
-- Formats the current moment in time, using the local
-- time, the Frech locale, and the specified time string
print(now:formatLocalTime("%A, %d %B %Y", "fr"))
-- Returns a specific moment in time as a DateTime instance
DateTime.fromLocalTime({
year = 2023,
month = "aug",
local someDayInTheFuture = DateTime.fromLocalTime({
year = 3033,
month = 8,
day = 26,
hour = 16,
minute = 56,
@ -69,160 +186,139 @@ export type DateTimeValues = {
millisecond = 892,
})
-- Returns the current local time as a DateTime instance
DateTime.fromLocalTime()
-- Extracts the current local date & time as separate values (same values as above table)
print(now:toLocalTime())
-- Returns a DateTime instance from a given float, where the whole denotes the
-- seconds and the fraction denotes the milliseconds
-- Returns a DateTime instance from a given float, where the whole
-- denotes the seconds and the fraction denotes the milliseconds
-- Note that the fraction for millis here is completely optional
DateTime.fromUnixTimestamp(871978212313.321)
-- Returns the current time in terms of UTC
DateTime.now():toUniversalTime()
-- Extracts the current universal (UTC) date & time as separate values
print(now:toUniversalTime())
```
]=]
local dateTime = {
unixTimestamp = 0,
unixTimestampMillis = 0,
}
local dateTime = {}
--[=[
@within DateTime
Returns a `DateTime` representing the current moment in time.
@return A DateTime instance
@return DateTime -- The new DateTime object
]=]
function dateTime.now(): typeof(dateTime)
function dateTime.now(): DateTime
return nil :: any
end
--[=[
@within DateTime
Returns a new `DateTime` object from the given Unix timestamp, or
the number of **seconds** since January 1st, 1970 at 00:00 (UTC).
Creates a new `DateTime` from the given UNIX timestamp.
@param unixTimestamp The number of seconds or milliseconds (or both) since the Unix epoch. The fraction part of a float denotes the milliseconds.
@return A DateTime instance
This timestamp may contain both a whole and fractional part -
where the fractional part denotes milliseconds / nanoseconds.
Example usage of fractions:
- `DateTime.fromUnixTimestamp(123456789.001)` - one millisecond
- `DateTime.fromUnixTimestamp(123456789.000000001)` - one nanosecond
Note that the fractional part has limited precision down to exactly
one nanosecond, any fraction that is more precise will get truncated.
@param unixTimestamp -- Seconds passed since the UNIX epoch
@return DateTime -- The new DateTime object
]=]
function dateTime.fromUnixTimestamp(unixTimestamp: number?): typeof(dateTime)
function dateTime.fromUnixTimestamp(unixTimestamp: number): DateTime
return nil :: any
end
--[=[
@within DateTime
Returns a new `DateTime` using the given units from a UTC time. The
values accepted are similar to those found in the time value table
returned by `toUniversalTime`.
- Date units (year, month, day) that produce an invalid date will raise an error. For example, January 32nd or February 29th on a non-leap year.
- Time units (hour, minute, second, millisecond) that are outside their normal range are valid. For example, 90 minutes will cause the hour to roll over by 1; -10 seconds will cause the minute value to roll back by 1.
- Non-integer values are rounded down. For example, providing 2.5 hours will be equivalent to providing 2 hours, not 2 hours 30 minutes.
- Omitted values are assumed to be their lowest value in their normal range, except for year which defaults to 1970.
Creates a new `DateTime` from the given date & time values table, in universal (UTC) time.
@param dateTime Optional values for the dateTime instance, defaults to the current time
@return A DateTime instance
The given table must contains the following values:
| Key | Type | Range |
|----------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
An additional `millisecond` value may also be included,
and should be within the range `0 -> 999`, but is optional.
This constructor is fallible and may throw an error in the following situations:
- Date units (year, month, day) that produce an invalid date will raise an error.
For example, January 32nd or February 29th on a non-leap year.
- Non-integer values are rounded down. For example, providing 2.5 hours
is equivalent to providing 2 hours, not 2 hours and 30 minutes.
@param dateTime -- Table containing date & time values
@return DateTime -- The new DateTime object
]=]
function dateTime.fromUniversalTime(dateTime: DateTimeValues?): typeof(dateTime)
function dateTime.fromUniversalTime(values: DateTimeValues): DateTime
return nil :: any
end
--[=[
@within DateTime
Returns a new `DateTime` using the given units from a local time. The
values accepted are similar to those found in the time value table
returned by `toLocalTime`.
- Date units (year, month, day) that produce an invalid date will raise an error. For example, January 32nd or February 29th on a non-leap year.
- Time units (hour, minute, second, millisecond) that are outside their normal range are valid. For example, 90 minutes will cause the hour to roll over by 1; -10 seconds will cause the minute value to roll back by 1.
- Non-integer values are rounded down. For example, providing 2.5 hours will be equivalent to providing 2 hours, not 2 hours 30 minutes.
- Omitted values are assumed to be their lowest value in their normal range, except for year which defaults to 1970.
Creates a new `DateTime` from the given date & time values table, in local time.
@param dateTime Optional values for the dateTime instance, defaults to the current time
@return A DateTime instance
The given table must contains the following values:
| Key | Type | Range |
|----------|----------|----------------|
| `year` | `number` | `1400 -> 9999` |
| `month` | `number` | `1 -> 12` |
| `day` | `number` | `1 -> 31` |
| `hour` | `number` | `0 -> 23` |
| `minute` | `number` | `0 -> 59` |
| `second` | `number` | `0 -> 60` |
An additional `millisecond` value may also be included,
and should be within the range `0 -> 999`, but is optional.
This constructor is fallible and may throw an error in the following situations:
- Date units (year, month, day) that produce an invalid date will raise an error.
For example, January 32nd or February 29th on a non-leap year.
- Non-integer values are rounded down. For example, providing 2.5 hours
is equivalent to providing 2 hours, not 2 hours and 30 minutes.
@param dateTime -- Table containing date & time values
@return DateTime -- The new DateTime object
]=]
function dateTime.fromLocalTime(dateTime: DateTimeValues?): typeof(dateTime)
function dateTime.fromLocalTime(values: DateTimeValues): DateTime
return nil :: any
end
--[=[
@within DateTime
Returns a `DateTime` from an ISO 8601 date-time string in UTC
time, such as those returned by `toIsoDate`. If the
string parsing fails, the function returns `nil`.
An example ISO 8601 date-time string would be `2020-01-02T10:30:45Z`,
which represents January 2nd 2020 at 10:30 AM, 45 seconds.
Creates a new `DateTime` from an ISO 8601 date-time string.
@param isoDate An ISO 8601 formatted string
@return A DateTime instance
This constructor is fallible and may throw an error if the given
string does not strictly follow the ISO 8601 date-time string format.
Some examples of valid ISO 8601 date-time strings:
- `2020-02-22T18:12:08Z`
- `2000-01-31T12:34:56+05:00`
- `1970-01-01T00:00:00.055Z`
@param isoDate -- An ISO 8601 formatted string
@return DateTime -- The new DateTime object
]=]
function dateTime.fromIsoDate(iso_date: string): typeof(dateTime)?
function dateTime.fromIsoDate(isoDate: string): DateTime
return nil :: any
end
--[=[
@within DateTime
Formats a date as a ISO 8601 date-time string, returns None if the DateTime
object is invalid. The value returned by this function could be passed to
`fromLocalTime` to produce the original `DateTime` object.
@return ISO 8601 formatted string
]=]
function dateTime:toIsoDate(): string
return nil :: any
end
--[=[
@within DateTime
Converts the value of this `DateTime` object to local time. The returned table
contains the following keys: `Year`, `Month`, `Day`, `Hour`, `Minute`, `Second`,
`Millisecond`. For more details, see the time value table in this data type's
description. The values within this table could be passed to `fromLocalTime`
to produce the original `DateTime` object.
@return A table of DateTime values
]=]
function dateTime:toLocalTime(): DateTimeValues & { month: number }
return nil :: any
end
--[=[
@within DateTime
Converts the value of this `DateTime` object to universal time. The returned table
contains the following keys: `Year`, `Month`, `Day`, `Hour`, `Minute`, `Second`,
`Millisecond`. For more details, see the time value table in this data type's
description. The values within this table could be passed to `fromUniversalTime`
to produce the original `DateTime` object.
@return A table of DateTime values
]=]
function dateTime:toUniversalTime(): DateTimeValues & { month: number }
return nil :: any
end
--[=[
@within DateTime
Generates a string from the `DateTime` value interpreted as the specified timezone
and a format string. The format string should contain tokens, which will
replace to certain date/time values described by the `DateTime` object.
@param timezone The timezone the formatted time string should follow
@param formatString A format string of strfttime items. See [chrono docs](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
@param locale The locale the time should be formatted in
@return A table of DateTime values
]=]
function dateTime:formatTime(timezone: Timezone, formatString: string, locale: Locale): string
return nil :: any
end
export type DateTime = typeof(dateTime)
return dateTime