Skip to main content

codex_helper_core/
usage_day.rs

1use chrono::{DateTime, Duration, FixedOffset, Local, NaiveDate, TimeZone, Timelike, Utc};
2
3const UTC_DAY_MS: u64 = 86_400_000;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct UsageDayWindow {
7    pub day: i32,
8    pub start_ms: u64,
9    pub end_ms: u64,
10}
11
12pub fn current_local_day() -> i32 {
13    local_day_from_ms(now_ms())
14}
15
16pub fn local_day_from_ms(timestamp_ms: u64) -> i32 {
17    local_datetime_from_ms(timestamp_ms)
18        .and_then(|dt| day_from_date(dt.date_naive()))
19        .unwrap_or_else(|| utc_day_from_ms(timestamp_ms))
20}
21
22pub fn local_hour_from_ms(timestamp_ms: u64) -> u8 {
23    local_datetime_from_ms(timestamp_ms)
24        .map(|dt| dt.hour() as u8)
25        .unwrap_or_else(|| ((timestamp_ms / 3_600_000) % 24) as u8)
26}
27
28pub fn local_day_window(day: i32) -> Option<UsageDayWindow> {
29    let date = date_from_day(day)?;
30    let next_date = date.checked_add_signed(Duration::days(1))?;
31    let start = Local
32        .from_local_datetime(&date.and_hms_opt(0, 0, 0)?)
33        .earliest()?
34        .timestamp_millis();
35    let end = Local
36        .from_local_datetime(&next_date.and_hms_opt(0, 0, 0)?)
37        .earliest()?
38        .timestamp_millis();
39    Some(UsageDayWindow {
40        day,
41        start_ms: u64::try_from(start).ok()?,
42        end_ms: u64::try_from(end).ok()?,
43    })
44}
45
46pub fn fixed_offset_day_from_ms(timestamp_ms: u64, offset_seconds: i32) -> Option<i32> {
47    let offset = FixedOffset::east_opt(offset_seconds)?;
48    let utc = utc_datetime_from_ms(timestamp_ms)?;
49    day_from_date(utc.with_timezone(&offset).date_naive())
50}
51
52pub fn fixed_offset_hour_from_ms(timestamp_ms: u64, offset_seconds: i32) -> Option<u8> {
53    let offset = FixedOffset::east_opt(offset_seconds)?;
54    let utc = utc_datetime_from_ms(timestamp_ms)?;
55    Some(utc.with_timezone(&offset).hour() as u8)
56}
57
58pub fn fixed_offset_day_window(day: i32, offset_seconds: i32) -> Option<UsageDayWindow> {
59    let offset = FixedOffset::east_opt(offset_seconds)?;
60    let date = date_from_day(day)?;
61    let next_date = date.checked_add_signed(Duration::days(1))?;
62    let start = offset
63        .from_local_datetime(&date.and_hms_opt(0, 0, 0)?)
64        .single()?
65        .with_timezone(&Utc)
66        .timestamp_millis();
67    let end = offset
68        .from_local_datetime(&next_date.and_hms_opt(0, 0, 0)?)
69        .single()?
70        .with_timezone(&Utc)
71        .timestamp_millis();
72    Some(UsageDayWindow {
73        day,
74        start_ms: u64::try_from(start).ok()?,
75        end_ms: u64::try_from(end).ok()?,
76    })
77}
78
79pub fn format_day(day: i32) -> String {
80    date_from_day(day)
81        .map(|date| date.format("%Y-%m-%d").to_string())
82        .unwrap_or_else(|| "-".to_string())
83}
84
85pub fn utc_day_from_ms(timestamp_ms: u64) -> i32 {
86    (timestamp_ms / UTC_DAY_MS) as i32
87}
88
89fn now_ms() -> u64 {
90    std::time::SystemTime::now()
91        .duration_since(std::time::UNIX_EPOCH)
92        .map(|d| d.as_millis() as u64)
93        .unwrap_or(0)
94}
95
96fn local_datetime_from_ms(timestamp_ms: u64) -> Option<DateTime<Local>> {
97    let timestamp_ms = i64::try_from(timestamp_ms).ok()?;
98    Local.timestamp_millis_opt(timestamp_ms).single()
99}
100
101fn utc_datetime_from_ms(timestamp_ms: u64) -> Option<DateTime<Utc>> {
102    let timestamp_ms = i64::try_from(timestamp_ms).ok()?;
103    Utc.timestamp_millis_opt(timestamp_ms).single()
104}
105
106pub fn day_from_date(date: NaiveDate) -> Option<i32> {
107    let days = date.signed_duration_since(epoch_date()).num_days();
108    i32::try_from(days).ok()
109}
110
111pub fn date_from_day(day: i32) -> Option<NaiveDate> {
112    epoch_date().checked_add_signed(Duration::days(i64::from(day)))
113}
114
115fn epoch_date() -> NaiveDate {
116    NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid Unix epoch date")
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn fixed_offset_day_uses_local_calendar_date() {
125        let timestamp_ms = 1_704_038_400_000; // 2023-12-31T16:00:00Z
126        let day = fixed_offset_day_from_ms(timestamp_ms, 8 * 60 * 60).expect("day");
127
128        assert_eq!(format_day(day), "2024-01-01");
129        assert_eq!(utc_day_from_ms(timestamp_ms), day - 1);
130    }
131
132    #[test]
133    fn fixed_offset_hour_uses_local_clock_hour() {
134        let timestamp_ms = 1_704_036_600_000; // 2023-12-31T15:30:00Z
135
136        assert_eq!(
137            fixed_offset_hour_from_ms(timestamp_ms, 8 * 60 * 60),
138            Some(23)
139        );
140    }
141
142    #[test]
143    fn fixed_offset_day_window_converts_midnight_to_utc_bounds() {
144        let day = day_from_date(NaiveDate::from_ymd_opt(2024, 1, 1).expect("date")).expect("day");
145        let window = fixed_offset_day_window(day, 8 * 60 * 60).expect("window");
146
147        assert_eq!(window.start_ms, 1_704_038_400_000);
148        assert_eq!(window.end_ms, 1_704_124_800_000);
149    }
150
151    #[test]
152    fn format_day_handles_epoch_and_before_epoch() {
153        assert_eq!(format_day(0), "1970-01-01");
154        assert_eq!(format_day(-1), "1969-12-31");
155    }
156}