Implement DateTime Built-in Library (#94)

This commit is contained in:
Erica Marigold 2023-09-11 23:22:07 +05:30 committed by GitHub
parent 3967c1ecbb
commit e2aef015fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1233 additions and 6 deletions

144
Cargo.lock generated
View file

@ -41,6 +41,21 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.5.0" version = "0.5.0"
@ -293,6 +308,36 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d9d13be47a5b7c3907137f1290b0459a7f80efb26be8c52afb11963bccb02"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"time 0.1.45",
"wasm-bindgen",
"windows-targets 0.48.5",
]
[[package]]
name = "chrono_lc"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4556d06f1286632cf49ef465898936b17c1b903e232965f2b52ebbc6bd5390a"
dependencies = [
"chrono",
"lazy_static",
"num-integer",
"serde",
"serde_derive",
"serde_json",
"walkdir",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.4.0" version = "4.4.0"
@ -392,6 +437,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.9" version = "0.2.9"
@ -882,6 +933,29 @@ dependencies = [
"tungstenite", "tungstenite",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.4.0" version = "0.4.0"
@ -1032,6 +1106,8 @@ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
"async-trait", "async-trait",
"chrono",
"chrono_lc",
"clap", "clap",
"console", "console",
"dialoguer", "dialoguer",
@ -1046,6 +1122,7 @@ dependencies = [
"itertools", "itertools",
"lz4_flex", "lz4_flex",
"mlua", "mlua",
"num-traits",
"once_cell", "once_cell",
"os_str_bytes", "os_str_bytes",
"path-clean", "path-clean",
@ -1201,6 +1278,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.16" version = "0.2.16"
@ -1328,9 +1415,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.12" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1836,6 +1923,15 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -1869,9 +1965,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.186" version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -1888,9 +1984,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.186" version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2190,6 +2286,17 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.2.27" version = "0.2.27"
@ -2572,6 +2679,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -2587,6 +2704,12 @@ version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -2715,6 +2838,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"

View file

@ -106,6 +106,11 @@ reqwest = { version = "0.11", default-features = false, features = [
] } ] }
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] } tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
### DATETIME
chrono = "0.4.29"
chrono_lc = "0.1.3"
num-traits = "0.2.16"
### CLI ### CLI
anyhow = { optional = true, version = "1.0" } anyhow = { optional = true, version = "1.0" }

View file

@ -0,0 +1,177 @@
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

@ -0,0 +1,243 @@
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,176 @@
use chrono::Month;
use mlua::prelude::*;
pub(crate) mod builder;
pub(crate) mod date_time;
use self::{
builder::DateTimeBuilder,
date_time::{DateTime, TimestampType, Timezone},
};
use crate::lune::util::TableBuilder;
// TODO: Proper error handling and stuff
pub fn create(lua: &'static 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))
})?
.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",
)),
}
}
}
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("unixTimestampMillis", |_, this| {
Ok(this.unix_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")))
},
);
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!")),
},
_ => Err(LuaError::external("Invalid enum type, string expected")),
}
}
}

View file

