bity_ic_canister_time/
lib.rs1use ic_cdk_timers::TimerId;
19use std::time::Duration;
20
21use bity_ic_types::{Milliseconds, Second, TimestampMillis, TimestampNanos};
22use time::{OffsetDateTime, Time, Weekday};
23
24pub const SECOND_IN_MS: Milliseconds = 1000;
26pub const MINUTE_IN_MS: Milliseconds = SECOND_IN_MS * 60;
28pub const HOUR_IN_MS: Milliseconds = MINUTE_IN_MS * 60;
30pub const DAY_IN_MS: Milliseconds = HOUR_IN_MS * 24;
32pub const WEEK_IN_MS: Milliseconds = DAY_IN_MS * 7;
34
35pub const DAY_IN_SECONDS: Second = 24 * 60 * 60;
37
38pub const NANOS_PER_MILLISECOND: u64 = 1_000_000;
40
41pub fn timestamp_seconds() -> u64 {
46 timestamp_nanos() / 1_000_000_000
47}
48
49pub fn timestamp_millis() -> u64 {
54 timestamp_nanos() / 1_000_000
55}
56
57pub fn timestamp_micros() -> u64 {
62 timestamp_nanos() / 1_000
63}
64
65#[cfg(target_arch = "wasm32")]
72pub fn timestamp_nanos() -> u64 {
73 unsafe { ic0::time() as u64 }
74}
75
76#[cfg(not(target_arch = "wasm32"))]
83pub fn timestamp_nanos() -> u64 {
84 use std::time::SystemTime;
85
86 SystemTime::now()
87 .duration_since(SystemTime::UNIX_EPOCH)
88 .unwrap()
89 .as_nanos() as u64
90}
91
92pub fn now_millis() -> TimestampMillis {
97 now_nanos() / NANOS_PER_MILLISECOND
98}
99
100#[cfg(target_arch = "wasm32")]
107pub fn now_nanos() -> TimestampNanos {
108 ic_cdk::api::time()
109}
110
111#[cfg(not(target_arch = "wasm32"))]
118pub fn now_nanos() -> TimestampNanos {
119 0
120}
121
122pub fn run_now_then_interval(interval: Duration, func: fn()) -> TimerId {
131 ic_cdk_timers::set_timer(Duration::ZERO, async move { func() });
132 ic_cdk_timers::set_timer_interval(interval, move || async move { func() })
133}
134
135pub fn run_interval(interval: Duration, func: fn()) {
141 ic_cdk_timers::set_timer_interval(interval, move || async move { func() });
142}
143
144pub fn run_once(func: fn()) {
149 ic_cdk_timers::set_timer(Duration::ZERO, async move { func() });
150}
151
152pub fn start_job_daily_at(hour: u8, func: fn()) {
153 if let Some(next_timestamp) = calculate_next_timestamp(hour) {
154 let now_millis = now_millis();
155
156 if next_timestamp > now_millis {
157 let delay = Duration::from_millis(next_timestamp - now_millis);
158
159 ic_cdk_timers::set_timer(delay, async move {
160 run_now_then_interval(Duration::from_millis(DAY_IN_MS), func);
161 });
162
163 tracing::info!(
164 "Job scheduled to start at the next {}:00. (Timestamp: {})",
165 hour,
166 next_timestamp
167 );
168 } else {
169 tracing::error!("Failed to calculate a valid timestamp for the next job.");
170 }
171 } else {
172 tracing::error!("Invalid hour provided for job scheduling: {}", hour);
173 }
174}
175
176fn calculate_next_timestamp(hour: u8) -> Option<u64> {
177 if hour > 23 {
178 return None;
179 }
180
181 let now = OffsetDateTime::from_unix_timestamp((now_millis() / 1000) as i64).ok()?;
182 let target_time = Time::from_hms(hour, 0, 0).ok()?;
183
184 let next_occurrence = if now.time().hour() >= hour {
185 now.saturating_add(time::Duration::days(1))
187 .replace_time(target_time)
188 } else {
189 now.replace_time(target_time)
191 };
192
193 Some(next_occurrence.unix_timestamp() as u64 * 1000) }
195
196fn calculate_next_weekday_timestamp(
197 weekday: Weekday,
198 hour: u8,
199 now_fn: impl Fn() -> u64,
200) -> Option<u64> {
201 if hour > 23 {
202 return None;
203 }
204
205 let now_millis = now_fn();
206 let now = OffsetDateTime::from_unix_timestamp((now_millis / 1000) as i64).ok()?;
207 let target_time = Time::from_hms(hour, 0, 0).ok()?;
208
209 let mut next_occurrence = now.replace_time(target_time);
210 while next_occurrence.weekday() != weekday || next_occurrence < now {
211 next_occurrence = next_occurrence.saturating_add(time::Duration::days(1));
212 }
213
214 Some(next_occurrence.unix_timestamp() as u64 * 1000)
215}
216
217pub fn start_job_weekly_at(weekday: Weekday, hour: u8, func: fn(), now_fn: &impl Fn() -> u64) {
218 if let Some(next_timestamp) = calculate_next_weekday_timestamp(weekday, hour, now_fn) {
219 let now_millis = now_fn();
220
221 if next_timestamp > now_millis {
222 let delay = Duration::from_millis(next_timestamp - now_millis);
223
224 ic_cdk_timers::set_timer(delay, async move {
225 run_now_then_interval(Duration::from_millis(DAY_IN_MS * 7), func);
226 });
227
228 tracing::info!(
229 "Job scheduled to start on {:?} at {}:00. (Timestamp: {})",
230 weekday,
231 hour,
232 next_timestamp
233 );
234 } else {
235 tracing::error!("Failed to calculate a valid timestamp for the next weekly job.");
236 }
237 } else {
238 tracing::error!("Invalid hour provided for weekly job scheduling: {}", hour);
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use time::macros::datetime;
246
247 #[test]
248 fn test_calculate_next_timestamp() {
249 let now = datetime!(2024-11-23 10:52:11 UTC);
254
255 let target_hour = 12; let expected_delay = 12 * 3600 * 1000;
259
260 let calculated_delay =
261 calculate_next_timestamp(target_hour).expect("Failed to calculate next timestamp");
262
263 println!("Calculated delay: {} milliseconds", calculated_delay);
264
265 assert_eq!(
267 calculated_delay, expected_delay,
268 "Expected delay: {}, Calculated delay: {}",
269 expected_delay, calculated_delay
270 );
271 }
272
273 #[test]
274 fn test_calculate_next_weekday_timestamp() {
275 use time::{OffsetDateTime, Weekday};
276
277 let mock_now = || {
279 let fixed_time = OffsetDateTime::parse(
280 "2025-02-28T16:00:00Z", &time::format_description::well_known::Rfc3339,
282 )
283 .unwrap();
284 fixed_time.unix_timestamp() as u64 * 1000
285 };
286
287 let mock_func = || {
288 tracing::info!("Weekly job executed!");
289 };
290
291 let res = calculate_next_weekday_timestamp(Weekday::Friday, 15, &mock_now).unwrap();
293
294 assert_eq!(res, 1741359600000); let mock_now = || {
298 let fixed_time = OffsetDateTime::parse(
299 "2025-02-27T14:55:00Z", &time::format_description::well_known::Rfc3339,
301 )
302 .unwrap();
303 fixed_time.unix_timestamp() as u64 * 1000
304 };
305
306 let mock_func = || {
307 tracing::info!("Weekly job executed!");
308 };
309
310 let res = calculate_next_weekday_timestamp(Weekday::Friday, 15, &mock_now).unwrap();
312
313 assert_eq!(res, 1740754800000); }
315}