Skip to main content

bity_ic_canister_time/
lib.rs

1//! Module for time-related operations in Internet Computer canisters.
2//!
3//! This module provides utilities for working with time in canisters, including
4//! timestamp functions, time constants, and timer scheduling functions. It supports
5//! both WASM and non-WASM environments.
6//!
7//! # Example
8//! ```
9//! use icrc7_nft::libraries::canister_time::{timestamp_millis, run_interval};
10//! use std::time::Duration;
11//!
12//! let current_time = timestamp_millis();
13//! run_interval(Duration::from_secs(60), || {
14//!     println!("Running periodic task");
15//! });
16//! ```
17
18use ic_cdk_timers::TimerId;
19use std::time::Duration;
20
21use bity_ic_types::{Milliseconds, Second, TimestampMillis, TimestampNanos};
22use time::{OffsetDateTime, Time, Weekday};
23
24/// Number of milliseconds in one second
25pub const SECOND_IN_MS: Milliseconds = 1000;
26/// Number of milliseconds in one minute
27pub const MINUTE_IN_MS: Milliseconds = SECOND_IN_MS * 60;
28/// Number of milliseconds in one hour
29pub const HOUR_IN_MS: Milliseconds = MINUTE_IN_MS * 60;
30/// Number of milliseconds in one day
31pub const DAY_IN_MS: Milliseconds = HOUR_IN_MS * 24;
32/// Number of milliseconds in one week
33pub const WEEK_IN_MS: Milliseconds = DAY_IN_MS * 7;
34
35/// Number of seconds in one day
36pub const DAY_IN_SECONDS: Second = 24 * 60 * 60;
37
38/// Number of nanoseconds in one millisecond
39pub const NANOS_PER_MILLISECOND: u64 = 1_000_000;
40
41/// Returns the current timestamp in seconds.
42///
43/// # Returns
44/// The current Unix timestamp in seconds
45pub fn timestamp_seconds() -> u64 {
46    timestamp_nanos() / 1_000_000_000
47}
48
49/// Returns the current timestamp in milliseconds.
50///
51/// # Returns
52/// The current Unix timestamp in milliseconds
53pub fn timestamp_millis() -> u64 {
54    timestamp_nanos() / 1_000_000
55}
56
57/// Returns the current timestamp in microseconds.
58///
59/// # Returns
60/// The current Unix timestamp in microseconds
61pub fn timestamp_micros() -> u64 {
62    timestamp_nanos() / 1_000
63}
64
65/// Returns the current timestamp in nanoseconds (WASM implementation).
66///
67/// This function is only available when targeting the WASM architecture.
68///
69/// # Returns
70/// The current timestamp in nanoseconds
71#[cfg(target_arch = "wasm32")]
72pub fn timestamp_nanos() -> u64 {
73    unsafe { ic0::time() as u64 }
74}
75
76/// Returns the current timestamp in nanoseconds (non-WASM implementation).
77///
78/// This function is only available when not targeting the WASM architecture.
79///
80/// # Returns
81/// The current Unix timestamp in nanoseconds
82#[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
92/// Returns the current time in milliseconds.
93///
94/// # Returns
95/// The current time in milliseconds since the Unix epoch
96pub fn now_millis() -> TimestampMillis {
97    now_nanos() / NANOS_PER_MILLISECOND
98}
99
100/// Returns the current time in nanoseconds (WASM implementation).
101///
102/// This function is only available when targeting the WASM architecture.
103///
104/// # Returns
105/// The current time in nanoseconds since the Unix epoch
106#[cfg(target_arch = "wasm32")]
107pub fn now_nanos() -> TimestampNanos {
108    ic_cdk::api::time()
109}
110
111/// Returns the current time in nanoseconds (non-WASM implementation).
112///
113/// This function is only available when not targeting the WASM architecture.
114///
115/// # Returns
116/// Always returns 0 in non-WASM environments
117#[cfg(not(target_arch = "wasm32"))]
118pub fn now_nanos() -> TimestampNanos {
119    0
120}
121
122/// Runs a function immediately and then at the specified interval.
123///
124/// # Arguments
125/// * `interval` - The duration between subsequent executions
126/// * `func` - The function to execute
127///
128/// # Returns
129/// The `TimerId` that can be used to cancel the timer
130pub 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
135/// Runs a function at the specified interval.
136///
137/// # Arguments
138/// * `interval` - The duration between executions
139/// * `func` - The function to execute
140pub fn run_interval(interval: Duration, func: fn()) {
141    ic_cdk_timers::set_timer_interval(interval, move || async move { func() });
142}
143
144/// Runs a function once immediately.
145///
146/// # Arguments
147/// * `func` - The function to execute
148pub 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        // Target hour has passed today, get tomorrow's date at target hour
186        now.saturating_add(time::Duration::days(1))
187            .replace_time(target_time)
188    } else {
189        // Target hour hasn't passed, get today's date at target hour
190        now.replace_time(target_time)
191    };
192
193    Some(next_occurrence.unix_timestamp() as u64 * 1000) // Convert to milliseconds
194}
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        // Mock current time: Sat Nov 23 2024 10:52:11 UTC
250        // 1732355531
251        // 1732362731
252        //       7128
253        let now = datetime!(2024-11-23 10:52:11 UTC);
254
255        let target_hour = 12; // Next target hour is 12 o'clock
256
257        // Expected delay: 15 hours from 20:52:11 -> 12:00:00 next day
258        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        // Verify the calculated delay matches the expected delay
266        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        // test 1
278        let mock_now = || {
279            let fixed_time = OffsetDateTime::parse(
280                "2025-02-28T16:00:00Z", // Friday 1 hour
281                &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        // Start job for next Friday at 3:00 PM
292        let res = calculate_next_weekday_timestamp(Weekday::Friday, 15, &mock_now).unwrap();
293
294        assert_eq!(res, 1741359600000); // 7 Mar 2025, 15:00:00
295
296        // test 2
297        let mock_now = || {
298            let fixed_time = OffsetDateTime::parse(
299                "2025-02-27T14:55:00Z", // Friday 1 hour
300                &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        // Start job for next Friday at 3:00 PM
311        let res = calculate_next_weekday_timestamp(Weekday::Friday, 15, &mock_now).unwrap();
312
313        assert_eq!(res, 1740754800000); // 28 Feb 2025, 15:00:00
314    }
315}