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
35const MAX_SECS_FOR_NANOS: f64 = u64::MAX as f64 / NANOSECONDS_IN_SECOND as f64;
37const MAX_SECS_FOR_MILLIS: f64 = u64::MAX as f64 / MILLISECONDS_IN_SECOND as f64;
39const MAX_MILLIS_FOR_NANOS: f64 = u64::MAX as f64 / NANOSECONDS_IN_MILLISECOND as f64;
41const MAX_MICROS_FOR_NANOS: f64 = u64::MAX as f64 / NANOSECONDS_IN_MICROSECOND as f64;
43
44#[cfg(test)]
46mod compile_time_checks {
47 use static_assertions::const_assert_eq;
48
49 use super::*;
50
51 const_assert_eq!(NANOSECONDS_IN_SECOND, 1_000_000_000);
53 const_assert_eq!(NANOSECONDS_IN_MILLISECOND, 1_000_000);
54 const_assert_eq!(NANOSECONDS_IN_MICROSECOND, 1_000);
55 const_assert_eq!(MILLISECONDS_IN_SECOND, 1_000);
56
57 const_assert_eq!(
59 NANOSECONDS_IN_SECOND,
60 MILLISECONDS_IN_SECOND * NANOSECONDS_IN_MILLISECOND
61 );
62 const_assert_eq!(
63 NANOSECONDS_IN_MILLISECOND,
64 NANOSECONDS_IN_MICROSECOND * 1_000
65 );
66 const_assert_eq!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MILLISECOND, 1_000);
67 const_assert_eq!(
68 NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MICROSECOND,
69 1_000_000
70 );
71}
72
73pub const WEEKDAYS: [Weekday; 5] = [
75 Weekday::Mon,
76 Weekday::Tue,
77 Weekday::Wed,
78 Weekday::Thu,
79 Weekday::Fri,
80];
81
82#[allow(
85 clippy::cast_possible_truncation,
86 clippy::cast_sign_loss,
87 reason = "Intentional for unit conversion, may lose precision after clamping"
88)]
89pub fn secs_to_nanos(secs: f64) -> anyhow::Result<u64> {
90 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
91 if secs <= 0.0 {
92 return Ok(0);
93 }
94 anyhow::ensure!(
95 secs <= MAX_SECS_FOR_NANOS,
96 "seconds {secs} exceeds maximum representable value {MAX_SECS_FOR_NANOS}"
97 );
98 let nanos = secs * NANOSECONDS_IN_SECOND as f64;
99 Ok(nanos.trunc() as u64)
100}
101
102#[allow(
105 clippy::cast_possible_truncation,
106 clippy::cast_sign_loss,
107 reason = "Intentional for unit conversion, may lose precision after clamping"
108)]
109pub fn secs_to_millis(secs: f64) -> anyhow::Result<u64> {
110 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
111 if secs <= 0.0 {
112 return Ok(0);
113 }
114 anyhow::ensure!(
115 secs <= MAX_SECS_FOR_MILLIS,
116 "seconds {secs} exceeds maximum representable value {MAX_SECS_FOR_MILLIS}"
117 );
118 let millis = secs * MILLISECONDS_IN_SECOND as f64;
119 Ok(millis.trunc() as u64)
120}
121
122#[must_use]
127pub fn secs_to_nanos_unchecked(secs: f64) -> u64 {
128 secs_to_nanos(secs).expect("secs_to_nanos_unchecked: invalid or overflowing input")
129}
130
131#[allow(
136 clippy::cast_possible_truncation,
137 clippy::cast_sign_loss,
138 reason = "Intentional for unit conversion, may lose precision after clamping"
139)]
140pub fn millis_to_nanos(millis: f64) -> anyhow::Result<u64> {
141 anyhow::ensure!(
142 millis.is_finite(),
143 "milliseconds must be finite, was {millis}"
144 );
145 if millis <= 0.0 {
146 return Ok(0);
147 }
148 anyhow::ensure!(
149 millis <= MAX_MILLIS_FOR_NANOS,
150 "milliseconds {millis} exceeds maximum representable value {MAX_MILLIS_FOR_NANOS}"
151 );
152 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
153 Ok(nanos.trunc() as u64)
154}
155
156#[must_use]
158pub fn millis_to_nanos_unchecked(millis: f64) -> u64 {
159 millis_to_nanos(millis).expect("millis_to_nanos_unchecked: invalid or overflowing input")
160}
161
162#[allow(
167 clippy::cast_possible_truncation,
168 clippy::cast_sign_loss,
169 reason = "Intentional for unit conversion, may lose precision after clamping"
170)]
171pub fn micros_to_nanos(micros: f64) -> anyhow::Result<u64> {
172 anyhow::ensure!(
173 micros.is_finite(),
174 "microseconds must be finite, was {micros}"
175 );
176 if micros <= 0.0 {
177 return Ok(0);
178 }
179 anyhow::ensure!(
180 micros <= MAX_MICROS_FOR_NANOS,
181 "microseconds {micros} exceeds maximum representable value {MAX_MICROS_FOR_NANOS}"
182 );
183 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
184 Ok(nanos.trunc() as u64)
185}
186
187#[must_use]
189pub fn micros_to_nanos_unchecked(micros: f64) -> u64 {
190 micros_to_nanos(micros).expect("micros_to_nanos_unchecked: invalid or overflowing input")
191}
192
193#[allow(
198 clippy::cast_precision_loss,
199 reason = "Precision loss acceptable for time conversion"
200)]
201#[must_use]
202pub fn nanos_to_secs(nanos: u64) -> f64 {
203 let seconds = nanos / NANOSECONDS_IN_SECOND;
204 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
205 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
206}
207
208#[must_use]
210pub const fn nanos_to_millis(nanos: u64) -> u64 {
211 nanos / NANOSECONDS_IN_MILLISECOND
212}
213
214#[must_use]
216pub const fn nanos_to_micros(nanos: u64) -> u64 {
217 nanos / NANOSECONDS_IN_MICROSECOND
218}
219
220#[inline]
222#[must_use]
223pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
224 let datetime = unix_nanos.to_datetime_utc();
225 datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
226}
227
228#[inline]
251pub fn iso8601_to_unix_nanos(date_string: String) -> anyhow::Result<UnixNanos> {
252 date_string
253 .parse::<UnixNanos>()
254 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{date_string}': {e}"))
255}
256
257#[inline]
260#[must_use]
261pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
262 let datetime = unix_nanos.to_datetime_utc();
263 datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
264}
265
266#[must_use]
268pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
269 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
270}
271
272pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
278 let date =
279 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
280 let current_weekday = date.weekday().number_from_monday();
281
282 let offset = i64::from(match current_weekday {
284 1..=5 => 0, 6 => 1, _ => 2, });
288 let last_closest = date - TimeDelta::days(offset);
290
291 let unix_timestamp_ns = last_closest
293 .and_hms_nano_opt(0, 0, 0, 0)
294 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
295
296 let raw_ns = unix_timestamp_ns
298 .and_utc()
299 .timestamp_nanos_opt()
300 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))?;
301 let ns_u64 =
302 u64::try_from(raw_ns).map_err(|_| anyhow::anyhow!("Negative timestamp: {raw_ns}"))?;
303 Ok(UnixNanos::from(ns_u64))
304}
305
306pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
312 let timestamp_ns = timestamp_ns.as_u64();
313 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
314 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
315 let secs_i64 = i64::try_from(seconds)
317 .map_err(|_| anyhow::anyhow!("Timestamp seconds overflow: {seconds}"))?;
318 let timestamp = DateTime::from_timestamp(secs_i64, nanoseconds)
319 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
320 let now = Utc::now();
321
322 if timestamp > now {
324 return Ok(false);
325 }
326
327 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
329}
330
331pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
337 match datetime.checked_sub_months(chrono::Months::new(n)) {
338 Some(result) => Ok(result),
339 None => anyhow::bail!("Failed to subtract {n} months from {datetime}"),
340 }
341}
342
343pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
349 match datetime.checked_add_months(chrono::Months::new(n)) {
350 Some(result) => Ok(result),
351 None => anyhow::bail!("Failed to add {n} months to {datetime}"),
352 }
353}
354
355pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
361 let datetime = unix_nanos.to_datetime_utc();
362 let result = subtract_n_months(datetime, n)?;
363 let timestamp = match result.timestamp_nanos_opt() {
364 Some(ts) => ts,
365 None => anyhow::bail!("Timestamp out of range after subtracting {n} months"),
366 };
367
368 if timestamp < 0 {
369 anyhow::bail!("Negative timestamp not allowed");
370 }
371
372 Ok(UnixNanos::from(timestamp as u64))
373}
374
375pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
381 let datetime = unix_nanos.to_datetime_utc();
382 let result = add_n_months(datetime, n)?;
383 let timestamp = match result.timestamp_nanos_opt() {
384 Some(ts) => ts,
385 None => anyhow::bail!("Timestamp out of range after adding {n} months"),
386 };
387
388 if timestamp < 0 {
389 anyhow::bail!("Negative timestamp not allowed");
390 }
391
392 Ok(UnixNanos::from(timestamp as u64))
393}
394
395pub fn add_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
401 let months = n.checked_mul(12).ok_or_else(|| {
402 anyhow::anyhow!("Failed to add {n} years to {datetime}: month count overflow")
403 })?;
404
405 match datetime.checked_add_months(chrono::Months::new(months)) {
406 Some(result) => Ok(result),
407 None => anyhow::bail!("Failed to add {n} years to {datetime}"),
408 }
409}
410
411pub fn subtract_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
417 let months = n.checked_mul(12).ok_or_else(|| {
418 anyhow::anyhow!("Failed to subtract {n} years from {datetime}: month count overflow")
419 })?;
420
421 match datetime.checked_sub_months(chrono::Months::new(months)) {
422 Some(result) => Ok(result),
423 None => anyhow::bail!("Failed to subtract {n} years from {datetime}"),
424 }
425}
426
427pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
433 let datetime = unix_nanos.to_datetime_utc();
434 let result = add_n_years(datetime, n)?;
435 let timestamp = match result.timestamp_nanos_opt() {
436 Some(ts) => ts,
437 None => anyhow::bail!("Timestamp out of range after adding {n} years"),
438 };
439
440 if timestamp < 0 {
441 anyhow::bail!("Negative timestamp not allowed");
442 }
443
444 Ok(UnixNanos::from(timestamp as u64))
445}
446
447pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
453 let datetime = unix_nanos.to_datetime_utc();
454 let result = subtract_n_years(datetime, n)?;
455 let timestamp = match result.timestamp_nanos_opt() {
456 Some(ts) => ts,
457 None => anyhow::bail!("Timestamp out of range after subtracting {n} years"),
458 };
459
460 if timestamp < 0 {
461 anyhow::bail!("Negative timestamp not allowed");
462 }
463
464 Ok(UnixNanos::from(timestamp as u64))
465}
466
467#[must_use]
469pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
470 assert!(month >= 1 && month <= 12, "`month` must be in 1..=12");
472
473 match month {
475 2 => {
476 if is_leap_year(year) {
477 29
478 } else {
479 28
480 }
481 }
482 4 | 6 | 9 | 11 => 30,
483 _ => 31, }
485}
486
487#[must_use]
489pub const fn is_leap_year(year: i32) -> bool {
490 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
491}
492
493#[cfg(test)]
494#[allow(
495 clippy::float_cmp,
496 reason = "Exact float comparisons acceptable in tests"
497)]
498mod tests {
499 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
500 use rstest::rstest;
501
502 use super::*;
503
504 #[rstest]
505 #[case(0.0, 0)]
506 #[case(1.0, 1_000_000_000)]
507 #[case(1.1, 1_100_000_000)]
508 #[case(42.0, 42_000_000_000)]
509 #[case(0.000_123_5, 123_500)]
510 #[case(0.000_000_01, 10)]
511 #[case(0.000_000_001, 1)]
512 #[case(9.999_999_999, 9_999_999_999)]
513 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
514 let result = secs_to_nanos(value).unwrap();
515 assert_eq!(result, expected);
516 }
517
518 #[rstest]
519 #[case(0.0, 0)]
520 #[case(1.0, 1_000)]
521 #[case(1.1, 1_100)]
522 #[case(42.0, 42_000)]
523 #[case(0.012_34, 12)]
524 #[case(0.001, 1)]
525 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
526 let result = secs_to_millis(value).unwrap();
527 assert_eq!(result, expected);
528 }
529
530 #[rstest]
531 fn test_secs_to_nanos_unchecked_matches_checked() {
532 assert_eq!(secs_to_nanos_unchecked(1.1), secs_to_nanos(1.1).unwrap());
533 }
534
535 #[rstest]
536 fn test_secs_to_nanos_non_finite_errors() {
537 let err = secs_to_nanos(f64::NAN).unwrap_err();
538 assert!(err.to_string().contains("finite"));
539 }
540
541 #[rstest]
542 fn test_secs_to_nanos_overflow_errors() {
543 let err = secs_to_nanos(MAX_SECS_FOR_NANOS + 1.0).unwrap_err();
544 assert!(err.to_string().contains("exceeds"));
545 }
546
547 #[rstest]
548 fn test_secs_to_millis_non_finite_errors() {
549 let err = secs_to_millis(f64::INFINITY).unwrap_err();
550 assert!(err.to_string().contains("finite"));
551 }
552
553 #[rstest]
554 fn test_millis_to_nanos_overflow_errors() {
555 let err = millis_to_nanos(MAX_MILLIS_FOR_NANOS + 1.0).unwrap_err();
556 assert!(err.to_string().contains("exceeds"));
557 }
558
559 #[rstest]
560 fn test_millis_to_nanos_non_finite_errors() {
561 let err = millis_to_nanos(f64::NEG_INFINITY).unwrap_err();
562 assert!(err.to_string().contains("finite"));
563 }
564
565 #[rstest]
566 fn test_micros_to_nanos_non_finite_errors() {
567 let err = micros_to_nanos(f64::NAN).unwrap_err();
568 assert!(err.to_string().contains("finite"));
569 }
570
571 #[rstest]
572 fn test_micros_to_nanos_overflow_errors() {
573 let err = micros_to_nanos(MAX_MICROS_FOR_NANOS * 2.0).unwrap_err();
575 assert!(err.to_string().contains("exceeds"));
576 }
577
578 #[rstest]
579 fn test_secs_to_nanos_negative_infinity_errors() {
580 let result = secs_to_nanos(f64::NEG_INFINITY);
581 assert!(result.is_err());
582 }
583
584 #[rstest]
585 #[should_panic(expected = "`month` must be in 1..=12")]
586 fn test_last_day_of_month_invalid_month() {
587 let _ = last_day_of_month(2024, 0);
588 }
589
590 #[rstest]
591 #[case(0.0, 0)]
592 #[case(1.0, 1_000_000)]
593 #[case(1.1, 1_100_000)]
594 #[case(42.0, 42_000_000)]
595 #[case(0.000_123_4, 123)]
596 #[case(0.000_01, 10)]
597 #[case(0.000_001, 1)]
598 #[case(9.999_999, 9_999_999)]
599 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
600 let result = millis_to_nanos(value).unwrap();
601 assert_eq!(result, expected);
602 }
603
604 #[rstest]
605 fn test_millis_to_nanos_unchecked_matches_checked() {
606 assert_eq!(
607 millis_to_nanos_unchecked(1.1),
608 millis_to_nanos(1.1).unwrap()
609 );
610 }
611
612 #[rstest]
613 #[case(0.0, 0)]
614 #[case(1.0, 1_000)]
615 #[case(1.1, 1_100)]
616 #[case(42.0, 42_000)]
617 #[case(0.1234, 123)]
618 #[case(0.01, 10)]
619 #[case(0.001, 1)]
620 #[case(9.999, 9_999)]
621 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
622 let result = micros_to_nanos(value).unwrap();
623 assert_eq!(result, expected);
624 }
625
626 #[rstest]
627 fn test_micros_to_nanos_unchecked_matches_checked() {
628 assert_eq!(
629 micros_to_nanos_unchecked(1.1),
630 micros_to_nanos(1.1).unwrap()
631 );
632 }
633
634 #[rstest]
635 #[case(0, 0.0)]
636 #[case(1, 1e-09)]
637 #[case(1_000_000_000, 1.0)]
638 #[case(42_897_123_111, 42.897_123_111)]
639 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
640 let result = nanos_to_secs(value);
641 assert_eq!(result, expected);
642 }
643
644 #[rstest]
645 #[case(0, 0)]
646 #[case(1_000_000, 1)]
647 #[case(1_000_000_000, 1000)]
648 #[case(42_897_123_111, 42897)]
649 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
650 let result = nanos_to_millis(value);
651 assert_eq!(result, expected);
652 }
653
654 #[rstest]
655 #[case(0, 0)]
656 #[case(1_000, 1)]
657 #[case(1_000_000_000, 1_000_000)]
658 #[case(42_897_123, 42_897)]
659 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
660 let result = nanos_to_micros(value);
661 assert_eq!(result, expected);
662 }
663
664 #[rstest]
665 #[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) {
672 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
673 assert_eq!(result, expected);
674 }
675
676 #[rstest]
677 #[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) {
682 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
683 assert_eq!(result, expected);
684 }
685
686 #[rstest]
687 #[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(
692 #[case] year: i32,
693 #[case] month: u32,
694 #[case] day: u32,
695 #[case] expected: u64,
696 ) {
697 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
698 assert_eq!(result, expected);
699 }
700
701 #[rstest]
702 fn test_last_closest_weekday_nanos_with_invalid_date() {
703 let result = last_weekday_nanos(2023, 4, 31);
704 assert!(result.is_err());
705 }
706
707 #[rstest]
708 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
709 let result = last_weekday_nanos(2023, 2, 30);
710 assert!(result.is_err());
711 }
712
713 #[rstest]
714 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
715 let result = last_weekday_nanos(9999, 12, 31);
716 assert!(result.is_err());
717 }
718
719 #[rstest]
720 fn test_is_within_last_24_hours_when_now() {
721 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
722 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
723 }
724
725 #[rstest]
726 fn test_is_within_last_24_hours_when_two_days_ago() {
727 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
728 .timestamp_nanos_opt()
729 .unwrap();
730 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
731 }
732
733 #[rstest]
734 fn test_is_within_last_24_hours_when_future() {
735 let future_ns = (Utc::now() + TimeDelta::try_hours(1).unwrap())
737 .timestamp_nanos_opt()
738 .unwrap();
739 assert!(!is_within_last_24_hours(UnixNanos::from(future_ns as u64)).unwrap());
740
741 let future_ns = (Utc::now() + TimeDelta::try_days(1).unwrap())
743 .timestamp_nanos_opt()
744 .unwrap();
745 assert!(!is_within_last_24_hours(UnixNanos::from(future_ns as u64)).unwrap());
746 }
747
748 #[rstest]
749 #[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(
754 #[case] input: DateTime<Utc>,
755 #[case] months: u32,
756 #[case] expected: DateTime<Utc>,
757 ) {
758 let result = subtract_n_months(input, months).unwrap();
759 assert_eq!(result, expected);
760 }
761
762 #[rstest]
763 #[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(
768 #[case] input: DateTime<Utc>,
769 #[case] months: u32,
770 #[case] expected: DateTime<Utc>,
771 ) {
772 let result = add_n_months(input, months).unwrap();
773 assert_eq!(result, expected);
774 }
775
776 #[rstest]
777 fn test_add_n_years_overflow() {
778 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
779 let err = add_n_years(datetime, u32::MAX).unwrap_err();
780 assert!(err.to_string().contains("month count overflow"));
781 }
782
783 #[rstest]
784 fn test_subtract_n_years_overflow() {
785 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
786 let err = subtract_n_years(datetime, u32::MAX).unwrap_err();
787 assert!(err.to_string().contains("month count overflow"));
788 }
789
790 #[rstest]
791 fn test_add_n_years_nanos_overflow() {
792 let nanos = UnixNanos::from(0);
793 let err = add_n_years_nanos(nanos, u32::MAX).unwrap_err();
794 assert!(err.to_string().contains("month count overflow"));
795 }
796
797 #[rstest]
798 #[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) {
803 let result = last_day_of_month(year, month);
804 assert_eq!(result, expected);
805 }
806
807 #[rstest]
808 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
813 let result = is_leap_year(year);
814 assert_eq!(result, expected);
815 }
816
817 #[rstest]
818 #[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) {
827 let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
828 assert_eq!(result.as_u64(), expected);
829 }
830
831 #[rstest]
832 #[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) {
837 let result = iso8601_to_unix_nanos(input.to_string());
838 assert!(result.is_err());
839 }
840
841 #[rstest]
842 fn test_iso8601_roundtrip() {
843 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
844 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
845 let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
846 assert_eq!(parsed_nanos, original_nanos);
847 }
848
849 #[rstest]
850 fn test_add_n_years_nanos_normal_case() {
851 let start = UnixNanos::from(Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap());
853 let result = add_n_years_nanos(start, 1).unwrap();
854 let expected = UnixNanos::from(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap());
855 assert_eq!(result, expected);
856 }
857
858 #[rstest]
859 fn test_add_n_years_nanos_prevents_negative_timestamp() {
860 let start = UnixNanos::from(0); let result = add_n_years_nanos(start, 1);
866 assert!(result.is_ok());
867 }
868}