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}