lune_std_datetime/
date_time.rs

1use std::cmp::Ordering;
2
3use mlua::prelude::*;
4
5use chrono::DateTime as ChronoDateTime;
6use chrono::prelude::*;
7use chrono_lc::LocaleDate;
8
9use crate::result::{DateTimeError, DateTimeResult};
10use crate::values::DateTimeValues;
11
12const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
13const DEFAULT_LOCALE: &str = "en";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub struct DateTime {
17    // NOTE: We store this as the UTC time zone since it is the most commonly
18    // used and getting the generics right for TimeZone is somewhat tricky,
19    // but none of the method implementations below should rely on this tz
20    inner: ChronoDateTime<Utc>,
21}
22
23impl DateTime {
24    /**
25        Creates a new `DateTime` struct representing the current moment in time.
26
27        See [`chrono::DateTime::now`] for additional details.
28    */
29    #[must_use]
30    pub fn now() -> Self {
31        Self { inner: Utc::now() }
32    }
33
34    /**
35        Creates a new `DateTime` struct from the given `unix_timestamp`,
36        which is a float of seconds passed since the UNIX epoch.
37
38        This is somewhat unconventional, but fits our Luau interface and dynamic types quite well.
39        To use this method the same way you would use a more traditional `from_unix_timestamp`
40        that takes a `u64` of seconds or similar type, casting the value is sufficient:
41
42        ```rust ignore
43        DateTime::from_unix_timestamp_float(123456789u64 as f64)
44        ```
45
46        See [`chrono::DateTime::from_timestamp`] for additional details.
47
48        # Errors
49
50        Returns an error if the input value is out of range.
51    */
52    pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult<Self> {
53        let whole = unix_timestamp.trunc() as i64;
54        let fract = unix_timestamp.fract();
55        let nanos = (fract * 1_000_000_000f64)
56            .round()
57            .clamp(u32::MIN as f64, u32::MAX as f64) as u32;
58        let inner = ChronoDateTime::<Utc>::from_timestamp(whole, nanos)
59            .ok_or(DateTimeError::OutOfRangeUnspecified)?;
60        Ok(Self { inner })
61    }
62
63    /**
64        Transforms individual date & time values into a new
65        `DateTime` struct, using the universal (UTC) time zone.
66
67        See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
68        for additional details and cases where this constructor may return an error.
69
70        # Errors
71
72        Returns an error if the date or time values are invalid.
73    */
74    pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult<Self> {
75        let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
76            .ok_or(DateTimeError::InvalidDate)?;
77
78        let time = NaiveTime::from_hms_milli_opt(
79            values.hour,
80            values.minute,
81            values.second,
82            values.millisecond,
83        )
84        .ok_or(DateTimeError::InvalidTime)?;
85
86        let inner = Utc.from_utc_datetime(&NaiveDateTime::new(date, time));
87
88        Ok(Self { inner })
89    }
90
91    /**
92        Transforms individual date & time values into a new
93        `DateTime` struct, using the current local time zone.
94
95        See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
96        for additional details and cases where this constructor may return an error.
97
98        # Errors
99
100        Returns an error if the date or time values are invalid or ambiguous.
101    */
102    pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult<Self> {
103        let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
104            .ok_or(DateTimeError::InvalidDate)?;
105
106        let time = NaiveTime::from_hms_milli_opt(
107            values.hour,
108            values.minute,
109            values.second,
110            values.millisecond,
111        )
112        .ok_or(DateTimeError::InvalidTime)?;
113
114        let inner = Local
115            .from_local_datetime(&NaiveDateTime::new(date, time))
116            .single()
117            .ok_or(DateTimeError::Ambiguous)?
118            .with_timezone(&Utc);
119
120        Ok(Self { inner })
121    }
122
123    /**
124        Formats the `DateTime` using the universal (UTC) time
125        zone, the given format string, and the given locale.
126
127        `format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
128
129        See [`chrono_lc::DateTime::formatl`] for additional details.
130    */
131    #[must_use]
132    pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String {
133        self.inner
134            .with_timezone(&Local)
135            .formatl(
136                format.unwrap_or(DEFAULT_FORMAT),
137                locale.unwrap_or(DEFAULT_LOCALE),
138            )
139            .to_string()
140    }
141
142    /**
143        Formats the `DateTime` using the universal (UTC) time
144        zone, the given format string, and the given locale.
145
146        `format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
147
148        See [`chrono_lc::DateTime::formatl`] for additional details.
149    */
150    #[must_use]
151    pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String {
152        self.inner
153            .with_timezone(&Utc)
154            .formatl(
155                format.unwrap_or(DEFAULT_FORMAT),
156                locale.unwrap_or(DEFAULT_LOCALE),
157            )
158            .to_string()
159    }
160
161    /**
162        Parses a time string in the RFC 3339 format, such as
163        `1996-12-19T16:39:57-08:00`, into a new `DateTime` struct.
164
165        See [`chrono::DateTime::parse_from_rfc3339`] for additional details.
166
167        # Errors
168
169        Returns an error if the input string is not a valid RFC 3339 date-time.
170    */
171    pub fn from_rfc_3339(date: impl AsRef<str>) -> DateTimeResult<Self> {
172        let inner = ChronoDateTime::parse_from_rfc3339(date.as_ref())?.with_timezone(&Utc);
173        Ok(Self { inner })
174    }
175
176    /**
177        Parses a time string in the RFC 2822 format, such as
178        `Tue, 1 Jul 2003 10:52:37 +0200`, into a new `DateTime` struct.
179
180        See [`chrono::DateTime::parse_from_rfc2822`] for additional details.
181
182        # Errors
183
184        Returns an error if the input string is not a valid RFC 2822 date-time.
185    */
186    pub fn from_rfc_2822(date: impl AsRef<str>) -> DateTimeResult<Self> {
187        let inner = ChronoDateTime::parse_from_rfc2822(date.as_ref())?.with_timezone(&Utc);
188        Ok(Self { inner })
189    }
190
191    /**
192        Extracts individual date & time values from this
193        `DateTime`, using the current local time zone.
194    */
195    #[must_use]
196    pub fn to_local_time(self) -> DateTimeValues {
197        DateTimeValues::from(self.inner.with_timezone(&Local))
198    }
199
200    /**
201        Extracts individual date & time values from this
202        `DateTime`, using the universal (UTC) time zone.
203    */
204    #[must_use]
205    pub fn to_universal_time(self) -> DateTimeValues {
206        DateTimeValues::from(self.inner.with_timezone(&Utc))
207    }
208
209    /**
210        Formats a time string in the RFC 3339 format, such as `1996-12-19T16:39:57-08:00`.
211
212        See [`chrono::DateTime::to_rfc3339`] for additional details.
213    */
214    #[must_use]
215    pub fn to_rfc_3339(self) -> String {
216        self.inner.to_rfc3339()
217    }
218
219    /**
220        Formats a time string in the RFC 2822 format, such as `Tue, 1 Jul 2003 10:52:37 +0200`.
221
222        See [`chrono::DateTime::to_rfc2822`] for additional details.
223    */
224    #[must_use]
225    pub fn to_rfc_2822(self) -> String {
226        self.inner.to_rfc2822()
227    }
228}
229
230impl LuaUserData for DateTime {
231    fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
232        fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp()));
233        fields.add_field_method_get("unixTimestampMillis", |_, this| {
234            Ok(this.inner.timestamp_millis())
235        });
236    }
237
238    fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
239        // Metamethods to compare DateTime as instants in time
240        methods.add_meta_method(
241            LuaMetaMethod::Eq,
242            |_, this: &Self, other: LuaUserDataRef<Self>| Ok(this.eq(&other)),
243        );
244        methods.add_meta_method(
245            LuaMetaMethod::Lt,
246            |_, this: &Self, other: LuaUserDataRef<Self>| {
247                Ok(matches!(this.cmp(&other), Ordering::Less))
248            },
249        );
250        methods.add_meta_method(
251            LuaMetaMethod::Le,
252            |_, this: &Self, other: LuaUserDataRef<Self>| {
253                Ok(matches!(this.cmp(&other), Ordering::Less | Ordering::Equal))
254            },
255        );
256        // Normal methods
257        methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_rfc_3339())); // FUTURE: Remove this rfc3339 alias method
258        methods.add_method("toRfc3339", |_, this, ()| Ok(this.to_rfc_3339()));
259        methods.add_method("toRfc2822", |_, this, ()| Ok(this.to_rfc_2822()));
260        methods.add_method(
261            "formatUniversalTime",
262            |_, this, (format, locale): (Option<String>, Option<String>)| {
263                Ok(this.format_string_universal(format.as_deref(), locale.as_deref()))
264            },
265        );
266        methods.add_method(
267            "formatLocalTime",
268            |_, this, (format, locale): (Option<String>, Option<String>)| {
269                Ok(this.format_string_local(format.as_deref(), locale.as_deref()))
270            },
271        );
272        methods.add_method("toUniversalTime", |_, this: &Self, ()| {
273            Ok(this.to_universal_time())
274        });
275        methods.add_method("toLocalTime", |_, this: &Self, ()| Ok(this.to_local_time()));
276    }
277}