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}