Skip to main content

yt/
time.rs

1use chrono::{DateTime, Local, NaiveDate, SecondsFormat, Utc};
2
3/// `2026-10-09 14:05` in the local time zone.
4pub fn local(millis: i64) -> String {
5    DateTime::<Utc>::from_timestamp_millis(millis)
6        .map(|time| {
7            time.with_timezone(&Local)
8                .format("%Y-%m-%d %H:%M")
9                .to_string()
10        })
11        .unwrap_or_default()
12}
13
14/// `2026-10-09T12:05:00Z`, for JSON output.
15pub fn utc(millis: i64) -> String {
16    DateTime::<Utc>::from_timestamp_millis(millis)
17        .map(|time| time.to_rfc3339_opts(SecondsFormat::Secs, true))
18        .unwrap_or_default()
19}
20
21/// `2026-10-09`; YouTrack stores date-only values as midnight UTC.
22pub fn date(millis: i64) -> String {
23    DateTime::<Utc>::from_timestamp_millis(millis)
24        .map(|time| time.format("%Y-%m-%d").to_string())
25        .unwrap_or_default()
26}
27
28/// Midnight UTC of a `YYYY-MM-DD` day in milliseconds, the form YouTrack stores dates in.
29pub fn day_millis(value: &str) -> Option<i64> {
30    NaiveDate::parse_from_str(value, "%Y-%m-%d")
31        .ok()
32        .and_then(|day| day.and_hms_opt(0, 0, 0))
33        .map(|midnight| midnight.and_utc().timestamp_millis())
34}
35
36/// Today in the local time zone as `YYYY-MM-DD`.
37pub fn today() -> String {
38    Local::now().format("%Y-%m-%d").to_string()
39}
40
41/// Minutes as hours and minutes: `12h 30m`, `45m`, `0m`.
42pub fn hours_minutes(minutes: i64) -> String {
43    match (minutes / 60, minutes % 60) {
44        (0, rest) => format!("{rest}m"),
45        (hours, 0) => format!("{hours}h"),
46        (hours, rest) => format!("{hours}h {rest}m"),
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn utc_and_dates() {
56        assert_eq!(utc(0), "1970-01-01T00:00:00Z");
57        assert_eq!(utc(1_791_590_400_000), "2026-10-10T00:00:00Z");
58        assert_eq!(date(1_791_590_400_000), "2026-10-10");
59    }
60
61    #[test]
62    fn days_are_midnight_utc_and_print_back_unchanged() {
63        assert_eq!(day_millis("2026-10-10"), Some(1_791_590_400_000));
64        for day in ["2026-01-01", "2026-03-29", "2026-10-25", "2024-02-29"] {
65            assert_eq!(date(day_millis(day).unwrap()), day);
66        }
67        assert_eq!(day_millis("2026-02-30"), None);
68        assert_eq!(day_millis("10.10.2026"), None);
69        assert_eq!(day_millis(""), None);
70    }
71
72    #[test]
73    fn today_is_a_local_day() {
74        assert!(day_millis(&today()).is_some());
75    }
76
77    #[test]
78    fn minutes_read_as_hours_and_minutes() {
79        assert_eq!(hours_minutes(0), "0m");
80        assert_eq!(hours_minutes(45), "45m");
81        assert_eq!(hours_minutes(120), "2h");
82        assert_eq!(hours_minutes(750), "12h 30m");
83    }
84
85    #[test]
86    fn local_has_minutes() {
87        assert_eq!(local(1_791_590_400_000).len(), "2026-10-10 00:00".len());
88    }
89}