timekit/
lib.rs

1// Bring in the constants from const.rs
2pub mod constants;
3
4use constants::*;
5use std::time::{SystemTime, UNIX_EPOCH};
6use std::{fmt, ops::Add, ops::Sub};
7
8/// Struct for holding the full date and time information.
9#[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    /// Creates a new `DateTime` object.
22    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        // Unix 초를 계산하기 위해 UTC 시간 기준으로 보정
48        let mut total_seconds = 0;
49
50        // 연도 계산
51        for y in 1970..year {
52            total_seconds += if is_leap_year(y) { 366 } else { 365 } * SECONDS_IN_DAY;
53        }
54
55        // 월 계산
56        for m in 1..month {
57            total_seconds += days_in_month(m, year) as i64 * SECONDS_IN_DAY;
58        }
59
60        // 일, 시, 분, 초 계산
61        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        // 시간대 오프셋 적용 (UTC 기준으로 보정)
67        total_seconds -= timezone.offset_in_seconds();
68
69        // UTC 기준의 `DateTime` 생성
70        let utc_datetime = Self::from_unix_seconds(total_seconds, timezone)?;
71
72        Ok(utc_datetime)
73    }
74
75    /// Calculate the total seconds since Unix Epoch (1970-01-01 00:00:00)
76    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        // 연도는 1970년부터 시작
85        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        // 1. 연도 계산: 1970년부터 현재 연도 이전까지의 총 초 계산
92        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        // 2. 월 계산: 1월부터 현재 월 이전까지의 총 초 계산
101        for m in 1..month {
102            total_seconds += days_in_month(m, year) as i64 * SECONDS_IN_DAY;
103        }
104
105        // 3. 일 계산: 현재 월에서 1일부터 현재 일 이전까지의 총 초 계산
106        total_seconds += (day as i64 - 1) * SECONDS_IN_DAY;
107
108        // 4. 시, 분, 초 계산
109        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        // 연도 계산
131        for year in 1970..self.year {
132            total_seconds += if is_leap_year(year) { 366 } else { 365 } * SECONDS_IN_DAY;
133        }
134
135        // 월 계산
136        for month in 1..self.month {
137            total_seconds += days_in_month(month, self.year) as i64 * SECONDS_IN_DAY;
138        }
139
140        // 일, 시, 분, 초 계산
141        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        // 시간대 오프셋 적용 (UTC 기준으로 변환)
147        total_seconds - self.timezone.offset_in_seconds()
148    }
149
150    pub fn from_unix_seconds(unix_seconds: i64, timezone: TimeZone) -> Result<Self, String> {
151        // 시간대 오프셋 적용 (UTC에서 로컬 시간으로 변환)
152        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        // 연도 계산
160        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        // 월 계산
167        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        // 일, 시, 분, 초 계산
174        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(); // 현재 시간을 Unix 시간으로 변환
194        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; // 초 단위로 더하기
203        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) // 다시 DateTime으로 변환
209    }
210
211    pub fn sub_timedelta(&self, delta: TimeDelta) -> Result<Self, String> {
212        let current_unix = self.to_unix_seconds(); // 현재 시간을 Unix 시간으로 변환
213        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; // 초 단위로 빼기
221        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) // 다시 DateTime으로 변환
229    }
230}
231
232impl fmt::Display for DateTime {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        // Format the DateTime struct to a readable "YYYY-MM-DD HH:MM:SS" format.
235        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/// TimeDelta struct to represent a time difference similar to Python's timedelta.
271#[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            // 항상 seconds는 출력
314            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/// Enum for representing time zones with precomputed UTC offsets in seconds.
338#[derive(Debug, Clone, Copy)]
339pub enum TimeZone {
340    UTC,
341    KST,     // Korea Standard Time (UTC+9)
342    EST,     // Eastern Standard Time (UTC-5)
343    PST,     // Pacific Standard Time (UTC-8)
344    JST,     // Japan Standard Time (UTC+9)
345    IST,     // India Standard Time (UTC+5:30)
346    CET,     // Central European Time (UTC+1)
347    AST,     // Atlantic Standard Time (UTC-4)
348    CST,     // Central Standard Time (UTC-6)
349    MST,     // Mountain Standard Time (UTC-7)
350    AKST,    // Alaska Standard Time (UTC-9)
351    HST,     // Hawaii Standard Time (UTC-10)
352    BST,     // British Summer Time (UTC+1)
353    WET,     // Western European Time (UTC+0)
354    EET,     // Eastern European Time (UTC+2)
355    SAST,    // South Africa Standard Time (UTC+2)
356    EAT,     // East Africa Time (UTC+3)
357    AEST,    // Australian Eastern Standard Time (UTC+10)
358    ACST,    // Australian Central Standard Time (UTC+9:30)
359    AWST,    // Australian Western Standard Time (UTC+8)
360    CSTAsia, // China Standard Time (UTC+8)
361    SGT,     // Singapore Time (UTC+8)
362    HKT,     // Hong Kong Time (UTC+8)
363}
364
365impl TimeZone {
366    /// Returns the precomputed UTC offset in seconds for each time zone.
367    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
396/// Returns the current date and time adjusted for the specified time zone.
397///
398/// This function calculates the current date and time based on the system's current time
399/// (measured as the number of seconds since the UNIX Epoch: 1970-01-01 00:00:00 UTC)
400/// and adjusts it according to the time zone provided. The time zone offsets are hardcoded
401/// to avoid unnecessary runtime computation.
402///
403/// The function follows these steps:
404/// 1. Retrieves the current system time as seconds since the UNIX Epoch.
405/// 2. Applies the specified time zone's UTC offset to the seconds.
406/// 3. Converts the adjusted seconds into days, hours, minutes, and seconds.
407/// 4. Determines the corresponding year, month, and day using the leap year rules.
408/// 5. Returns the computed time as a `DateTime` object containing the year, month, day, hour, minute, and second.
409///
410/// # Parameters:
411/// * `timezone`: The `TimeZone` enum that specifies the time zone for which the current time should be adjusted.
412///
413/// # Returns:
414/// * `DateTime`: A struct containing the current year, month, day, hour, minute, and second, adjusted to the specified time zone.
415///
416/// # Panics:
417/// * The function will panic if the system's time goes backwards (i.e., if the current time is somehow earlier than the UNIX Epoch).
418///
419/// # Example:
420/// ```
421/// use timekit;
422/// use timekit::TimeZone;
423/// let current_time_kst = timekit::now(TimeZone::KST);  // Returns current time in Korea Standard Time (KST).
424/// let current_time_utc = timekit::now(TimeZone::UTC);  // Returns current time in UTC.
425/// ```
426pub fn now(timezone: TimeZone) -> Result<DateTime, String> {
427    // Get the current system time since UNIX_EPOCH in seconds and milliseconds.
428    let now = SystemTime::now();
429    let duration_since_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards");
430
431    // Total seconds since UNIX epoch.
432    let total_seconds = duration_since_epoch.as_secs();
433
434    // Get the time zone offset in seconds.
435    //let timezone_offset = timezone.offset_in_seconds();
436    //let adjusted_seconds = (total_seconds as i64 + timezone_offset) as u64;
437    // Adjust total seconds based on the time zone offset.
438    let adjusted_seconds = adjust_second_with_timezone(total_seconds, timezone);
439
440    calculate_date_since_epoch(adjusted_seconds as i64, timezone)
441}
442
443/// Determines if a given year is a leap year.
444///
445/// A leap year is a year that is divisible by 4 but not divisible by 100,
446/// except when the year is also divisible by 400. This rule is part of the
447/// Gregorian calendar, which adds an extra day to February (29 days) to
448/// keep the calendar year synchronized with the astronomical year.
449///
450/// # Parameters:
451/// * `year`: The year as a `u64` to be checked for leap year status.
452///
453/// # Returns:
454/// * `true` if the year is a leap year, otherwise `false`.
455///
456/// # Example:
457/// ```
458/// use timekit::is_leap_year;
459/// let leap_year = is_leap_year(2024);  // true
460/// let common_year = is_leap_year(2023);  // false
461/// ```
462pub const fn is_leap_year(year: u64) -> bool {
463    // A leap year is divisible by 4 but not divisible by 100,
464    // except if it is divisible by 400.
465    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
466}
467
468/// Returns the number of days in a given month and year.
469///
470/// This function returns the number of days in a month. It accounts for leap years
471/// in February, where there are 29 days instead of the usual 28. All other months
472/// follow the standard day count:
473/// - January, March, May, July, August, October, and December have 31 days.
474/// - April, June, September, and November have 30 days.
475/// - February has 28 days in common years and 29 days in leap years.
476///
477/// # Parameters:
478/// * `month`: The month (1-12) as a `u64`. 1 corresponds to January, and 12 corresponds to December.
479/// * `year`: The year as a `u64`. The year is needed to determine whether February has 28 or 29 days in case of a leap year.
480///
481/// # Returns:
482/// * The number of days in the specified month as a `u64`.
483///
484/// # Example:
485/// ```
486/// use timekit::days_in_month;
487/// let days_in_january = days_in_month(1, 2024);  // 31
488/// let days_in_february_leap_year = days_in_month(2, 2024);  // 29
489/// let days_in_february_common_year = days_in_month(2, 2023);  // 28
490/// ```
491pub fn days_in_month(month: u64, year: u64) -> u64 {
492    match month {
493        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, // January, March, May, July, August, October, December have 31 days
494        4 | 6 | 9 | 11 => 30,              // April, June, September, November have 30 days
495        2 => {
496            // February has 29 days in a leap year, otherwise it has 28 days
497            if is_leap_year(year) {
498                29
499            } else {
500                28
501            }
502        }
503        _ => 0, // Invalid month input, returns 0 (shouldn't happen with proper input validation)
504    }
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    // Convert adjusted seconds into days, hours, minutes, and seconds.
532    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    // Year calculation (starting from 1970).
540    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    // Month and day calculation.
547    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; // Days start from 1.
553
554    // Return the DateTime object.
555    DateTime::new(year, month, day, hour, minute, second, timezone)
556}