@ -2,6 +2,7 @@ use std::str::FromStr;
use mlua::prelude::*; use mlua::prelude::*;
mod datetime;
mod fs; mod fs;
mod luau; mod luau;
mod net; mod net;
@ -15,6 +16,7 @@ mod roblox;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum LuneBuiltin { pub enum LuneBuiltin {
DateTime,
Fs, Fs,
Luau, Luau,
Net, Net,
@ -32,6 +34,7 @@ where
{ {
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
Self::DateTime => "datetime",
Self::Fs => "fs", Self::Fs => "fs",
Self::Luau => "luau", Self::Luau => "luau",
Self::Net => "net", Self::Net => "net",
@ -46,6 +49,7 @@ where
pub fn create(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> { pub fn create(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
let res = match self { let res = match self {
Self::DateTime => datetime::create(lua),
Self::Fs => fs::create(lua), Self::Fs => fs::create(lua),
Self::Luau => luau::create(lua), Self::Luau => luau::create(lua),
Self::Net => net::create(lua), Self::Net => net::create(lua),
@ -70,6 +74,7 @@ impl FromStr for LuneBuiltin {
type Err = String; type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() { match s.trim().to_ascii_lowercase().as_str() {
"datetime" => Ok(Self::DateTime),
"fs" => Ok(Self::Fs), "fs" => Ok(Self::Fs),
"luau" => Ok(Self::Luau), "luau" => Ok(Self::Luau),
"net" => Ok(Self::Net), "net" => Ok(Self::Net),

View file

@ -106,6 +106,16 @@ create_tests! {
task_delay: "task/delay", task_delay: "task/delay",
task_spawn: "task/spawn", task_spawn: "task/spawn",
task_wait: "task/wait", 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")] #[cfg(feature = "roblox")]

View file

@ -0,0 +1,70 @@
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,11 @@
local DateTime = require("@lune/DateTime")
assert(
DateTime.fromIsoDate("2023-08-26T16:56:28Z") ~= nil,
"expected DateTime.fromIsoDate() to return DateTime, got nil"
)
assert(
DateTime.fromIsoDate("1929-12-05T23:18:23Z") ~= nil,
"expected DateTime.fromIsoDate() to return DateTime, got nil"
)

View file

@ -0,0 +1,45 @@
local DateTime = require("@lune/DateTime")
assert(
DateTime.fromLocalTime()["unixTimestamp"] == os.time(),
"expected DateTime.fromLocalTime() with no args to return DateTime at current moment"
)
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,
"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"])
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,
"expected DateTime.fromLocalTime() with DateTimeValues arg with millis to return 1693049188892ms"
)

View file

@ -0,0 +1,32 @@
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"
)
assert(
DateTime.fromUniversalTime({
["year"] = 2023,
["month"] = "aug",
["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,
"expected DateTime.fromUniversalTime() with DateTimeValues arg with millis to return 1693068988892ms"
)

View file

@ -0,0 +1,16 @@
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,
"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"]
== ((1693114921 * 1000) + 632) - 1,
"expected DateTime.fromUnixTimestamp() with millis and seconds float to return correct millis timestamp"
)

8
tests/datetime/now.luau Normal file
View file

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

View file

@ -0,0 +1,9 @@
local DateTime = require("@lune/DateTime")
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()"
)

View file

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

View file

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

228
types/DateTime.luau Normal file
View file

@ -0,0 +1,228 @@
-- 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: number,
month: number | ShortMonth | Month,
day: number,
hour: number,
minute: number,
second: number,
millisecond: number,
}
--[=[
@class DateTime
Built-in library for date & time manipulation
### Example usage
```lua
local DateTime = require("@lune/DateTime")
-- Returns the current moment in time as a ISO 8601 string
DateTime.now():toIsoDate()
-- Returns the current moment in time as per the format template in French
DateTime.now():formatTime("utc", "%A, %d %B %Y", "fr")
-- Returns a specific moment in time as a DateTime instance
DateTime.fromLocalTime({
year = 2023,
month = "aug",
day = 26,
hour = 16,
minute = 56,
second = 28,
millisecond = 892,
})
-- Returns the current local time as a DateTime instance
DateTime.fromLocalTime()
-- Returns a DateTime instance from a given float, where the whole denotes the
-- seconds and the fraction denotes the milliseconds
DateTime.fromUnixTimestamp(871978212313.321)
-- Returns the current time in terms of UTC
DateTime.now():toUniversalTime()
```
]=]
local dateTime = {
unixTimestamp = 0,
unixTimestampMillis = 0,
}
--[=[
@within DateTime
Returns a `DateTime` representing the current moment in time.
@return A DateTime instance
]=]
function dateTime.now(): typeof(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).
@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
]=]
function dateTime.fromUnixTimestamp(unixTimestamp: number?): typeof(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.
@param dateTime Optional values for the dateTime instance, defaults to the current time
@return A DateTime instance
]=]
function dateTime.fromUniversalTime(dateTime: DateTimeValues?): typeof(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.
@param dateTime Optional values for the dateTime instance, defaults to the current time
@return A DateTime instance
]=]
function dateTime.fromLocalTime(dateTime: DateTimeValues?): typeof(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.
@param isoDate An ISO 8601 formatted string
@return A DateTime instance
]=]
function dateTime.fromIsoDate(iso_date: string): typeof(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