nautilus_core/
datetime.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Common data and time functions.
17
18use chrono::{DateTime, Datelike, NaiveDate, SecondsFormat, TimeDelta, Utc, Weekday};
19
20use crate::UnixNanos;
21
22/// Number of milliseconds in one second.
23pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
24
25/// Number of nanoseconds in one second.
26pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
27
28/// Number of nanoseconds in one millisecond.
29pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
30
31/// Number of nanoseconds in one microsecond.
32pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
33
34/// List of weekdays (Monday to Friday).
35pub const WEEKDAYS: [Weekday; 5] = [
36    Weekday::Mon,
37    Weekday::Tue,
38    Weekday::Wed,
39    Weekday::Thu,
40    Weekday::Fri,
41];
42
43/// Converts seconds to nanoseconds (ns).
44#[unsafe(no_mangle)]
45pub extern "C" fn secs_to_nanos(secs: f64) -> u64 {
46    (secs * NANOSECONDS_IN_SECOND as f64) as u64
47}
48
49/// Converts seconds to milliseconds (ms).
50#[unsafe(no_mangle)]
51pub extern "C" fn secs_to_millis(secs: f64) -> u64 {
52    (secs * MILLISECONDS_IN_SECOND as f64) as u64
53}
54
55/// Converts milliseconds (ms) to nanoseconds (ns).
56#[unsafe(no_mangle)]
57pub extern "C" fn millis_to_nanos(millis: f64) -> u64 {
58    (millis * NANOSECONDS_IN_MILLISECOND as f64) as u64
59}
60
61/// Converts microseconds (μs) to nanoseconds (ns).
62#[unsafe(no_mangle)]
63pub extern "C" fn micros_to_nanos(micros: f64) -> u64 {
64    (micros * NANOSECONDS_IN_MICROSECOND as f64) as u64
65}
66
67/// Converts nanoseconds (ns) to seconds.
68#[unsafe(no_mangle)]
69pub extern "C" fn nanos_to_secs(nanos: u64) -> f64 {
70    nanos as f64 / NANOSECONDS_IN_SECOND as f64
71}
72
73/// Converts nanoseconds (ns) to milliseconds (ms).
74#[unsafe(no_mangle)]
75pub const extern "C" fn nanos_to_millis(nanos: u64) -> u64 {
76    nanos / NANOSECONDS_IN_MILLISECOND
77}
78
79/// Converts nanoseconds (ns) to microseconds (μs).
80#[unsafe(no_mangle)]
81pub const extern "C" fn nanos_to_micros(nanos: u64) -> u64 {
82    nanos / NANOSECONDS_IN_MICROSECOND
83}
84
85/// Converts a UNIX nanoseconds timestamp to an ISO 8601 (RFC 3339) format string.
86#[inline]
87#[must_use]
88pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
89    let datetime = unix_nanos.to_datetime_utc();
90    datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
91}
92
93/// Converts a UNIX nanoseconds timestamp to an ISO 8601 (RFC 3339) format string
94/// with millisecond precision.
95#[inline]
96#[must_use]
97pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
98    let datetime = unix_nanos.to_datetime_utc();
99    datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
100}
101
102/// Floor the given UNIX nanoseconds to the nearest microsecond.
103#[must_use]
104pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
105    (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
106}
107
108/// Calculates the last weekday (Mon-Fri) from the given `year`, `month` and `day`.
109///
110/// # Errors
111///
112/// Returns an error if the date is invalid.
113pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
114    let date =
115        NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
116    let current_weekday = date.weekday().number_from_monday();
117
118    // Calculate the offset in days for closest weekday (Mon-Fri)
119    let offset = i64::from(match current_weekday {
120        1..=5 => 0, // Monday to Friday, no adjustment needed
121        6 => 1,     // Saturday, adjust to previous Friday
122        _ => 2,     // Sunday, adjust to previous Friday
123    });
124    // Calculate last closest weekday
125    let last_closest = date - TimeDelta::days(offset);
126
127    // Convert to UNIX nanoseconds
128    let unix_timestamp_ns = last_closest
129        .and_hms_nano_opt(0, 0, 0, 0)
130        .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
131
132    Ok(UnixNanos::from(
133        unix_timestamp_ns
134            .and_utc()
135            .timestamp_nanos_opt()
136            .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))? as u64,
137    ))
138}
139
140/// Check whether the given UNIX nanoseconds timestamp is within the last 24 hours.
141///
142/// # Errors
143///
144/// Returns an error if the timestamp is invalid.
145pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
146    let timestamp_ns = timestamp_ns.as_u64();
147    let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
148    let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
149    let timestamp = DateTime::from_timestamp(seconds as i64, nanoseconds)
150        .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
151    let now = Utc::now();
152
153    Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
154}
155
156/// Subtract `n` months from a chrono `DateTime<Utc>`.
157#[must_use]
158pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> DateTime<Utc> {
159    datetime
160        .checked_sub_months(chrono::Months::new(n))
161        .expect("Failed to subtract months")
162}
163
164/// Add `n` months to a chrono `DateTime<Utc>`.
165#[must_use]
166pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> DateTime<Utc> {
167    datetime
168        .checked_add_months(chrono::Months::new(n))
169        .expect("Failed to add months")
170}
171
172/// Subtract `n` months from a given UNIX nanoseconds timestamp.
173#[must_use]
174pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> UnixNanos {
175    let datetime = unix_nanos.to_datetime_utc();
176    (subtract_n_months(datetime, n)
177        .timestamp_nanos_opt()
178        .expect("Months should be within 584 years") as u64)
179        .into()
180}
181
182/// Add `n` months to a given UNIX nanoseconds timestamp.
183#[must_use]
184pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> UnixNanos {
185    let date = unix_nanos.to_datetime_utc();
186    (add_n_months(date, n)
187        .timestamp_nanos_opt()
188        .expect("Months should be within 584 years") as u64)
189        .into()
190}
191
192/// Returns the last valid day of `(year, month)`.
193#[must_use]
194pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
195    // E.g., for February, check leap year logic
196    match month {
197        1 => 31,
198        2 => {
199            if is_leap_year(year) {
200                29
201            } else {
202                28
203            }
204        }
205        3 => 31,
206        4 => 30,
207        5 => 31,
208        6 => 30,
209        7 => 31,
210        8 => 31,
211        9 => 30,
212        10 => 31,
213        11 => 30,
214        12 => 31,
215        _ => 31, // fallback
216    }
217}
218
219/// Basic leap-year check
220#[must_use]
221pub const fn is_leap_year(year: i32) -> bool {
222    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
223}
224
225////////////////////////////////////////////////////////////////////////////////
226// Tests
227////////////////////////////////////////////////////////////////////////////////
228#[cfg(test)]
229mod tests {
230    use chrono::{DateTime, TimeDelta, TimeZone, Utc};
231    use rstest::rstest;
232
233    use super::*;
234
235    #[rstest]
236    #[case(0.0, 0)]
237    #[case(1.0, 1_000_000_000)]
238    #[case(1.1, 1_100_000_000)]
239    #[case(42.0, 42_000_000_000)]
240    #[case(0.000_123_5, 123_500)]
241    #[case(0.000_000_01, 10)]
242    #[case(0.000_000_001, 1)]
243    #[case(9.999_999_999, 9_999_999_999)]
244    fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
245        let result = secs_to_nanos(value);
246        assert_eq!(result, expected);
247    }
248
249    #[rstest]
250    #[case(0.0, 0)]
251    #[case(1.0, 1_000)]
252    #[case(1.1, 1_100)]
253    #[case(42.0, 42_000)]
254    #[case(0.012_34, 12)]
255    #[case(0.001, 1)]
256    fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
257        let result = secs_to_millis(value);
258        assert_eq!(result, expected);
259    }
260
261    #[rstest]
262    #[case(0.0, 0)]
263    #[case(1.0, 1_000_000)]
264    #[case(1.1, 1_100_000)]
265    #[case(42.0, 42_000_000)]
266    #[case(0.000_123_4, 123)]
267    #[case(0.000_01, 10)]
268    #[case(0.000_001, 1)]
269    #[case(9.999_999, 9_999_999)]
270    fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
271        let result = millis_to_nanos(value);
272        assert_eq!(result, expected);
273    }
274
275    #[rstest]
276    #[case(0.0, 0)]
277    #[case(1.0, 1_000)]
278    #[case(1.1, 1_100)]
279    #[case(42.0, 42_000)]
280    #[case(0.1234, 123)]
281    #[case(0.01, 10)]
282    #[case(0.001, 1)]
283    #[case(9.999, 9_999)]
284    fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
285        let result = micros_to_nanos(value);
286        assert_eq!(result, expected);
287    }
288
289    #[rstest]
290    #[case(0, 0.0)]
291    #[case(1, 1e-09)]
292    #[case(1_000_000_000, 1.0)]
293    #[case(42_897_123_111, 42.897_123_111)]
294    fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
295        let result = nanos_to_secs(value);
296        assert_eq!(result, expected);
297    }
298
299    #[rstest]
300    #[case(0, 0)]
301    #[case(1_000_000, 1)]
302    #[case(1_000_000_000, 1000)]
303    #[case(42_897_123_111, 42897)]
304    fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
305        let result = nanos_to_millis(value);
306        assert_eq!(result, expected);
307    }
308
309    #[rstest]
310    #[case(0, 0)]
311    #[case(1_000, 1)]
312    #[case(1_000_000_000, 1_000_000)]
313    #[case(42_897_123, 42_897)]
314    fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
315        let result = nanos_to_micros(value);
316        assert_eq!(result, expected);
317    }
318
319    #[rstest]
320    #[case(0, "1970-01-01T00:00:00.000000000Z")] // Unix epoch
321    #[case(1, "1970-01-01T00:00:00.000000001Z")] // 1 nanosecond
322    #[case(1_000, "1970-01-01T00:00:00.000001000Z")] // 1 microsecond
323    #[case(1_000_000, "1970-01-01T00:00:00.001000000Z")] // 1 millisecond
324    #[case(1_000_000_000, "1970-01-01T00:00:01.000000000Z")] // 1 second
325    #[case(1_702_857_600_000_000_000, "2023-12-18T00:00:00.000000000Z")] // Specific date
326    fn test_unix_nanos_to_iso8601(#[case] nanos: u64, #[case] expected: &str) {
327        let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
328        assert_eq!(result, expected);
329    }
330
331    #[rstest]
332    #[case(0, "1970-01-01T00:00:00.000Z")] // Unix epoch
333    #[case(1_000_000, "1970-01-01T00:00:00.001Z")] // 1 millisecond
334    #[case(1_000_000_000, "1970-01-01T00:00:01.000Z")] // 1 second
335    #[case(1_702_857_600_123_456_789, "2023-12-18T00:00:00.123Z")] // With millisecond precision
336    fn test_unix_nanos_to_iso8601_millis(#[case] nanos: u64, #[case] expected: &str) {
337        let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
338        assert_eq!(result, expected);
339    }
340
341    #[rstest]
342    #[case(2023, 12, 15, 1_702_598_400_000_000_000)] // Fri
343    #[case(2023, 12, 16, 1_702_598_400_000_000_000)] // Sat
344    #[case(2023, 12, 17, 1_702_598_400_000_000_000)] // Sun
345    #[case(2023, 12, 18, 1_702_857_600_000_000_000)] // Mon
346    fn test_last_closest_weekday_nanos_with_valid_date(
347        #[case] year: i32,
348        #[case] month: u32,
349        #[case] day: u32,
350        #[case] expected: u64,
351    ) {
352        let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
353        assert_eq!(result, expected);
354    }
355
356    #[rstest]
357    fn test_last_closest_weekday_nanos_with_invalid_date() {
358        let result = last_weekday_nanos(2023, 4, 31);
359        assert!(result.is_err());
360    }
361
362    #[rstest]
363    fn test_last_closest_weekday_nanos_with_nonexistent_date() {
364        let result = last_weekday_nanos(2023, 2, 30);
365        assert!(result.is_err());
366    }
367
368    #[rstest]
369    fn test_last_closest_weekday_nanos_with_invalid_conversion() {
370        let result = last_weekday_nanos(9999, 12, 31);
371        assert!(result.is_err());
372    }
373
374    #[rstest]
375    fn test_is_within_last_24_hours_when_now() {
376        let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
377        assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
378    }
379
380    #[rstest]
381    fn test_is_within_last_24_hours_when_two_days_ago() {
382        let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
383            .timestamp_nanos_opt()
384            .unwrap();
385        assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
386    }
387
388    #[rstest]
389    #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] // Leap year February
390    #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 12, Utc.with_ymd_and_hms(2023, 3, 31, 12, 0, 0).unwrap())] // One year earlier
391    #[case(Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2023, 12, 31, 12, 0, 0).unwrap())] // Wrapping to previous year
392    #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 2, Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap())] // Multiple months back
393    fn test_subtract_n_months(
394        #[case] input: DateTime<Utc>,
395        #[case] months: u32,
396        #[case] expected: DateTime<Utc>,
397    ) {
398        let result = subtract_n_months(input, months);
399        assert_eq!(result, expected);
400    }
401
402    #[rstest]
403    #[case(Utc.with_ymd_and_hms(2023, 2, 28, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2023, 3, 28, 12, 0, 0).unwrap())] // Simple month addition
404    #[case(Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] // Leap year February
405    #[case(Utc.with_ymd_and_hms(2023, 12, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap())] // Wrapping to next year
406    #[case(Utc.with_ymd_and_hms(2023, 1, 31, 12, 0, 0).unwrap(), 13, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] // Crossing year boundary with multiple months
407    fn test_add_n_months(
408        #[case] input: DateTime<Utc>,
409        #[case] months: u32,
410        #[case] expected: DateTime<Utc>,
411    ) {
412        let result = add_n_months(input, months);
413        assert_eq!(result, expected);
414    }
415
416    #[rstest]
417    #[case(2024, 2, 29)] // Leap year February
418    #[case(2023, 2, 28)] // Non-leap year February
419    #[case(2024, 12, 31)] // December
420    #[case(2023, 11, 30)] // November
421    fn test_last_day_of_month(#[case] year: i32, #[case] month: u32, #[case] expected: u32) {
422        let result = last_day_of_month(year, month);
423        assert_eq!(result, expected);
424    }
425
426    #[rstest]
427    #[case(2024, true)] // Leap year divisible by 4
428    #[case(1900, false)] // Not leap year, divisible by 100 but not 400
429    #[case(2000, true)] // Leap year, divisible by 400
430    #[case(2023, false)] // Non-leap year
431    fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
432        let result = is_leap_year(year);
433        assert_eq!(result, expected);
434    }
435}