aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/time.rs
blob: 4ac4ce7db90000a202f97e4f7d779a013082e888 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
use crate::Date;
use chrono::{
    DateTime, Duration, FixedOffset as Offset, Local, NaiveDateTime, NaiveTime, TimeZone, Timelike,
    Utc,
};
use std::{cmp::Ordering, ops::Sub};

/// A convenience wrapper around [DateTime][t] with fixed timezone
///
/// [t]: chrono::DateTime
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
    inner: DateTime<Offset>,
}

impl From<DateTime<Offset>> for Time {
    fn from(inner: DateTime<Offset>) -> Self {
        Self { inner }
    }
}

impl<'t> Sub for &'t Time {
    type Output = Duration;

    fn sub(self, o: &'t Time) -> Self::Output {
        self.inner - o.inner
    }
}

impl Time {
    /// Get the current local time and pin it to a fixed Tz offset
    pub fn now() -> Self {
        let now = Local::now();
        Self {
            inner: build_datetime(now.time()),
        }
    }

    pub(crate) fn date(&self) -> chrono::Date<Offset> {
        self.inner.date()
    }

    /// Check if a time stamp happened _after_ a date
    pub fn after(&self, date: &Date) -> bool {
        &Date::from(self.date()) > date
    }
    
    #[cfg(test)]
    pub(crate) fn fixed(hour: u32, min: u32, sec: u32) -> Self {
        Self {
            inner: build_datetime(NaiveTime::from_hms(hour, min, sec)),
        }
    }

    /// Return a new instance that is rounded to nearest 15 minutes
    ///
    /// It uses the internally provided offset to do rounding, meaning
    /// that the timezone information will not change, even when
    /// rounding values that were created in a different timezone.
    pub fn round(&self) -> Self {
        let naive = self.inner.time();

        let (new_min, incr_hour) = match naive.minute() {
            // 0-7 => (0, false)
            m if m > 0 && m < 7 => (0, false),
            // 7-22 => (15, false)
            m if m >= 7 && m < 22 => (15, false),
            // 22-37 => (30, false)
            m if m >= 22 && m < 37 => (30, false),
            // 37-52 => (45, false)
            m if m >= 37 && m < 52 => (45, false),
            // 52-59 => (0, true)
            m if m >= 52 && m <= 59 => (0, true),
            _ => unreachable!(),
        };

        let hour = naive.hour();
        let new = NaiveTime::from_hms(if incr_hour { hour + 1 } else { hour }, new_min, 0);
        let offset = self.inner.offset();
        let date = self.inner.date();

        Self {
            inner: DateTime::from_utc(NaiveDateTime::new(date.naive_local(), new), *offset),
        }
    }

    pub fn hour(&self) -> u32 {
        self.inner.hour()
    }

    pub fn minute(&self) -> u32 {
        self.inner.minute()
    }

    pub fn second(&self) -> u32 {
        self.inner.second()
    }
}

/// Build a DateTime with the current local fixed offset
fn build_datetime(nt: NaiveTime) -> DateTime<Offset> {
    let date = Utc::now().date().naive_local();
    let offset = Local.offset_from_utc_date(&date);

    DateTime::from_utc(NaiveDateTime::new(date, nt), offset)
}

#[test]
fn simple() {
    let t = Time::fixed(10, 44, 0);
    let round = t.round();
    assert_eq!(round.minute(), 45);

    let t = Time::fixed(6, 8, 0);
    let round = t.round();
    assert_eq!(round.minute(), 15);

    let t = Time::fixed(6, 55, 0);
    let round = t.round();
    assert_eq!(round.minute(), 0);
    assert_eq!(round.hour(), 7);
}