tenda_runtime/
date.rs

1use chrono::{
2    DateTime, Datelike, FixedOffset, LocalResult, Offset, TimeZone, Timelike, Utc, Weekday,
3};
4use chrono_tz::Tz;
5use std::ops::{Add, Sub};
6
7use crate::runtime_error::RuntimeError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Date {
11    ts: i64,
12    tz: FixedOffset,
13}
14
15impl Date {
16    pub fn from_timestamp_millis(ts: i64, tz: Option<i32>) -> Result<Self, Box<RuntimeError>> {
17        let tz = tz.map(|offset| FixedOffset::east_opt(offset).unwrap());
18
19        match Utc.timestamp_millis_opt(ts) {
20            LocalResult::Single(_) => Ok(Self {
21                ts,
22                tz: tz.unwrap_or(FixedOffset::east_opt(0).unwrap()),
23            }),
24            _ => Err(Box::new(RuntimeError::InvalidTimestamp {
25                timestamp: ts,
26                span: None,
27                stacktrace: vec![],
28            })),
29        }
30    }
31
32    pub fn from_iso_string(s: &str) -> Result<Self, Box<RuntimeError>> {
33        let fixed_dt = match chrono::DateTime::parse_from_rfc3339(s) {
34            Ok(dt) => dt,
35            Err(e) => {
36                return Err(Box::new(RuntimeError::DateIsoParseError {
37                    source: e,
38                    span: None,
39                    stacktrace: vec![],
40                }))
41            }
42        };
43
44        let utc_ts = fixed_dt.with_timezone(&Utc).timestamp_millis();
45        let offset = fixed_dt.offset().fix();
46
47        Ok(Self {
48            ts: utc_ts,
49            tz: offset,
50        })
51    }
52
53    pub fn with_named_timezone(&self, tz_str: &str) -> Result<Self, Box<RuntimeError>> {
54        let named_zone = match tz_str.parse::<Tz>() {
55            Ok(z) => z,
56            Err(_) => {
57                return Err(Box::new(RuntimeError::InvalidTimeZoneString {
58                    tz_str: tz_str.into(),
59                    span: None,
60                    stacktrace: vec![],
61                }));
62            }
63        };
64
65        let utc_dt = Utc.timestamp_millis_opt(self.ts).single().unwrap();
66        let dt_in_zone = utc_dt.with_timezone(&named_zone);
67        let new_offset = dt_in_zone.offset().fix();
68
69        Ok(Self {
70            ts: self.ts,
71            tz: new_offset,
72        })
73    }
74
75    pub fn to_offset_string(&self) -> String {
76        let total_seconds = self.tz.local_minus_utc();
77
78        let sign = if total_seconds >= 0 { '+' } else { '-' };
79        let secs = total_seconds.abs();
80
81        let hours = secs / 3600;
82        let minutes = (secs % 3600) / 60;
83
84        format!("{}{:02}:{:02}", sign, hours, minutes)
85    }
86
87    pub fn to_iso_string(&self) -> String {
88        let dt_tz = self.as_datetime_tz();
89        dt_tz.to_rfc3339()
90    }
91
92    pub fn to_timestamp_millis(&self) -> i64 {
93        self.ts
94    }
95
96    pub fn year(&self) -> i32 {
97        self.as_datetime_tz().year()
98    }
99
100    pub fn month(&self) -> u32 {
101        self.as_datetime_tz().month()
102    }
103
104    pub fn day(&self) -> u32 {
105        self.as_datetime_tz().day()
106    }
107
108    pub fn hour(&self) -> u32 {
109        self.as_datetime_tz().hour()
110    }
111
112    pub fn minute(&self) -> u32 {
113        self.as_datetime_tz().minute()
114    }
115
116    pub fn second(&self) -> u32 {
117        self.as_datetime_tz().second()
118    }
119
120    pub fn weekday(&self) -> u8 {
121        match self.as_datetime_tz().weekday() {
122            Weekday::Sun => 0,
123            Weekday::Mon => 1,
124            Weekday::Tue => 2,
125            Weekday::Wed => 3,
126            Weekday::Thu => 4,
127            Weekday::Fri => 5,
128            Weekday::Sat => 6,
129        }
130    }
131
132    pub fn ordinal(&self) -> u32 {
133        self.as_datetime_tz().ordinal()
134    }
135
136    pub fn iso_week(&self) -> u32 {
137        self.as_datetime_tz().iso_week().week()
138    }
139
140    pub fn is_leap_year(&self) -> bool {
141        let year = self.year();
142        (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
143    }
144
145    fn as_datetime_tz(&self) -> DateTime<FixedOffset> {
146        let utc_dt = Utc.timestamp_millis_opt(self.ts).single().unwrap();
147
148        utc_dt.with_timezone(&self.tz)
149    }
150}
151
152impl Add<i64> for Date {
153    type Output = Self;
154
155    fn add(self, rhs: i64) -> Self::Output {
156        Date {
157            ts: self.ts + rhs,
158            tz: self.tz,
159        }
160    }
161}
162
163impl Sub<i64> for Date {
164    type Output = Self;
165
166    fn sub(self, rhs: i64) -> Self::Output {
167        Date {
168            ts: self.ts - rhs,
169            tz: self.tz,
170        }
171    }
172}
173
174impl Ord for Date {
175    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
176        self.ts.cmp(&other.ts)
177    }
178}
179
180impl PartialOrd for Date {
181    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
182        Some(self.ts.cmp(&other.ts))
183    }
184}