mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +00:00
Refactor DateTime builtin to be a wrapper around chrono, improve error handling, misc smaller changes and consistency improvements
This commit is contained in:
parent
7a05563f0f
commit
fad48b9603
19 changed files with 900 additions and 895 deletions
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 1400–9999.
|
||||
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 1400–9999.
|
||||
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()),
|
||||
)
|
||||
}
|
||||
}
|
32
src/lune/builtins/datetime/error.rs
Normal file
32
src/lune/builtins/datetime/error.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())
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
170
src/lune/builtins/datetime/values.rs
Normal file
170
src/lune/builtins/datetime/values.rs
Normal 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)
|
||||
}
|
||||
}
|
21
src/tests.rs
21
src/tests.rs
|
@ -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")]
|
||||
|
|
53
tests/datetime/formatLocalTime.luau
Normal file
53
tests/datetime/formatLocalTime.luau
Normal 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)"
|
||||
)
|
|
@ -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)"
|
||||
)
|
15
tests/datetime/formatUniversalTime.luau
Normal file
15
tests/datetime/formatUniversalTime.luau
Normal 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()"
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
local DateTime = require("@lune/DateTime")
|
||||
local DateTime = require("@lune/datetime")
|
||||
|
||||
assert(
|
||||
DateTime.fromIsoDate("2023-08-26T16:56:28Z") ~= nil,
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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)}`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
||||
)
|
||||
|
|
|
@ -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}`
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue