1pub mod constants;
3
4use constants::*;
5use std::time::{SystemTime, UNIX_EPOCH};
6use std::{fmt, ops::Add, ops::Sub};
7
8#[derive(Debug, Clone, Copy)]
10pub struct DateTime {
11 pub year: u64,
12 pub month: u64,
13 pub day: u64,
14 pub hour: u64,
15 pub minute: u64,
16 pub second: u64,
17 pub timezone: TimeZone,
18}
19
20impl DateTime {
21 pub fn new(
23 year: u64,
24 month: u64,
25 day: u64,
26 hour: u64,
27 minute: u64,
28 second: u64,
29 timezone: TimeZone,
30 ) -> Result<Self, String> {
31 if month < 1 || month > 12 {
32 return Err("Invalid month".to_string());
33 }
34 if day < 1 || day > days_in_month(month, year) {
35 return Err("Invalid day".to_string());
36 }
37 if hour > 23 {
38 return Err("Invalid hour".to_string());
39 }
40 if minute > 59 {
41 return Err("Invalid minute".to_string());
42 }
43 if second > 59 {
44 return Err("Invalid second".to_string());
45 }
46
47 let mut total_seconds = 0;
49
50 for y in 1970..year {
52 total_seconds += if is_leap_year(y) { 366 } else { 365 } * SECONDS_IN_DAY;
53 }
54
55 for m in 1..month {
57 total_seconds += days_in_month(m, year) as i64 * SECONDS_IN_DAY;
58 }
59
60 total_seconds += (day - 1) as i64 * SECONDS_IN_DAY;
62 total_seconds += hour as i64 * SECONDS_IN_HOUR;
63 total_seconds += minute as i64 * SECONDS_IN_MINUTE;
64 total_seconds += second as i64;
65
66 total_seconds -= timezone.offset_in_seconds();
68
69 let utc_datetime = Self::from_unix_seconds(total_seconds, timezone)?;
71
72 Ok(utc_datetime)
73 }
74
75 pub fn calculate_total_seconds(
77 year: u64,
78 month: u64,
79 day: u64,
80 hour: u64,
81 minute: u64,
82 second: u64,
83 ) -> Result<i64, String> {
84 if year < 1970 {
86 return Err("Year must be 1970 or later".to_string());
87 }
88
89 let mut total_seconds: i64 = 0;
90
91 for y in 1970..year {
93 total_seconds += if is_leap_year(y) {
94 SECONDS_IN_LEAPYEAR
95 } else {
96 SECONDS_IN_YEAR
97 };
98 }
99
100 for m in 1..month {
102 total_seconds += days_in_month(m, year) as i64 * SECONDS_IN_DAY;
103 }
104
105 total_seconds += (day as i64 - 1) * SECONDS_IN_DAY;
107
108 total_seconds += hour as i64 * SECONDS_IN_HOUR;
110 total_seconds += minute as i64 * SECONDS_IN_MINUTE;
111 total_seconds += second as i64;
112
113 Ok(total_seconds)
114 }
115
116 pub fn strftime(&self, format: &str) -> String {
117 let mut result = format.to_string();
118 result = result.replace("%Y", &format!("{:04}", self.year));
119 result = result.replace("%m", &format!("{:02}", self.month));
120 result = result.replace("%d", &format!("{:02}", self.day));
121 result = result.replace("%H", &format!("{:02}", self.hour));
122 result = result.replace("%M", &format!("{:02}", self.minute));
123 result = result.replace("%S", &format!("{:02}", self.second));
124 result
125 }
126
127 pub fn to_unix_seconds(&self) -> i64 {
128 let mut total_seconds: i64 = 0;
129
130 for year in 1970..self.year {
132 total_seconds += if is_leap_year(year) { 366 } else { 365 } * SECONDS_IN_DAY;
133 }
134
135 for month in 1..self.month {
137 total_seconds += days_in_month(month, self.year) as i64 * SECONDS_IN_DAY;
138 }
139
140 total_seconds += (self.day - 1) as i64 * SECONDS_IN_DAY;
142 total_seconds += self.hour as i64 * SECONDS_IN_HOUR;
143 total_seconds += self.minute as i64 * SECONDS_IN_MINUTE;
144 total_seconds += self.second as i64;
145
146 total_seconds - self.timezone.offset_in_seconds()
148 }
149
150 pub fn from_unix_seconds(unix_seconds: i64, timezone: TimeZone) -> Result<Self, String> {
151 let adjusted_seconds = unix_seconds + timezone.offset_in_seconds();
153 let mut remaining_seconds = adjusted_seconds;
154
155 if remaining_seconds < 0 {
156 return Err("Unix seconds cannot represent a date before 1970-01-01".to_string());
157 }
158
159 let mut year = 1970;
161 while remaining_seconds >= (if is_leap_year(year) { 366 } else { 365 }) * SECONDS_IN_DAY {
162 remaining_seconds -= (if is_leap_year(year) { 366 } else { 365 }) * SECONDS_IN_DAY;
163 year += 1;
164 }
165
166 let mut month = 1;
168 while remaining_seconds >= days_in_month(month, year) as i64 * SECONDS_IN_DAY {
169 remaining_seconds -= days_in_month(month, year) as i64 * SECONDS_IN_DAY;
170 month += 1;
171 }
172
173 let day = (remaining_seconds / SECONDS_IN_DAY) as u64 + 1;
175 remaining_seconds %= SECONDS_IN_DAY;
176 let hour = (remaining_seconds / 3600) as u64;
177 remaining_seconds %= 3600;
178 let minute = (remaining_seconds / 60) as u64;
179 let second = (remaining_seconds % 60) as u64;
180
181 Ok(Self {
182 year,
183 month,
184 day,
185 hour,
186 minute,
187 second,
188 timezone,
189 })
190 }
191
192 pub fn add_timedelta(&self, delta: TimeDelta) -> Result<Self, String> {
193 let current_unix = self.to_unix_seconds(); let delta_seconds = compute_total_seconds(
195 delta.weeks,
196 delta.days,
197 delta.hours,
198 delta.minutes,
199 delta.seconds,
200 );
201 let timezone = self.timezone.clone();
202 let new_unix = current_unix + delta_seconds; if new_unix < 0 {
204 return Err(
205 "Resulting DateTime is before Unix epoch (1970-01-01 00:00:00 UTC)".to_string(),
206 );
207 }
208 DateTime::from_unix_seconds(new_unix, timezone) }
210
211 pub fn sub_timedelta(&self, delta: TimeDelta) -> Result<Self, String> {
212 let current_unix = self.to_unix_seconds(); let delta_seconds = compute_total_seconds(
214 delta.weeks,
215 delta.days,
216 delta.hours,
217 delta.minutes,
218 delta.seconds,
219 );
220 let new_unix = current_unix - delta_seconds; let timezone = self.timezone.clone();
222 if new_unix < 0 {
223 return Err(
224 "Resulting DateTime is before Unix epoch (1970-01-01 00:00:00 UTC)".to_string(),
225 );
226 }
227
228 DateTime::from_unix_seconds(new_unix, timezone) }
230}
231
232impl fmt::Display for DateTime {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 write!(
236 f,
237 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
238 self.year, self.month, self.day, self.hour, self.minute, self.second
239 )
240 }
241}
242
243impl Add<TimeDelta> for DateTime {
244 type Output = Result<DateTime, String>;
245
246 fn add(self, delta: TimeDelta) -> Self::Output {
247 DateTime::add_timedelta(&self, delta)
248 }
249}
250
251impl Sub<TimeDelta> for DateTime {
252 type Output = Result<DateTime, String>;
253
254 fn sub(self, delta: TimeDelta) -> Self::Output {
255 DateTime::sub_timedelta(&self, delta)
256 }
257}
258
259impl PartialEq for DateTime {
260 fn eq(&self, other: &Self) -> bool {
261 self.year == other.year
262 && self.month == other.month
263 && self.day == other.day
264 && self.hour == other.hour
265 && self.minute == other.minute
266 && self.second == other.second
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub struct TimeDelta {
273 pub weeks: i64,
274 pub days: i64,
275 pub hours: i64,
276 pub minutes: i64,
277 pub seconds: i64,
278}
279
280impl std::fmt::Display for TimeDelta {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 let mut components = Vec::new();
283
284 if self.weeks != 0 {
285 components.push(format!(
286 "{} week{}",
287 self.weeks,
288 if self.weeks.abs() == 1 { "" } else { "s" }
289 ));
290 }
291 if self.days != 0 {
292 components.push(format!(
293 "{} day{}",
294 self.days,
295 if self.days.abs() == 1 { "" } else { "s" }
296 ));
297 }
298 if self.hours != 0 {
299 components.push(format!(
300 "{} hour{}",
301 self.hours,
302 if self.hours.abs() == 1 { "" } else { "s" }
303 ));
304 }
305 if self.minutes != 0 {
306 components.push(format!(
307 "{} minute{}",
308 self.minutes,
309 if self.minutes.abs() == 1 { "" } else { "s" }
310 ));
311 }
312 if self.seconds != 0 || components.is_empty() {
313 components.push(format!(
315 "{} second{}",
316 self.seconds,
317 if self.seconds.abs() == 1 { "" } else { "s" }
318 ));
319 }
320
321 write!(f, "{}", components.join(", "))
322 }
323}
324
325impl Default for TimeDelta {
326 fn default() -> Self {
327 Self {
328 weeks: 0,
329 days: 0,
330 hours: 0,
331 minutes: 0,
332 seconds: 0,
333 }
334 }
335}
336
337#[derive(Debug, Clone, Copy)]
339pub enum TimeZone {
340 UTC,
341 KST, EST, PST, JST, IST, CET, AST, CST, MST, AKST, HST, BST, WET, EET, SAST, EAT, AEST, ACST, AWST, CSTAsia, SGT, HKT, }
364
365impl TimeZone {
366 pub const fn offset_in_seconds(&self) -> i64 {
368 match self {
369 TimeZone::UTC => OFFSET_UTC,
370 TimeZone::KST => OFFSET_KST,
371 TimeZone::EST => OFFSET_EST,
372 TimeZone::PST => OFFSET_PST,
373 TimeZone::JST => OFFSET_JST,
374 TimeZone::IST => OFFSET_IST,
375 TimeZone::CET => OFFSET_CET,
376 TimeZone::AST => OFFSET_AST,
377 TimeZone::CST => OFFSET_CST,
378 TimeZone::MST => OFFSET_MST,
379 TimeZone::AKST => OFFSET_AKST,
380 TimeZone::HST => OFFSET_HST,
381 TimeZone::BST => OFFSET_BST,
382 TimeZone::WET => OFFSET_WET,
383 TimeZone::EET => OFFSET_EET,
384 TimeZone::SAST => OFFSET_SAST,
385 TimeZone::EAT => OFFSET_EAT,
386 TimeZone::AEST => OFFSET_AEST,
387 TimeZone::ACST => OFFSET_ACST,
388 TimeZone::AWST => OFFSET_AWST,
389 TimeZone::CSTAsia => OFFSET_CST_ASIA,
390 TimeZone::SGT => OFFSET_SGT,
391 TimeZone::HKT => OFFSET_HKT,
392 }
393 }
394}
395
396pub fn now(timezone: TimeZone) -> Result<DateTime, String> {
427 let now = SystemTime::now();
429 let duration_since_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards");
430
431 let total_seconds = duration_since_epoch.as_secs();
433
434 let adjusted_seconds = adjust_second_with_timezone(total_seconds, timezone);
439
440 calculate_date_since_epoch(adjusted_seconds as i64, timezone)
441}
442
443pub const fn is_leap_year(year: u64) -> bool {
463 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
466}
467
468pub fn days_in_month(month: u64, year: u64) -> u64 {
492 match month {
493 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 => {
496 if is_leap_year(year) {
498 29
499 } else {
500 28
501 }
502 }
503 _ => 0, }
505}
506
507pub const fn compute_total_seconds(
508 weeks: i64,
509 days: i64,
510 hours: i64,
511 minutes: i64,
512 seconds: i64,
513) -> i64 {
514 weeks * SECONDS_IN_WEEK as i64
515 + days * SECONDS_IN_DAY as i64
516 + hours * SECONDS_IN_HOUR as i64
517 + minutes * SECONDS_IN_MINUTE as i64
518 + seconds
519}
520
521pub const fn adjust_second_with_timezone(total_seconds: u64, timezone: TimeZone) -> u64 {
522 let timezone_offset = timezone.offset_in_seconds();
523 let adjusted_seconds = (total_seconds as i64 + timezone_offset) as u64;
524 adjusted_seconds
525}
526
527pub fn calculate_date_since_epoch(
528 adjusted_seconds: i64,
529 timezone: TimeZone,
530) -> Result<DateTime, String> {
531 let mut days = adjusted_seconds as u64 / SECONDS_IN_DAY as u64;
533 let remainder_seconds = adjusted_seconds % SECONDS_IN_DAY;
534 let hour = (remainder_seconds / SECONDS_IN_HOUR) as u64;
535 let remainder_seconds = remainder_seconds % SECONDS_IN_HOUR;
536 let minute = (remainder_seconds / SECONDS_IN_MINUTE) as u64;
537 let second = (remainder_seconds % SECONDS_IN_MINUTE) as u64;
538
539 let mut year = 1970;
541 while days >= if is_leap_year(year) { 366 } else { 365 } {
542 days -= if is_leap_year(year) { 366 } else { 365 };
543 year += 1;
544 }
545
546 let mut month = 1;
548 while days >= days_in_month(month, year) {
549 days -= days_in_month(month, year);
550 month += 1;
551 }
552 let day = days as u64 + 1; DateTime::new(year, month, day, hour, minute, second, timezone)
556}