1use std::time::{Duration, UNIX_EPOCH};
19
20use chrono::{DateTime, Datelike, NaiveDate, SecondsFormat, TimeDelta, Timelike, Utc, Weekday};
21
22use crate::UnixNanos;
23
24pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
26
27pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
29
30pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
32
33pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
35
36pub const WEEKDAYS: [Weekday; 5] = [
38 Weekday::Mon,
39 Weekday::Tue,
40 Weekday::Wed,
41 Weekday::Thu,
42 Weekday::Fri,
43];
44
45#[inline]
47#[no_mangle]
48pub extern "C" fn secs_to_nanos(secs: f64) -> u64 {
49 (secs * NANOSECONDS_IN_SECOND as f64) as u64
50}
51
52#[inline]
54#[no_mangle]
55pub extern "C" fn secs_to_millis(secs: f64) -> u64 {
56 (secs * MILLISECONDS_IN_SECOND as f64) as u64
57}
58
59#[inline]
61#[no_mangle]
62pub extern "C" fn millis_to_nanos(millis: f64) -> u64 {
63 (millis * NANOSECONDS_IN_MILLISECOND as f64) as u64
64}
65
66#[inline]
68#[no_mangle]
69pub extern "C" fn micros_to_nanos(micros: f64) -> u64 {
70 (micros * NANOSECONDS_IN_MICROSECOND as f64) as u64
71}
72
73#[inline]
75#[no_mangle]
76pub extern "C" fn nanos_to_secs(nanos: u64) -> f64 {
77 nanos as f64 / NANOSECONDS_IN_SECOND as f64
78}
79
80#[inline]
82#[no_mangle]
83pub const extern "C" fn nanos_to_millis(nanos: u64) -> u64 {
84 nanos / NANOSECONDS_IN_MILLISECOND
85}
86
87#[inline]
89#[no_mangle]
90pub const extern "C" fn nanos_to_micros(nanos: u64) -> u64 {
91 nanos / NANOSECONDS_IN_MICROSECOND
92}
93
94#[inline]
96#[must_use]
97pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
98 let dt = DateTime::<Utc>::from(UNIX_EPOCH + Duration::from_nanos(unix_nanos.as_u64()));
99 dt.to_rfc3339_opts(SecondsFormat::Nanos, true)
100}
101
102#[inline]
105#[must_use]
106pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
107 let dt = DateTime::<Utc>::from(UNIX_EPOCH + Duration::from_nanos(unix_nanos.as_u64()));
108 dt.to_rfc3339_opts(SecondsFormat::Millis, true)
109}
110
111#[must_use]
113pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
114 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
115}
116
117pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
123 let date =
124 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
125 let current_weekday = date.weekday().number_from_monday();
126
127 let offset = i64::from(match current_weekday {
129 1..=5 => 0, 6 => 1, _ => 2, });
133 let last_closest = date - TimeDelta::days(offset);
135
136 let unix_timestamp_ns = last_closest
138 .and_hms_nano_opt(0, 0, 0, 0)
139 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
140
141 Ok(UnixNanos::from(
142 unix_timestamp_ns
143 .and_utc()
144 .timestamp_nanos_opt()
145 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))? as u64,
146 ))
147}
148
149pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
155 let timestamp_ns = timestamp_ns.as_u64();
156 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
157 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
158 let timestamp = DateTime::from_timestamp(seconds as i64, nanoseconds)
159 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
160 let now = Utc::now();
161
162 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
163}
164
165#[must_use]
167pub fn subtract_n_months(dt: DateTime<Utc>, n: isize) -> Option<DateTime<Utc>> {
168 let year = dt.year();
175 let month = dt.month() as isize; let day = dt.day(); let mut new_month = month - n;
179 let mut new_year = year;
180
181 while new_month <= 0 {
183 new_month += 12;
184 new_year -= 1;
185 }
186 let last_day_of_new_month = last_day_of_month(new_year, new_month as u32);
188 let new_day = day.min(last_day_of_new_month);
189
190 let new_date = chrono::NaiveDate::from_ymd_opt(new_year, new_month as u32, new_day)?;
192 let new_naive_datetime =
193 new_date.and_hms_micro_opt(dt.hour(), dt.minute(), dt.second(), dt.nanosecond() / 1000)?;
194
195 let new_dt = DateTime::<Utc>::from_naive_utc_and_offset(new_naive_datetime, chrono::Utc);
197 Some(new_dt)
198}
199
200#[must_use]
202pub fn add_n_months(dt: DateTime<Utc>, n: isize) -> Option<DateTime<Utc>> {
203 let year = dt.year();
205 let month = dt.month() as isize;
206 let day = dt.day();
207
208 let mut new_month = month + n;
209 let mut new_year = year;
210
211 while new_month > 12 {
213 new_month -= 12;
214 new_year += 1;
215 }
216 let last_day_of_new_month = last_day_of_month(new_year, new_month as u32);
217 let new_day = day.min(last_day_of_new_month);
218
219 let new_date = chrono::NaiveDate::from_ymd_opt(new_year, new_month as u32, new_day)?;
220 let new_naive_datetime =
221 new_date.and_hms_micro_opt(dt.hour(), dt.minute(), dt.second(), dt.nanosecond() / 1000)?;
222
223 Some(DateTime::<Utc>::from_naive_utc_and_offset(
224 new_naive_datetime,
225 chrono::Utc,
226 ))
227}
228
229#[must_use]
231pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
232 match month {
234 1 => 31,
235 2 => {
236 if is_leap_year(year) {
237 29
238 } else {
239 28
240 }
241 }
242 3 => 31,
243 4 => 30,
244 5 => 31,
245 6 => 30,
246 7 => 31,
247 8 => 31,
248 9 => 30,
249 10 => 31,
250 11 => 30,
251 12 => 31,
252 _ => 31, }
254}
255
256#[must_use]
258pub const fn is_leap_year(year: i32) -> bool {
259 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
260}
261
262#[cfg(test)]
266mod tests {
267 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
268 use rstest::rstest;
269
270 use super::*;
271
272 #[rstest]
273 #[case(0.0, 0)]
274 #[case(1.0, 1_000_000_000)]
275 #[case(1.1, 1_100_000_000)]
276 #[case(42.0, 42_000_000_000)]
277 #[case(0.000_123_5, 123_500)]
278 #[case(0.000_000_01, 10)]
279 #[case(0.000_000_001, 1)]
280 #[case(9.999_999_999, 9_999_999_999)]
281 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
282 let result = secs_to_nanos(value);
283 assert_eq!(result, expected);
284 }
285
286 #[rstest]
287 #[case(0.0, 0)]
288 #[case(1.0, 1_000)]
289 #[case(1.1, 1_100)]
290 #[case(42.0, 42_000)]
291 #[case(0.012_34, 12)]
292 #[case(0.001, 1)]
293 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
294 let result = secs_to_millis(value);
295 assert_eq!(result, expected);
296 }
297
298 #[rstest]
299 #[case(0.0, 0)]
300 #[case(1.0, 1_000_000)]
301 #[case(1.1, 1_100_000)]
302 #[case(42.0, 42_000_000)]
303 #[case(0.000_123_4, 123)]
304 #[case(0.000_01, 10)]
305 #[case(0.000_001, 1)]
306 #[case(9.999_999, 9_999_999)]
307 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
308 let result = millis_to_nanos(value);
309 assert_eq!(result, expected);
310 }
311
312 #[rstest]
313 #[case(0.0, 0)]
314 #[case(1.0, 1_000)]
315 #[case(1.1, 1_100)]
316 #[case(42.0, 42_000)]
317 #[case(0.1234, 123)]
318 #[case(0.01, 10)]
319 #[case(0.001, 1)]
320 #[case(9.999, 9_999)]
321 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
322 let result = micros_to_nanos(value);
323 assert_eq!(result, expected);
324 }
325
326 #[rstest]
327 #[case(0, 0.0)]
328 #[case(1, 1e-09)]
329 #[case(1_000_000_000, 1.0)]
330 #[case(42_897_123_111, 42.897_123_111)]
331 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
332 let result = nanos_to_secs(value);
333 assert_eq!(result, expected);
334 }
335
336 #[rstest]
337 #[case(0, 0)]
338 #[case(1_000_000, 1)]
339 #[case(1_000_000_000, 1000)]
340 #[case(42_897_123_111, 42897)]
341 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
342 let result = nanos_to_millis(value);
343 assert_eq!(result, expected);
344 }
345
346 #[rstest]
347 #[case(0, 0)]
348 #[case(1_000, 1)]
349 #[case(1_000_000_000, 1_000_000)]
350 #[case(42_897_123, 42_897)]
351 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
352 let result = nanos_to_micros(value);
353 assert_eq!(result, expected);
354 }
355
356 #[rstest]
357 #[case(0, "1970-01-01T00:00:00.000000000Z")] #[case(1, "1970-01-01T00:00:00.000000001Z")] #[case(1_000, "1970-01-01T00:00:00.000001000Z")] #[case(1_000_000, "1970-01-01T00:00:00.001000000Z")] #[case(1_000_000_000, "1970-01-01T00:00:01.000000000Z")] #[case(1_702_857_600_000_000_000, "2023-12-18T00:00:00.000000000Z")] fn test_unix_nanos_to_iso8601(#[case] nanos: u64, #[case] expected: &str) {
364 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
365 assert_eq!(result, expected);
366 }
367
368 #[rstest]
369 #[case(0, "1970-01-01T00:00:00.000Z")] #[case(1_000_000, "1970-01-01T00:00:00.001Z")] #[case(1_000_000_000, "1970-01-01T00:00:01.000Z")] #[case(1_702_857_600_123_456_789, "2023-12-18T00:00:00.123Z")] fn test_unix_nanos_to_iso8601_millis(#[case] nanos: u64, #[case] expected: &str) {
374 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
375 assert_eq!(result, expected);
376 }
377
378 #[rstest]
379 #[case(2023, 12, 15, 1_702_598_400_000_000_000)] #[case(2023, 12, 16, 1_702_598_400_000_000_000)] #[case(2023, 12, 17, 1_702_598_400_000_000_000)] #[case(2023, 12, 18, 1_702_857_600_000_000_000)] fn test_last_closest_weekday_nanos_with_valid_date(
384 #[case] year: i32,
385 #[case] month: u32,
386 #[case] day: u32,
387 #[case] expected: u64,
388 ) {
389 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
390 assert_eq!(result, expected);
391 }
392
393 #[rstest]
394 fn test_last_closest_weekday_nanos_with_invalid_date() {
395 let result = last_weekday_nanos(2023, 4, 31);
396 assert!(result.is_err());
397 }
398
399 #[rstest]
400 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
401 let result = last_weekday_nanos(2023, 2, 30);
402 assert!(result.is_err());
403 }
404
405 #[rstest]
406 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
407 let result = last_weekday_nanos(9999, 12, 31);
408 assert!(result.is_err());
409 }
410
411 #[rstest]
412 fn test_is_within_last_24_hours_when_now() {
413 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
414 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
415 }
416
417 #[rstest]
418 fn test_is_within_last_24_hours_when_two_days_ago() {
419 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
420 .timestamp_nanos_opt()
421 .unwrap();
422 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
423 }
424
425 #[rstest]
426 #[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())] #[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())] #[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())] #[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())] fn test_subtract_n_months(
431 #[case] input: DateTime<Utc>,
432 #[case] months: isize,
433 #[case] expected: DateTime<Utc>,
434 ) {
435 let result = subtract_n_months(input, months);
436 assert_eq!(result, Some(expected));
437 }
438
439 #[rstest]
440 #[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())] #[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())] #[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())] #[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())] fn test_add_n_months(
445 #[case] input: DateTime<Utc>,
446 #[case] months: isize,
447 #[case] expected: DateTime<Utc>,
448 ) {
449 let result = add_n_months(input, months);
450 assert_eq!(result, Some(expected));
451 }
452
453 #[rstest]
454 #[case(2024, 2, 29)] #[case(2023, 2, 28)] #[case(2024, 12, 31)] #[case(2023, 11, 30)] fn test_last_day_of_month(#[case] year: i32, #[case] month: u32, #[case] expected: u32) {
459 let result = last_day_of_month(year, month);
460 assert_eq!(result, expected);
461 }
462
463 #[rstest]
464 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
469 let result = is_leap_year(year);
470 assert_eq!(result, expected);
471 }
472}