Skip to main content

rustolio_utils/time/
mod.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11mod calendar;
12mod date;
13mod time;
14mod timezone;
15
16pub use ::time::{Month, Weekday};
17
18pub use calendar::{CalendarMonth, CalendarWeek};
19pub use date::Date;
20pub use time::Time;
21pub use timezone::Timezone;
22
23const MILLISECONDS_PER_SECOND: i64 = 1000;
24const MILLISECONDS_PER_MINUTE: i64 = MILLISECONDS_PER_SECOND * 60;
25const MILLISECONDS_PER_HOUR: i64 = MILLISECONDS_PER_MINUTE * 60;
26const MILLISECONDS_PER_DAY: i64 = MILLISECONDS_PER_HOUR * 24;
27const DAYS_FROM_ZERO_TO_UNIX: i64 = 719528;
28
29pub type Result<T> = std::result::Result<T, Error>;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Error {
33    OutOfRange,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct DateTime(::time::UtcDateTime, Timezone);
38
39impl DateTime {
40    pub fn new(date: Date, time: Time) -> Self {
41        let Date(date) = date;
42        let Time(time, timezone) = time;
43        let dt = ::time::UtcDateTime::new(date, time);
44        Self(dt, timezone)
45    }
46
47    pub const fn from_unix_timestamp(timestamp: i64, timezone: Timezone) -> Result<Self> {
48        let timestamp = timestamp + timezone.utc_offset_millis();
49        let Ok(utc) = ::time::UtcDateTime::from_unix_timestamp_nanos(timestamp as i128 * 1_000_000)
50        else {
51            return Err(Error::OutOfRange);
52        };
53        Ok(Self(utc, timezone))
54    }
55
56    pub const fn to_timezone(self, timezone: Timezone) -> Result<Self> {
57        let timestamp = self.unix_timestamp() - self.1.utc_offset_millis();
58        Self::from_unix_timestamp(timestamp, timezone)
59    }
60
61    pub fn now(timezone: Timezone) -> Result<Self> {
62        #[cfg(target_arch = "wasm32")]
63        let timestamp = js_sys::Date::now() as i64;
64
65        #[cfg(not(target_arch = "wasm32"))]
66        let timestamp = std::time::SystemTime::now()
67            .duration_since(std::time::UNIX_EPOCH)
68            .expect("Time went backwards")
69            .as_millis() as i64;
70
71        Self::from_unix_timestamp(timestamp, timezone)
72    }
73
74    pub const fn into_parts(self) -> (Date, Time) {
75        (Date(self.0.date()), Time(self.0.time(), self.1))
76    }
77
78    pub const fn hour(&self) -> u8 {
79        self.0.hour()
80    }
81
82    pub const fn minute(&self) -> u8 {
83        self.0.minute()
84    }
85
86    pub const fn second(&self) -> u8 {
87        self.0.second()
88    }
89
90    pub const fn millisecond(&self) -> u16 {
91        self.0.millisecond()
92    }
93
94    pub const fn timezone(&self) -> Timezone {
95        self.1
96    }
97
98    pub const fn year(&self) -> i32 {
99        self.0.year()
100    }
101
102    pub const fn month(&self) -> Month {
103        self.0.month()
104    }
105
106    pub const fn day(&self) -> u8 {
107        self.0.day()
108    }
109
110    pub const fn weekday(&self) -> Weekday {
111        self.0.weekday()
112    }
113
114    pub const fn unix_timestamp(&self) -> i64 {
115        (self.0.unix_timestamp_nanos() / 1_000_000) as i64
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::i64;
122
123    use super::*;
124
125    #[test]
126    fn test_date_time() {
127        let timestamp = 1672531200000;
128        let dt = DateTime::from_unix_timestamp(timestamp, Timezone::UTC).unwrap();
129        let expected = DateTime::new(
130            Date::new(1, Month::January, 2023).unwrap(),
131            Time::new(0, 0, 0, 0, Timezone::UTC).unwrap(),
132        );
133        assert_eq!(dt, expected);
134
135        let dt = dt.to_timezone(Timezone::CET).unwrap();
136        let expected = DateTime::new(
137            Date::new(1, Month::January, 2023).unwrap(),
138            Time::new(1, 0, 0, 0, Timezone::CET).unwrap(),
139        );
140        assert_eq!(dt, expected);
141
142        let dt = dt.to_timezone(Timezone::HST).unwrap();
143        let expected = DateTime::new(
144            Date::new(31, Month::December, 2022).unwrap(),
145            Time::new(14, 0, 0, 0, Timezone::HST).unwrap(),
146        );
147        assert_eq!(dt, expected);
148
149        let timestamp = 0;
150        let dt = DateTime::from_unix_timestamp(timestamp, Timezone::UTC).unwrap();
151        let expected = DateTime::new(
152            Date::new(1, Month::January, 1970).unwrap(),
153            Time::new(0, 0, 0, 0, Timezone::UTC).unwrap(),
154        );
155        assert_eq!(dt, expected);
156
157        let timestamp = -3 * MILLISECONDS_PER_HOUR;
158        let dt = DateTime::from_unix_timestamp(timestamp, Timezone::CET).unwrap();
159        let expected = DateTime::new(
160            Date::new(31, Month::December, 1969).unwrap(),
161            Time::new(22, 0, 0, 0, Timezone::CET).unwrap(),
162        );
163        assert_eq!(dt, expected);
164
165        let timestamp = -25567 * MILLISECONDS_PER_DAY;
166        let dt = DateTime::from_unix_timestamp(timestamp, Timezone::UTC).unwrap();
167        let expected = DateTime::new(
168            Date::new(1, Month::January, 1900).unwrap(),
169            Time::new(0, 0, 0, 0, Timezone::UTC).unwrap(),
170        );
171        assert_eq!(dt, expected);
172
173        let now = std::time::SystemTime::now()
174            .duration_since(std::time::UNIX_EPOCH)
175            .expect("Time went backwards")
176            .as_secs() as i64;
177        let dt = DateTime::from_unix_timestamp(now, Timezone::UTC).unwrap();
178        let timestamp = dt.unix_timestamp();
179        assert_eq!(timestamp, now);
180        let dt2 = DateTime::new(
181            Date::new(dt.day(), dt.month(), dt.year()).unwrap(),
182            Time::new(
183                dt.hour(),
184                dt.minute(),
185                dt.second(),
186                dt.millisecond(),
187                dt.timezone(),
188            )
189            .unwrap(),
190        );
191        assert_eq!(dt, dt2);
192    }
193
194    #[test]
195    fn test_date_time_big_numbers() {
196        let timestamp = 99_998_287_287_821;
197        let dt = DateTime::from_unix_timestamp(timestamp, Timezone::UTC).unwrap();
198        let expected = DateTime::new(
199            Date::new(27, Month::October, 5138).unwrap(),
200            Time::new(14, 1, 27, 821, Timezone::UTC).unwrap(),
201        );
202        assert_eq!(dt, expected);
203
204        let timestamp = -99_998_287_287_821;
205        let dt = DateTime::from_unix_timestamp(timestamp, Timezone::UTC).unwrap();
206        let expected = DateTime::new(
207            Date::new(7, Month::March, -1199).unwrap(),
208            Time::new(9, 58, 32, 179, Timezone::UTC).unwrap(),
209        );
210        assert_eq!(dt, expected);
211    }
212}