aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/time.rs
blob: 4cda0718d922bba1343b6fec0317418ce7e72040 (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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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 ToString for Time {
    fn to_string(&self) -> String {
        format!("{}", self.inner.format("%Y-%m-%d %H:%M:%S%:z"))
    }
}

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()
                    .with_second(0)
                    .and_then(|t| t.with_nanosecond(0))
                    .unwrap(),
            ),
        }
    }

    /// Get the time that might be rounded to the next 15 minutes
    pub(crate) fn rounded(r: bool) -> Self {
        if r {
            Time::now().round()
        } else {
            Time::now()
        }
    }

    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 < 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);
}