1use std::convert::TryFrom;
18
19use chrono::{DateTime, Datelike, NaiveDate, SecondsFormat, TimeDelta, Utc, Weekday};
20
21use crate::UnixNanos;
22
23pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
25
26pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
28
29pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
31
32pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
34
35pub const WEEKDAYS: [Weekday; 5] = [
37 Weekday::Mon,
38 Weekday::Tue,
39 Weekday::Wed,
40 Weekday::Thu,
41 Weekday::Fri,
42];
43
44#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
49#[must_use]
50pub fn secs_to_nanos(secs: f64) -> u64 {
51 let nanos = secs * NANOSECONDS_IN_SECOND as f64;
52 nanos.max(0.0).trunc() as u64
53}
54
55#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
60#[must_use]
61pub fn secs_to_millis(secs: f64) -> u64 {
62 let millis = secs * MILLISECONDS_IN_SECOND as f64;
63 millis.max(0.0).trunc() as u64
64}
65
66#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
71#[must_use]
72pub fn millis_to_nanos(millis: f64) -> u64 {
73 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
74 nanos.max(0.0).trunc() as u64
75}
76
77#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
82#[must_use]
83pub fn micros_to_nanos(micros: f64) -> u64 {
84 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
85 nanos.max(0.0).trunc() as u64
86}
87
88#[allow(clippy::cast_precision_loss)]
93#[must_use]
94pub fn nanos_to_secs(nanos: u64) -> f64 {
95 let seconds = nanos / NANOSECONDS_IN_SECOND;
96 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
97 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
98}
99
100#[must_use]
102pub const fn nanos_to_millis(nanos: u64) -> u64 {
103 nanos / NANOSECONDS_IN_MILLISECOND
104}
105
106#[must_use]
108pub const fn nanos_to_micros(nanos: u64) -> u64 {
109 nanos / NANOSECONDS_IN_MICROSECOND
110}
111
112#[inline]
114#[must_use]
115pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
116 let datetime = unix_nanos.to_datetime_utc();
117 datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
118}
119
120#[inline]
163pub fn iso8601_to_unix_nanos(date_string: String) -> anyhow::Result<UnixNanos> {
164 date_string
165 .parse::<UnixNanos>()
166 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{date_string}': {e}"))
167}
168
169#[inline]
172#[must_use]
173pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
174 let datetime = unix_nanos.to_datetime_utc();
175 datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
176}
177
178#[must_use]
180pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
181 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
182}
183
184pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
190 let date =
191 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
192 let current_weekday = date.weekday().number_from_monday();
193
194 let offset = i64::from(match current_weekday {
196 1..=5 => 0, 6 => 1, _ => 2, });
200 let last_closest = date - TimeDelta::days(offset);
202
203 let unix_timestamp_ns = last_closest
205 .and_hms_nano_opt(0, 0, 0, 0)
206 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
207
208 let raw_ns = unix_timestamp_ns
210 .and_utc()
211 .timestamp_nanos_opt()
212 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))?;
213 let ns_u64 =
214 u64::try_from(raw_ns).map_err(|_| anyhow::anyhow!("Negative timestamp: {raw_ns}"))?;
215 Ok(UnixNanos::from(ns_u64))
216}
217
218pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
224 let timestamp_ns = timestamp_ns.as_u64();
225 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
226 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
227 let secs_i64 = i64::try_from(seconds)
229 .map_err(|_| anyhow::anyhow!("Timestamp seconds overflow: {seconds}"))?;
230 let timestamp = DateTime::from_timestamp(secs_i64, nanoseconds)
231 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
232 let now = Utc::now();
233
234 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
235}
236
237pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
243 match datetime.checked_sub_months(chrono::Months::new(n)) {
244 Some(result) => Ok(result),
245 None => anyhow::bail!("Failed to subtract {n} months from {datetime}"),
246 }
247}
248
249pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
255 match datetime.checked_add_months(chrono::Months::new(n)) {
256 Some(result) => Ok(result),
257 None => anyhow::bail!("Failed to add {n} months to {datetime}"),
258 }
259}
260
261pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
267 let datetime = unix_nanos.to_datetime_utc();
268 let result = subtract_n_months(datetime, n)?;
269 let timestamp = match result.timestamp_nanos_opt() {
270 Some(ts) => ts,
271 None => anyhow::bail!("Timestamp out of range after subtracting {n} months"),
272 };
273
274 if timestamp < 0 {
275 anyhow::bail!("Negative timestamp not allowed");
276 }
277
278 Ok(UnixNanos::from(timestamp as u64))
279}
280
281pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
287 let datetime = unix_nanos.to_datetime_utc();
288 let result = add_n_months(datetime, n)?;
289 let timestamp = match result.timestamp_nanos_opt() {
290 Some(ts) => ts,
291 None => anyhow::bail!("Timestamp out of range after adding {n} months"),
292 };
293
294 if timestamp < 0 {
295 anyhow::bail!("Negative timestamp not allowed");
296 }
297
298 Ok(UnixNanos::from(timestamp as u64))
299}
300
301pub fn add_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
307 let months = n.checked_mul(12).ok_or_else(|| {
308 anyhow::anyhow!("Failed to add {n} years to {datetime}: month count overflow")
309 })?;
310
311 match datetime.checked_add_months(chrono::Months::new(months)) {
312 Some(result) => Ok(result),
313 None => anyhow::bail!("Failed to add {n} years to {datetime}"),
314 }
315}
316
317pub fn subtract_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
323 let months = n.checked_mul(12).ok_or_else(|| {
324 anyhow::anyhow!("Failed to subtract {n} years from {datetime}: month count overflow")
325 })?;
326
327 match datetime.checked_sub_months(chrono::Months::new(months)) {
328 Some(result) => Ok(result),
329 None => anyhow::bail!("Failed to subtract {n} years from {datetime}"),
330 }
331}
332
333pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
339 let datetime = unix_nanos.to_datetime_utc();
340 let result = add_n_years(datetime, n)?;
341 let timestamp = match result.timestamp_nanos_opt() {
342 Some(ts) => ts,
343 None => anyhow::bail!("Timestamp out of range after adding {n} years"),
344 };
345
346 Ok(UnixNanos::from(timestamp as u64))
347}
348
349pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
355 let datetime = unix_nanos.to_datetime_utc();
356 let result = subtract_n_years(datetime, n)?;
357 let timestamp = match result.timestamp_nanos_opt() {
358 Some(ts) => ts,
359 None => anyhow::bail!("Timestamp out of range after subtracting {n} years"),
360 };
361
362 if timestamp < 0 {
363 anyhow::bail!("Negative timestamp not allowed");
364 }
365
366 Ok(UnixNanos::from(timestamp as u64))
367}
368
369#[must_use]
371pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
372 assert!(month >= 1 && month <= 12, "`month` must be in 1..=12");
374
375 match month {
377 2 => {
378 if is_leap_year(year) {
379 29
380 } else {
381 28
382 }
383 }
384 4 | 6 | 9 | 11 => 30,
385 _ => 31, }
387}
388
389#[must_use]
391pub const fn is_leap_year(year: i32) -> bool {
392 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
393}
394
395#[cfg(test)]
399#[allow(clippy::float_cmp)]
400mod tests {
401 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
402 use rstest::rstest;
403
404 use super::*;
405
406 #[rstest]
407 #[case(0.0, 0)]
408 #[case(1.0, 1_000_000_000)]
409 #[case(1.1, 1_100_000_000)]
410 #[case(42.0, 42_000_000_000)]
411 #[case(0.000_123_5, 123_500)]
412 #[case(0.000_000_01, 10)]
413 #[case(0.000_000_001, 1)]
414 #[case(9.999_999_999, 9_999_999_999)]
415 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
416 let result = secs_to_nanos(value);
417 assert_eq!(result, expected);
418 }
419
420 #[rstest]
421 #[case(0.0, 0)]
422 #[case(1.0, 1_000)]
423 #[case(1.1, 1_100)]
424 #[case(42.0, 42_000)]
425 #[case(0.012_34, 12)]
426 #[case(0.001, 1)]
427 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
428 let result = secs_to_millis(value);
429 assert_eq!(result, expected);
430 }
431
432 #[rstest]
433 #[should_panic(expected = "`month` must be in 1..=12")]
434 fn test_last_day_of_month_invalid_month() {
435 let _ = last_day_of_month(2024, 0);
436 }
437
438 #[rstest]
439 #[case(0.0, 0)]
440 #[case(1.0, 1_000_000)]
441 #[case(1.1, 1_100_000)]
442 #[case(42.0, 42_000_000)]
443 #[case(0.000_123_4, 123)]
444 #[case(0.000_01, 10)]
445 #[case(0.000_001, 1)]
446 #[case(9.999_999, 9_999_999)]
447 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
448 let result = millis_to_nanos(value);
449 assert_eq!(result, expected);
450 }
451
452 #[rstest]
453 #[case(0.0, 0)]
454 #[case(1.0, 1_000)]
455 #[case(1.1, 1_100)]
456 #[case(42.0, 42_000)]
457 #[case(0.1234, 123)]
458 #[case(0.01, 10)]
459 #[case(0.001, 1)]
460 #[case(9.999, 9_999)]
461 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
462 let result = micros_to_nanos(value);
463 assert_eq!(result, expected);
464 }
465
466 #[rstest]
467 #[case(0, 0.0)]
468 #[case(1, 1e-09)]
469 #[case(1_000_000_000, 1.0)]
470 #[case(42_897_123_111, 42.897_123_111)]
471 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
472 let result = nanos_to_secs(value);
473 assert_eq!(result, expected);
474 }
475
476 #[rstest]
477 #[case(0, 0)]
478 #[case(1_000_000, 1)]
479 #[case(1_000_000_000, 1000)]
480 #[case(42_897_123_111, 42897)]
481 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
482 let result = nanos_to_millis(value);
483 assert_eq!(result, expected);
484 }
485
486 #[rstest]
487 #[case(0, 0)]
488 #[case(1_000, 1)]
489 #[case(1_000_000_000, 1_000_000)]
490 #[case(42_897_123, 42_897)]
491 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
492 let result = nanos_to_micros(value);
493 assert_eq!(result, expected);
494 }
495
496 #[rstest]
497 #[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) {
504 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
505 assert_eq!(result, expected);
506 }
507
508 #[rstest]
509 #[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) {
514 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
515 assert_eq!(result, expected);
516 }
517
518 #[rstest]
519 #[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(
524 #[case] year: i32,
525 #[case] month: u32,
526 #[case] day: u32,
527 #[case] expected: u64,
528 ) {
529 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
530 assert_eq!(result, expected);
531 }
532
533 #[rstest]
534 fn test_last_closest_weekday_nanos_with_invalid_date() {
535 let result = last_weekday_nanos(2023, 4, 31);
536 assert!(result.is_err());
537 }
538
539 #[rstest]
540 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
541 let result = last_weekday_nanos(2023, 2, 30);
542 assert!(result.is_err());
543 }
544
545 #[rstest]
546 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
547 let result = last_weekday_nanos(9999, 12, 31);
548 assert!(result.is_err());
549 }
550
551 #[rstest]
552 fn test_is_within_last_24_hours_when_now() {
553 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
554 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
555 }
556
557 #[rstest]
558 fn test_is_within_last_24_hours_when_two_days_ago() {
559 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
560 .timestamp_nanos_opt()
561 .unwrap();
562 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
563 }
564
565 #[rstest]
566 #[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(
571 #[case] input: DateTime<Utc>,
572 #[case] months: u32,
573 #[case] expected: DateTime<Utc>,
574 ) {
575 let result = subtract_n_months(input, months).unwrap();
576 assert_eq!(result, expected);
577 }
578
579 #[rstest]
580 #[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(
585 #[case] input: DateTime<Utc>,
586 #[case] months: u32,
587 #[case] expected: DateTime<Utc>,
588 ) {
589 let result = add_n_months(input, months).unwrap();
590 assert_eq!(result, expected);
591 }
592
593 #[rstest]
594 fn test_add_n_years_overflow() {
595 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
596 let err = add_n_years(datetime, u32::MAX).unwrap_err();
597 assert!(err.to_string().contains("month count overflow"));
598 }
599
600 #[rstest]
601 fn test_subtract_n_years_overflow() {
602 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
603 let err = subtract_n_years(datetime, u32::MAX).unwrap_err();
604 assert!(err.to_string().contains("month count overflow"));
605 }
606
607 #[rstest]
608 fn test_add_n_years_nanos_overflow() {
609 let nanos = UnixNanos::from(0);
610 let err = add_n_years_nanos(nanos, u32::MAX).unwrap_err();
611 assert!(err.to_string().contains("month count overflow"));
612 }
613
614 #[rstest]
615 #[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) {
620 let result = last_day_of_month(year, month);
621 assert_eq!(result, expected);
622 }
623
624 #[rstest]
625 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
630 let result = is_leap_year(year);
631 assert_eq!(result, expected);
632 }
633
634 #[rstest]
635 #[case("1970-01-01T00:00:00.000000000Z", 0)] #[case("1970-01-01T00:00:00.000000001Z", 1)] #[case("1970-01-01T00:00:00.001000000Z", 1_000_000)] #[case("1970-01-01T00:00:01.000000000Z", 1_000_000_000)] #[case("2023-12-18T00:00:00.000000000Z", 1_702_857_600_000_000_000)] #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] #[case("2024-02-10", 1_707_523_200_000_000_000)] fn test_iso8601_to_unix_nanos(#[case] input: &str, #[case] expected: u64) {
644 let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
645 assert_eq!(result.as_u64(), expected);
646 }
647
648 #[rstest]
649 #[case("invalid-date")] #[case("2024-02-30")] #[case("2024-13-01")] #[case("not a timestamp")] fn test_iso8601_to_unix_nanos_invalid(#[case] input: &str) {
654 let result = iso8601_to_unix_nanos(input.to_string());
655 assert!(result.is_err());
656 }
657
658 #[rstest]
659 fn test_iso8601_roundtrip() {
660 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
661 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
662 let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
663 assert_eq!(parsed_nanos, original_nanos);
664 }
665}