codex_helper_core/
usage_day.rs1use 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; 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; 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}