1use std::cmp::Ordering;
4use std::fmt;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct DateTime {
9 pub days: i64,
11 pub seconds: u32,
13}
14
15impl DateTime {
16 pub fn from_ymd(year: i32, month: u32, day: u32) -> Option<Self> {
18 if !(1..=12).contains(&month) || day == 0 || day > days_in_month(year, month) {
19 return None;
20 }
21 Some(Self { days: days_from_civil(year, month, day), seconds: 0 })
22 }
23
24 pub fn ymd(self) -> (i32, u32, u32) {
26 civil_from_days(self.days)
27 }
28
29 pub fn hms(self) -> (u32, u32, u32) {
31 (self.seconds / 3600, self.seconds / 60 % 60, self.seconds % 60)
32 }
33
34 pub fn weekday(self) -> u32 {
36 (self.days + 4).rem_euclid(7) as u32
37 }
38
39 pub fn parse(s: &str) -> Option<Self> {
42 let s = s.trim();
43 let bytes = s.as_bytes();
44 if bytes.len() < 10 || bytes[4] != b'-' || bytes[7] != b'-' {
45 return None;
46 }
47 let year: i32 = s.get(0..4)?.parse().ok()?;
48 let month: u32 = s.get(5..7)?.parse().ok()?;
49 let day: u32 = s.get(8..10)?.parse().ok()?;
50 let mut date = Self::from_ymd(year, month, day)?;
51 let rest = &s[10..];
52 if rest.is_empty() {
53 return Some(date);
54 }
55 let time = rest.strip_prefix('T').or_else(|| rest.strip_prefix(' '))?;
56 let h: u32 = time.get(0..2)?.parse().ok()?;
57 let m: u32 = time.get(3..5)?.parse().ok()?;
58 let sec: u32 = if time.as_bytes().get(5) == Some(&b':') { time.get(6..8)?.parse().ok()? } else { 0 };
59 if h > 23 || m > 59 || sec > 59 {
60 return None;
61 }
62 date.seconds = h * 3600 + m * 60 + sec;
63 Some(date)
64 }
65
66 pub fn now() -> Self {
69 #[cfg(target_arch = "wasm32")]
70 let secs = {
71 let now = js_sys::Date::new_0();
72 ((now.get_time() / 1000.0) - now.get_timezone_offset() * 60.0) as i64
74 };
75 #[cfg(not(target_arch = "wasm32"))]
76 let secs = std::time::SystemTime::now()
77 .duration_since(std::time::UNIX_EPOCH)
78 .map_or(0, |d| d.as_secs() as i64);
79 Self { days: secs.div_euclid(86_400), seconds: secs.rem_euclid(86_400) as u32 }
80 }
81
82 pub fn add_days(self, n: i64) -> Self {
84 Self { days: self.days + n, ..self }
85 }
86
87 pub fn add_months(self, n: i64) -> Self {
89 let (y, m, d) = self.ymd();
90 let total = i64::from(y) * 12 + i64::from(m) - 1 + n;
91 let (year, month) = (total.div_euclid(12) as i32, (total.rem_euclid(12) + 1) as u32);
92 let day = d.min(days_in_month(year, month));
93 Self { days: days_from_civil(year, month, day), seconds: self.seconds }
94 }
95}
96
97impl fmt::Display for DateTime {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 let (y, m, d) = self.ymd();
100 write!(f, "{y:04}-{m:02}-{d:02}")?;
101 if self.seconds != 0 {
102 let (h, mi, s) = self.hms();
103 write!(f, "T{h:02}:{mi:02}:{s:02}")?;
104 }
105 Ok(())
106 }
107}
108
109pub(crate) fn is_leap(year: i32) -> bool {
110 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
111}
112
113pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
114 match month {
115 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
116 4 | 6 | 9 | 11 => 30,
117 2 if is_leap(year) => 29,
118 2 => 28,
119 _ => 0,
120 }
121}
122
123fn days_from_civil(y: i32, m: u32, d: u32) -> i64 {
125 let y = i64::from(y) - i64::from(m <= 2);
126 let era = y.div_euclid(400);
127 let yoe = y - era * 400;
128 let m = i64::from(m);
129 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + i64::from(d) - 1;
130 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
131 era * 146_097 + doe - 719_468
132}
133
134fn civil_from_days(z: i64) -> (i32, u32, u32) {
135 let z = z + 719_468;
136 let era = z.div_euclid(146_097);
137 let doe = z - era * 146_097;
138 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
139 let y = yoe + era * 400;
140 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
141 let mp = (5 * doy + 2) / 153;
142 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
143 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
144 ((y + i64::from(m <= 2)) as i32, m, d)
145}
146
147#[derive(Clone, Debug, Default, PartialEq)]
149#[non_exhaustive]
150pub enum Value {
151 #[default]
153 Null,
154 Bool(bool),
156 Number(f64),
158 Text(String),
160 Date(DateTime),
162}
163
164impl Value {
165 #[cfg(feature = "json")]
168 pub fn from_json(json: &serde_json::Value) -> Self {
169 match json {
170 serde_json::Value::Bool(b) => Self::Bool(*b),
171 serde_json::Value::Number(n) => n.as_f64().map_or(Self::Null, Self::Number),
172 serde_json::Value::String(s) => Self::Text(s.clone()),
173 _ => Self::Null,
174 }
175 }
176
177 pub fn is_null(&self) -> bool {
179 matches!(self, Self::Null)
180 }
181
182 pub fn as_number(&self) -> Option<f64> {
184 match self {
185 Self::Number(n) => Some(*n),
186 Self::Bool(b) => Some(f64::from(u8::from(*b))),
187 Self::Text(s) => s.trim().parse().ok(),
188 _ => None,
189 }
190 }
191
192 pub fn as_date(&self) -> Option<DateTime> {
194 match self {
195 Self::Date(d) => Some(*d),
196 Self::Text(s) => DateTime::parse(s),
197 _ => None,
198 }
199 }
200
201 pub fn is_true(&self) -> bool {
203 match self {
204 Self::Bool(b) => *b,
205 Self::Number(n) => *n != 0.0 && !n.is_nan(),
206 Self::Text(s) => !s.is_empty(),
207 Self::Date(_) => true,
208 Self::Null => false,
209 }
210 }
211
212 pub fn compare(&self, other: &Self) -> Ordering {
215 match (self, other) {
216 (Self::Null, Self::Null) => Ordering::Equal,
217 (Self::Null, _) => Ordering::Less,
218 (_, Self::Null) => Ordering::Greater,
219 (Self::Number(a), Self::Number(b)) => a.total_cmp(b),
220 (Self::Date(a), Self::Date(b)) => a.cmp(b),
221 (Self::Bool(a), Self::Bool(b)) => a.cmp(b),
222 (Self::Text(a), Self::Text(b)) => {
223 let o = cmp_ignore_case(a, b);
224 if o == Ordering::Equal { a.cmp(b) } else { o }
225 }
226 (a, b) => match (a.as_date(), b.as_date()) {
227 (Some(x), Some(y)) => x.cmp(&y),
228 _ => match (a.as_number(), b.as_number()) {
229 (Some(x), Some(y)) => x.total_cmp(&y),
230 _ => a.to_text().cmp(&b.to_text()),
231 },
232 },
233 }
234 }
235
236 pub fn loose_eq(&self, other: &Self) -> bool {
238 match (self, other) {
239 (Self::Text(a), Self::Text(b)) => a == b,
240 (Self::Date(_), Self::Text(_)) | (Self::Text(_), Self::Date(_)) => {
241 self.as_date().is_some() && self.as_date() == other.as_date()
242 }
243 _ => {
244 self.compare(other) == Ordering::Equal
245 && std::mem::discriminant(self) == std::mem::discriminant(other)
246 }
247 }
248 }
249
250 pub fn to_text(&self) -> String {
252 match self {
253 Self::Null => String::new(),
254 Self::Bool(b) => b.to_string(),
255 Self::Number(n) => format_plain(*n),
256 Self::Text(s) => s.clone(),
257 Self::Date(d) => d.to_string(),
258 }
259 }
260}
261
262fn cmp_ignore_case(a: &str, b: &str) -> Ordering {
263 a.chars().flat_map(char::to_lowercase).cmp(b.chars().flat_map(char::to_lowercase))
264}
265
266pub(crate) fn format_plain(n: f64) -> String {
268 if n.fract() == 0.0 && n.abs() < 1e15 {
269 format!("{}", n as i64)
270 } else {
271 let s = format!("{n:.10}");
272 s.trim_end_matches('0').trim_end_matches('.').to_owned()
273 }
274}
275
276impl From<f64> for Value {
277 fn from(n: f64) -> Self {
278 Self::Number(n)
279 }
280}
281
282impl From<i64> for Value {
283 fn from(n: i64) -> Self {
284 Self::Number(n as f64)
285 }
286}
287
288impl From<bool> for Value {
289 fn from(b: bool) -> Self {
290 Self::Bool(b)
291 }
292}
293
294impl From<&str> for Value {
295 fn from(s: &str) -> Self {
296 Self::Text(s.to_owned())
297 }
298}
299
300impl From<String> for Value {
301 fn from(s: String) -> Self {
302 Self::Text(s)
303 }
304}
305
306impl From<DateTime> for Value {
307 fn from(d: DateTime) -> Self {
308 Self::Date(d)
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn civil_dates_round_trip() {
318 for days in [-800_000, -1, 0, 1, 19_000, 20_393, 2_000_000] {
319 let d = DateTime { days, seconds: 0 };
320 let (y, m, dd) = d.ymd();
321 assert_eq!(DateTime::from_ymd(y, m, dd), Some(d));
322 }
323 assert_eq!(DateTime::from_ymd(1970, 1, 1).map(|d| d.days), Some(0));
324 assert_eq!(DateTime::from_ymd(2026, 2, 29), None);
325 assert_eq!(DateTime::from_ymd(2024, 2, 29).map(|d| d.weekday()), Some(4), "a Thursday");
326 }
327
328 #[test]
329 fn parses_iso_dates_and_times() {
330 let d = DateTime::parse("2026-11-01T14:30:05Z").expect("valid");
331 assert_eq!((d.ymd(), d.hms()), ((2026, 11, 1), (14, 30, 5)));
332 assert_eq!(DateTime::parse("2026-11-01 08:15").map(|d| d.hms()), Some((8, 15, 0)));
333 assert!(DateTime::parse("01/11/2026").is_none());
334 assert!(DateTime::parse("2026-13-01").is_none());
335 }
336
337 #[test]
338 fn month_arithmetic_clamps_to_the_last_day() {
339 let jan31 = DateTime::from_ymd(2026, 1, 31).expect("valid");
340 assert_eq!(jan31.add_months(1).ymd(), (2026, 2, 28));
341 assert_eq!(jan31.add_months(-2).ymd(), (2025, 11, 30));
342 assert_eq!(jan31.add_days(1).ymd(), (2026, 2, 1));
343 }
344
345 #[test]
346 fn comparison_and_equality() {
347 assert_eq!(Value::from("apple").compare(&Value::from("Banana")), Ordering::Less);
348 assert_eq!(Value::Null.compare(&Value::from(0.0)), Ordering::Less);
349 assert!(Value::from(1.0).loose_eq(&Value::from(1_i64)));
350 assert!(!Value::from(1.0).loose_eq(&Value::from("1")));
351 assert!(
352 Value::from("2026-01-02").loose_eq(&Value::Date(DateTime::from_ymd(2026, 1, 2).expect("valid")))
353 );
354 assert_eq!(format_plain(2.50), "2.5");
355 assert_eq!(format_plain(-3.0), "-3");
356 }
357}