dia_time/
time.rs

1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Dia-Time
5
6Copyright (C) 2018-2022, 2024  Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2018-2022".
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU Lesser General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19GNU Lesser General Public License for more details.
20
21You should have received a copy of the GNU Lesser General Public License
22along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
24::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
25*/
26
27//! # Time
28
29use {
30    alloc::borrow::Cow,
31    core::{
32        cmp::Ordering,
33        fmt::{self, Debug, Formatter},
34        time::Duration,
35    },
36    crate::{Error, Month, Result as CrateResult, Weekday, month},
37};
38
39#[cfg(feature="libc")]
40use core::{
41    ffi::CStr,
42    ptr,
43};
44
45#[cfg(all(windows, feature="libc"))]
46use crate::{DAY, HOUR, MINUTE};
47
48#[cfg(any(feature="std", all(feature="std", feature="libc")))]
49use std::time::{SystemTime, UNIX_EPOCH};
50
51/// # Year
52pub type Year = i64;
53
54/// # Day
55pub type Day = u8;
56
57/// # Hour
58pub type Hour = u8;
59
60/// # Minute
61pub type Minute = u8;
62
63/// # Second
64pub type Second = u8;
65
66/// # Unix Second
67pub type UnixSecond = i64;
68
69/// # GMT Offset
70pub type GmtOffset = i32;
71
72#[cfg(test)]
73mod tests;
74
75const MAX_GMT_OFFSET: GmtOffset = 14 * 3600;
76const MIN_GMT_OFFSET: GmtOffset = -12 * 3600;
77
78const ONE_YEAR: UnixSecond = (crate::DAY * 365) as UnixSecond;
79const ONE_LEAP_YEAR: UnixSecond = (crate::DAY * 366) as UnixSecond;
80
81const SECONDS_OF_400_YEARS: UnixSecond = 12_622_780_800;
82const SECONDS_OF_1970_YEARS: UnixSecond = 62_167_219_200;
83
84/// # Time
85///
86/// ## Notes
87///
88/// - Leap years are calculated by [Gregorian calendar][wiki:gregorian-calendar].
89/// - Range of days: `[1..31]`.
90/// - Range of hours: `[0..23]`.
91/// - Range of minutes, seconds: `[0..59]`.
92/// - Range of GMT offsets: `[-12:00..+14:00]` (in seconds).
93///
94/// [wiki:gregorian-calendar]: https://en.wikipedia.org/wiki/Gregorian_calendar
95#[derive(Eq, Hash, PartialOrd, Clone)]
96pub struct Time {
97    year: Year,
98    month: Month,
99    day: Day,
100    weekday: Weekday,
101    hour: Hour,
102    minute: Minute,
103    second: Second,
104    gmt_offset: Option<GmtOffset>,
105}
106
107impl Time {
108
109    /// # Makes new instance
110    pub fn make(year: Year, month: Month, day: Day, hour: Hour, minute: Minute, second: Second, gmt_offset: Option<GmtOffset>)
111    -> CrateResult<Self> {
112        // Month & day
113        if day == 0 || match month {
114            Month::January | Month::March | Month::May | Month::July | Month::August | Month::October | Month::December => day > 31,
115            Month::April | Month::June | Month::September | Month::November => day > 30,
116            Month::February => day > if is_leap_year(year) {
117                month::END_OF_FEBRUARY_IN_LEAP_YEARS
118            } else {
119                month::END_OF_FEBRUARY_IN_COMMON_YEARS
120            },
121        } {
122            return Err(err!("Invalid day: {day} of {month}", day=day, month=month));
123        }
124
125        if hour > 23 {
126            return Err(err!("Invalid hour: {hour}", hour=hour));
127        }
128        if minute > 59 {
129            return Err(err!("Invalid minute: {minute}", minute=minute));
130        }
131        if second > 59 {
132            return Err(err!("Invalid second: {second}", second=second));
133        }
134
135        if let Some(gmt_offset) = gmt_offset.as_ref() {
136            if gmt_offset < &MIN_GMT_OFFSET || gmt_offset > &MAX_GMT_OFFSET {
137                return Err(err!("Invalid GMT offset: {gmt_offset}", gmt_offset=gmt_offset));
138            }
139        }
140
141        let mut result = Self {
142            year, month, day,
143            weekday: Weekday::Monday,
144            hour, minute, second,
145            gmt_offset,
146        };
147        result.weekday = {
148            let tmp = result.try_into_unix_seconds()?
149                .checked_add(gmt_offset.unwrap_or(0).into()).ok_or_else(|| err!())?
150                .checked_abs().ok_or_else(|| err!())?;
151            Weekday::try_from_unix((tmp / i64::try_from(crate::DAY).map_err(|_| err!())? + 4) % 7)?
152        };
153        Ok(result)
154    }
155
156    /// # Year
157    pub const fn year(&self) -> Year {
158        self.year
159    }
160
161    /// # Month
162    pub const fn month(&self) -> Month {
163        self.month
164    }
165
166    /// # Day
167    pub const fn day(&self) -> Day {
168        self.day
169    }
170
171    /// # Weekday
172    pub const fn weekday(&self) -> Weekday {
173        self.weekday
174    }
175
176    /// # Hour
177    pub const fn hour(&self) -> Hour {
178        self.hour
179    }
180
181    /// # Minute
182    pub const fn minute(&self) -> Minute {
183        self.minute
184    }
185
186    /// # Second
187    pub const fn second(&self) -> Second {
188        self.second
189    }
190
191    /// # GMT offset
192    pub const fn gmt_offset(&self) -> Option<GmtOffset> {
193        self.gmt_offset
194    }
195
196    /// # Makes current time in UTC
197    #[cfg(feature="std")]
198    #[doc(cfg(feature="std"))]
199    pub fn make_utc() -> CrateResult<Self> {
200        let (unix_seconds, is_positive) = match {
201            SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).map_err(|e| e.duration().as_secs())
202        } {
203            Ok(unix_seconds) => (unix_seconds, true),
204            Err(unix_seconds) => (unix_seconds, false),
205        };
206        let unix_seconds = UnixSecond::try_from(unix_seconds).map_err(|_| err!("Failed to convert {} into UnixSecond", unix_seconds))?;
207        try_unix_seconds_into_time(if is_positive { unix_seconds } else { -unix_seconds }, None)
208    }
209
210    /// # Converts self into UTC time
211    pub fn try_into_utc(&self) -> CrateResult<Self> {
212        match self.gmt_offset {
213            None => Ok(self.clone()),
214            Some(0) => Ok(Self {
215                gmt_offset: None,
216                ..*self
217            }),
218            Some(gmt_offset) => {
219                let today_seconds = (crate::HOUR * u64::from(self.hour) + crate::MINUTE * u64::from(self.minute) + u64::from(self.second))
220                    as GmtOffset;
221                let seconds = today_seconds - gmt_offset;
222                let (year, month, day, hour, minute, second) = if seconds < 0 {
223                    let (year, month, day) = match self.day {
224                        1 => {
225                            let year = match self.month {
226                                Month::January => self.year.checked_sub(1)
227                                    .ok_or_else(|| err!("Failed to subtract '{year}' by 1", year=self.year))?,
228                                _ => self.year,
229                            };
230                            let month = self.month.wrapping_last();
231                            let day = month::last_day_of_month(&month, is_leap_year(year));
232                            (year, month, day)
233                        },
234                        _ => (self.year, self.month, self.day - 1),
235                    };
236                    let (_, hour, minute, second) = crate::duration_to_dhms(&Duration::from_secs(crate::DAY - seconds.abs() as u64));
237                    (year, month, day, hour, minute, second)
238                } else if seconds < crate::DAY as GmtOffset {
239                    let (_, hour, minute, second) = crate::duration_to_dhms(&Duration::from_secs(seconds as u64));
240                    (self.year, self.month, self.day, hour, minute, second)
241                } else {
242                    let (year, month, day) = if self.day == month::last_day_of_month(&self.month, is_leap_year(self.year)) {
243                        let year = match self.month {
244                            Month::December => self.year.checked_add(1)
245                                .ok_or_else(|| err!("Failed to add 1 to '{year}'", year=self.year))?,
246                            _ => self.year,
247                        };
248                        (year, self.month.wrapping_next(), 1)
249                    } else {
250                        (self.year, self.month, self.day + 1)
251                    };
252                    let (_, hour, minute, second) = crate::duration_to_dhms(&Duration::from_secs(seconds as u64 - crate::DAY));
253                    (year, month, day, hour, minute, second)
254                };
255                Self::make(year, month, day, hour as Hour, minute as Minute, second as Second, None)
256            },
257        }
258    }
259
260    /// # Checks if this is a UTC time
261    pub fn is_utc(&self) -> bool {
262        matches!(&self.gmt_offset, Some(0) | None)
263    }
264
265    /// # Makes current local time
266    #[cfg(all(feature="std", feature="libc"))]
267    #[doc(cfg(all(feature="std", feature="libc")))]
268    pub fn make_local() -> CrateResult<Self> {
269        let gmt_offset = load_gmt_offset()?;
270
271        let (unix_seconds, is_positive) = match {
272            SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).map_err(|e| e.duration().as_secs())
273        } {
274            Ok(unix_seconds) => (unix_seconds, true),
275            Err(unix_seconds) => (unix_seconds, false),
276        };
277        let unix_seconds = UnixSecond::try_from(unix_seconds).map_err(|_| err!("Failed to convert {} into UnixSecond", unix_seconds))?;
278        try_unix_seconds_into_time(if is_positive { unix_seconds } else { -unix_seconds }, gmt_offset)
279    }
280
281    /// # Converts self into local time
282    #[cfg(feature="libc")]
283    #[doc(cfg(feature="libc"))]
284    pub fn try_into_local(&self) -> CrateResult<Self> {
285        try_unix_seconds_into_time(self.try_into_unix_seconds()?, load_gmt_offset()?)
286    }
287
288    /// # Converts self into Unix seconds
289    pub fn try_into_unix_seconds(&self) -> CrateResult<UnixSecond> {
290        if self.year >= 0 {
291            self.try_positive_time_into_unix_seconds()
292        } else {
293            self.try_negative_time_into_unix_seconds()
294        }
295    }
296
297    /// # Tries to convert self (positive time) into Unix seconds
298    fn try_positive_time_into_unix_seconds(&self) -> CrateResult<UnixSecond> {
299        if self.year < 0 {
300            return Err(err!("Year is negative"));
301        }
302
303        let mut unix_seconds = match (self.year / 400).checked_mul(SECONDS_OF_400_YEARS).map(|s|
304            s.checked_sub(SECONDS_OF_1970_YEARS).map(|mut s| {
305                for y in 0..self.year % 400 {
306                    s = match s.checked_add(if is_leap_year(y) { ONE_LEAP_YEAR } else { ONE_YEAR }) {
307                        Some(s) => s,
308                        None => return None,
309                    };
310                }
311                Some(s)
312            })
313        ) {
314            Some(Some(Some(unix_seconds))) => unix_seconds,
315            _ => return Err(err!("Year '{}' is too large", self.year))?,
316        };
317
318        let mut month = Month::January;
319        let is_leap_year = is_leap_year(self.year);
320        loop {
321            if month < self.month {
322                unix_seconds = unix_seconds.checked_add(month::seconds_of_month(&month, is_leap_year))
323                    .ok_or_else(|| err!("Time is too large"))?;
324                month = month.wrapping_next();
325            } else {
326                break;
327            }
328        }
329
330        unix_seconds = match unix_seconds.checked_add((crate::DAY * u64::from(self.day - 1)) as UnixSecond).map(|s|
331            s.checked_add((crate::HOUR * u64::from(self.hour)) as UnixSecond).map(|s|
332                s.checked_add((crate::MINUTE * u64::from(self.minute)) as UnixSecond)
333                    .map(|s| s.checked_add(self.second.into()))
334            )
335        ) {
336            Some(Some(Some(Some(unix_seconds)))) => unix_seconds,
337            _ => return Err(err!("Time is too large")),
338        };
339
340        if let Some(gmt_offset) = self.gmt_offset {
341            unix_seconds = unix_seconds.checked_sub(gmt_offset.into()).ok_or_else(|| err!("Time is too small"))?;
342        }
343
344        Ok(unix_seconds)
345    }
346
347    /// # Tries to convert self (negative time) into Unix seconds
348    fn try_negative_time_into_unix_seconds(&self) -> CrateResult<UnixSecond> {
349        if self.year >= 0 {
350            return Err(err!("Year is positive"));
351        }
352
353        let mut unix_seconds = match (self.year / 400).checked_mul(SECONDS_OF_400_YEARS).map(|s|
354            s.checked_sub(SECONDS_OF_1970_YEARS).map(|mut s| {
355                let remaining = (self.year % 400).abs();
356                match remaining {
357                    0 => s.checked_add(ONE_LEAP_YEAR),
358                    _ => {
359                        for y in 1..remaining {
360                            s = match s.checked_sub(if is_leap_year(y) { ONE_LEAP_YEAR } else { ONE_YEAR }) {
361                                Some(s) => s,
362                                None => return None,
363                            };
364                        }
365                        Some(s)
366                    },
367                }
368            })
369        ) {
370            Some(Some(Some(unix_seconds))) => unix_seconds,
371            _ => return Err(err!("Year '{}' is too small", self.year))?,
372        };
373
374        let mut month = Month::December;
375        let is_leap_year = is_leap_year(self.year);
376        loop {
377            if month > self.month {
378                unix_seconds = unix_seconds.checked_sub(month::seconds_of_month(&month, is_leap_year))
379                    .ok_or_else(|| err!("Time is too small"))?;
380                month = month.wrapping_last();
381            } else {
382                break;
383            }
384        }
385
386        unix_seconds = match unix_seconds.checked_sub(
387            month::seconds_of_month(&self.month, is_leap_year) - (crate::DAY * u64::from(self.day)) as UnixSecond
388        ).map(|s| {
389            let today_seconds = crate::HOUR * u64::from(self.hour) + crate::MINUTE * u64::from(self.minute) + u64::from(self.second);
390            s.checked_sub((crate::DAY - today_seconds) as UnixSecond)
391        }) {
392            Some(Some(unix_seconds)) => unix_seconds,
393            _ => return Err(err!("Time is too small")),
394        };
395
396        if let Some(gmt_offset) = self.gmt_offset {
397            unix_seconds = unix_seconds.checked_sub(gmt_offset.into()).ok_or_else(|| err!("Time is too small"))?;
398        }
399
400        Ok(unix_seconds)
401    }
402
403}
404
405impl PartialEq for Time {
406
407    fn eq(&self, other: &Self) -> bool {
408        self.cmp(other) == Ordering::Equal
409    }
410
411}
412
413impl Ord for Time {
414
415    fn cmp(&self, other: &Self) -> Ordering {
416        let self_seconds = match self.year.checked_sub(other.year) {
417            Some(0) => match self.month.to_unix() - other.month.to_unix() {
418                0 => match self.day as i8 - other.day as i8 {
419                    0 => 0 as GmtOffset,
420                    -1 => -(crate::DAY as GmtOffset),
421                    1 => crate::DAY as GmtOffset,
422                    other => return if other > 0 { Ordering::Greater } else { Ordering::Less },
423                },
424                month_delta => {
425                    let is_leap_year = is_leap_year(self.year);
426                    match month_delta {
427                        -1 => if month::last_day_of_month(&self.month, is_leap_year) == self.day && other.day == 1 {
428                            -(crate::DAY as GmtOffset)
429                        } else {
430                            return Ordering::Less;
431                        },
432                        1 => if self.day == 1 && month::last_day_of_month(&other.month, is_leap_year) == other.day {
433                            crate::DAY as GmtOffset
434                        } else {
435                            return Ordering::Greater;
436                        },
437                        other => return if other > 0 { Ordering::Greater } else { Ordering::Less },
438                    }
439                },
440            },
441            Some(1) => match (self.month, self.day, other.month, other.day) {
442                (Month::January, 1, Month::December, 31) => crate::DAY as GmtOffset,
443                _ => return Ordering::Greater,
444            },
445            Some(-1) => match (self.month, self.day, other.month, other.day) {
446                (Month::December, 31, Month::January, 1) => -(crate::DAY as GmtOffset),
447                _ => return Ordering::Less,
448            },
449            _ => return self.year.cmp(&other.year),
450        };
451
452        let self_seconds = self_seconds
453            + (crate::HOUR * u64::from(self.hour) + crate::MINUTE * u64::from(self.minute) + u64::from(self.second)) as GmtOffset
454            - self.gmt_offset.unwrap_or(0);
455        let other_seconds =
456            (crate::HOUR * u64::from(other.hour) + crate::MINUTE * u64::from(other.minute) + u64::from(other.second)) as GmtOffset
457            - other.gmt_offset.unwrap_or(0);
458        self_seconds.cmp(&other_seconds)
459    }
460
461}
462
463impl TryFrom<&Time> for Duration {
464
465    type Error = Error;
466
467    fn try_from(time: &Time) -> Result<Self, Self::Error> {
468        let unix_seconds = time.try_into_unix_seconds()?;
469        u64::try_from(unix_seconds).map(|s| Duration::from_secs(s)).map_err(|_| err!(
470            "Failed to convert '{unix_seconds}' into u64",
471            unix_seconds=unix_seconds,
472        ))
473    }
474
475}
476
477impl TryFrom<Time> for Duration {
478
479    type Error = Error;
480
481    fn try_from(time: Time) -> Result<Self, Self::Error> {
482        Self::try_from(&time)
483    }
484
485}
486
487impl TryFrom<&Duration> for Time {
488
489    type Error = Error;
490
491    fn try_from(duration: &Duration) -> Result<Self, Self::Error> {
492        try_duration_into_time(duration, None)
493    }
494
495}
496
497impl TryFrom<Duration> for Time {
498
499    type Error = Error;
500
501    fn try_from(duration: Duration) -> Result<Self, Self::Error> {
502        Self::try_from(&duration)
503    }
504
505}
506
507#[cfg(feature="std")]
508#[doc(cfg(feature="std"))]
509impl TryFrom<&SystemTime> for Time {
510
511    type Error = Error;
512
513    fn try_from(system_time: &SystemTime) -> Result<Self, Self::Error> {
514        match system_time.duration_since(UNIX_EPOCH) {
515            Ok(duration) => Self::try_from(duration),
516            Err(err) => Err(err!(
517                "Failed calculating duration of {system_time:?} since UNIX Epoch: {err}",
518                system_time=system_time, err=err,
519            )),
520        }
521    }
522
523}
524
525#[cfg(feature="std")]
526#[doc(cfg(feature="std"))]
527impl TryFrom<SystemTime> for Time {
528
529    type Error = Error;
530
531    fn try_from(system_time: SystemTime) -> Result<Self, Self::Error> {
532        Self::try_from(&system_time)
533    }
534
535}
536
537impl Debug for Time {
538
539    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
540        let month = match self.month {
541            Month::January => 1,
542            Month::February => 2,
543            Month::March => 3,
544            Month::April => 4,
545            Month::May => 5,
546            Month::June => 6,
547            Month::July => 7,
548            Month::August => 8,
549            Month::September => 9,
550            Month::October => 10,
551            Month::November => 11,
552            Month::December => 12,
553        };
554        let gmt_offset = match self.gmt_offset {
555            Some(gmt_offset) if gmt_offset != 0 => {
556                let hour = gmt_offset / crate::HOUR as GmtOffset;
557                let minute = ((gmt_offset - (hour * crate::HOUR as GmtOffset)) / crate::MINUTE as GmtOffset).abs();
558                Cow::Owned(format!(
559                    "[{sign}{hour:02}:{minute:02}]",
560                    sign=if gmt_offset >= 0 { concat!('+') } else { concat!() }, hour=hour, minute=minute,
561                ))
562            },
563            _ => Cow::Borrowed("UTC"),
564        };
565        write!(
566            f,
567            "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02} {gmt_offset}",
568            year=self.year, month=month, day=self.day, hour=self.hour, minute=self.minute, second=self.second, gmt_offset=gmt_offset,
569        )
570    }
571
572}
573
574/// # Loads GMT offset
575#[cfg(all(feature="libc", not(windows)))]
576fn load_gmt_offset() -> CrateResult<Option<GmtOffset>> {
577    match unsafe {
578        libc::time(ptr::null_mut())
579    } {
580        -1 => Err(fmt_err_code("time")),
581        time => {
582            let mut tm = libc::tm {
583                tm_year: 0, tm_mon: 0, tm_mday: 0,
584                tm_hour: 0, tm_min: 0, tm_sec: 0,
585                tm_wday: 0, tm_yday: 0, tm_isdst: 0, tm_gmtoff: 0,
586                #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))]
587                tm_zone: ptr::null_mut(),
588                #[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd")))]
589                tm_zone: ptr::null(),
590            };
591            if unsafe {
592                // NOTE: calling localtime_r(&0, ...) will return wrong GMT offset!
593                libc::localtime_r(&time, &mut tm).is_null()
594            } {
595                return Err(fmt_err_code("localtime_r"));
596            }
597            match tm.tm_gmtoff {
598                0 => Ok(None),
599                _ => Ok(Some(GmtOffset::try_from(tm.tm_gmtoff).map_err(|_| err!("Invalid Unix GMT offset: {}", tm.tm_gmtoff))?)),
600            }
601        },
602    }
603}
604
605/// # Loads GMT offset
606#[cfg(all(windows, feature="libc"))]
607fn load_gmt_offset() -> CrateResult<Option<GmtOffset>> {
608    let time = unsafe {
609        libc::time(ptr::null_mut())
610    };
611    let mut local = libc::tm {
612        tm_year: 0, tm_mon: 0, tm_mday: 0, tm_hour: 0, tm_min: 0, tm_sec: 0, tm_wday: 0, tm_yday: 0, tm_isdst: 0,
613    };
614    let mut gmt = local;
615    match unsafe {
616        (libc::localtime_s(&mut local, &time), libc::gmtime_s(&mut gmt, &time))
617    } {
618        (0, 0) => {},
619        (local, gmt) => return Err(fmt_fn_name_and_err_code("localtime_s", if local != 0 { local } else { gmt })),
620    };
621
622    let same_day = local.tm_mday == gmt.tm_mday;
623    match (|| {
624        let day_in_seconds = GmtOffset::try_from(DAY).ok()?;
625        let hour_in_seconds = GmtOffset::try_from(HOUR).ok()?;
626        let minute_in_seconds = GmtOffset::try_from(MINUTE).ok()?;
627
628        let seconds = |tm: &libc::tm| tm.tm_hour.checked_mul(hour_in_seconds)?.checked_add(tm.tm_min.checked_mul(minute_in_seconds)?);
629        let local_seconds = seconds(&local)?;
630        let gmt_seconds = seconds(&gmt)?;
631        let result = match cmp_tm(&local, &gmt) {
632            Ordering::Equal => 0,
633            Ordering::Greater | Ordering::Less if same_day => local_seconds.checked_sub(gmt_seconds)?,
634            Ordering::Greater => local_seconds.checked_add(day_in_seconds.checked_sub(gmt_seconds)?)?,
635            Ordering::Less => gmt_seconds.checked_add(day_in_seconds.checked_sub(local_seconds)?).map(|n| -n)?,
636        };
637        if local.tm_isdst > 0 {
638            result.checked_sub(hour_in_seconds)
639        } else {
640            Some(result)
641        }
642    })().ok_or_else(|| err!())? {
643        0 => Ok(None),
644        other => Ok(Some(GmtOffset::try_from(other).map_err(|_| err!("Invalid Unix GMT offset: {other}"))?)),
645    }
646}
647
648#[cfg(all(windows, feature="libc"))]
649fn cmp_tm(first: &libc::tm, second: &libc::tm) -> Ordering {
650    first.tm_year.cmp(&second.tm_year).then_with(||
651        first.tm_mon.cmp(&second.tm_mon).then_with(||
652            first.tm_mday.cmp(&second.tm_mday).then_with(||
653                first.tm_hour.cmp(&second.tm_hour).then_with(||
654                    first.tm_min.cmp(&second.tm_min).then_with(|| first.tm_sec.cmp(&second.tm_sec))
655                )
656            )
657        )
658    )
659}
660
661/// # Tries to convert a value of Unix seconds into time
662pub fn try_unix_seconds_into_time(unix_seconds: UnixSecond, gmt_offset: Option<GmtOffset>) -> CrateResult<Time> {
663    let unix_seconds = match gmt_offset {
664        Some(gmt_offset) => unix_seconds.checked_add(gmt_offset.into()).ok_or_else(|| err!(
665            "Failed adding GMT offset '{gmt_offset}' to Unix seconds '{unix_seconds}'", gmt_offset=gmt_offset, unix_seconds=unix_seconds,
666        ))?,
667        None => unix_seconds,
668    };
669
670    fn try_positive(all_seconds: UnixSecond, gmt_offset: Option<GmtOffset>) -> CrateResult<Time> {
671        let mut year = all_seconds / SECONDS_OF_400_YEARS * 400;
672        let mut all_seconds = all_seconds % SECONDS_OF_400_YEARS;
673        loop {
674            let one_year = if is_leap_year(year) { ONE_LEAP_YEAR } else { ONE_YEAR };
675            match all_seconds.checked_sub(one_year) {
676                Some(new_secs) if new_secs >= 0 => {
677                    year += 1;
678                    all_seconds = new_secs;
679                },
680                _ => break,
681            };
682        }
683
684        let (month, day, hour, minute, second) = month_day_hour_minute_second(is_leap_year(year), all_seconds);
685        Time::make(year, month, day, hour, minute, second, gmt_offset)
686    }
687
688    fn try_negative(all_seconds: UnixSecond, gmt_offset: Option<GmtOffset>) -> CrateResult<Time> {
689        let mut year = (all_seconds / SECONDS_OF_400_YEARS * 400).checked_sub(1).ok_or_else(|| err!("Failed to calculating year"))?;
690        let mut all_seconds = all_seconds % SECONDS_OF_400_YEARS;
691        loop {
692            let one_year = if is_leap_year(year) { ONE_LEAP_YEAR } else { ONE_YEAR };
693            match all_seconds.checked_add(one_year) {
694                Some(new_secs) if new_secs <= 0 => {
695                    year -= 1;
696                    all_seconds = new_secs;
697                },
698                _ => break,
699            };
700        }
701
702        let is_leap_year = is_leap_year(year);
703        all_seconds += if is_leap_year { ONE_LEAP_YEAR } else { ONE_YEAR };
704        let (month, day, hour, minute, second) = month_day_hour_minute_second(is_leap_year, all_seconds);
705        Time::make(year, month, day, hour, minute, second, gmt_offset)
706    }
707
708    fn month_day_hour_minute_second(is_leap_year: bool, mut all_seconds: UnixSecond) -> (Month, Day, Hour, Minute, Second) {
709        let mut month = Month::January;
710        loop {
711            match all_seconds.checked_sub(month::seconds_of_month(&month, is_leap_year)) {
712                Some(new_secs) if new_secs >= 0 => {
713                    month = month.wrapping_next();
714                    all_seconds = new_secs;
715                },
716                _ => break,
717            };
718        }
719
720        let day = ((all_seconds / crate::DAY as UnixSecond) + 1) as Day;
721        let all_seconds = all_seconds % crate::DAY as UnixSecond;
722
723        let hour = (all_seconds / crate::HOUR as UnixSecond) as Hour;
724        let all_seconds = all_seconds % crate::HOUR as UnixSecond;
725
726        let minute = (all_seconds / crate::MINUTE as UnixSecond) as Minute;
727        let second = (all_seconds % crate::MINUTE as UnixSecond) as Second;
728
729        (month, day, hour, minute, second)
730    }
731
732    let all_seconds = unix_seconds.checked_add(SECONDS_OF_1970_YEARS)
733        .ok_or_else(|| err!("Failed transforming Unix seconds: {}", unix_seconds))?;
734    if all_seconds >= 0 {
735        try_positive(all_seconds, gmt_offset)
736    } else {
737        try_negative(all_seconds, gmt_offset)
738    }
739}
740
741/// # Tries to convert a duration into time
742fn try_duration_into_time(duration: &Duration, gmt_offset: Option<GmtOffset>) -> CrateResult<Time> {
743    let unix_seconds = duration.as_secs();
744    let unix_seconds = UnixSecond::try_from(unix_seconds)
745        .map_err(|_| err!("Failed to convert {} into UnixSecond", unix_seconds))?;
746    try_unix_seconds_into_time(unix_seconds, gmt_offset)
747}
748
749/// # Tries to convert a duration into local time
750#[cfg(feature="libc")]
751#[doc(cfg(feature="libc"))]
752pub fn try_duration_into_local_time(duration: &Duration) -> CrateResult<Time> {
753    try_duration_into_time(duration, load_gmt_offset()?)
754}
755
756/// # Checks if a year is leap
757fn is_leap_year(year: Year) -> bool {
758    match year % 4 {
759        0 => match year % 100 {
760            0 => year % 400 == 0,
761            _ => true,
762        }
763        _ => false,
764    }
765}
766
767#[cfg(all(feature="libc", not(windows)))]
768fn fmt_err_code<S>(fn_name: S) -> Error where S: AsRef<str> {
769    fmt_fn_name_and_err_code(fn_name, unsafe {
770        *libc::__errno_location()
771    })
772}
773
774#[cfg(feature="libc")]
775fn fmt_fn_name_and_err_code<S>(fn_name: S, err_code: i32) -> Error where S: AsRef<str> {
776    err!(
777        "libc::{fn_name}() failed: {err_code}: {err:?}",
778        fn_name=fn_name.as_ref(),
779        err=unsafe { CStr::from_ptr(libc::strerror(err_code)) },
780    )
781}