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
44const _: () = {
46 assert!(NANOSECONDS_IN_SECOND == 1_000_000_000);
47 assert!(NANOSECONDS_IN_MILLISECOND == 1_000_000);
48 assert!(NANOSECONDS_IN_MICROSECOND == 1_000);
49 assert!(MILLISECONDS_IN_SECOND == 1_000);
50 assert!(NANOSECONDS_IN_SECOND == MILLISECONDS_IN_SECOND * NANOSECONDS_IN_MILLISECOND);
51 assert!(NANOSECONDS_IN_MILLISECOND == NANOSECONDS_IN_MICROSECOND * 1_000);
52 assert!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MILLISECOND == 1_000);
53 assert!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MICROSECOND == 1_000_000);
54};
55
56pub const WEEKDAYS: [Weekday; 5] = [
58 Weekday::Mon,
59 Weekday::Tue,
60 Weekday::Wed,
61 Weekday::Thu,
62 Weekday::Fri,
63];
64
65#[allow(
68 clippy::cast_possible_truncation,
69 clippy::cast_sign_loss,
70 reason = "Intentional for unit conversion, may lose precision after clamping"
71)]
72pub fn secs_to_nanos(secs: f64) -> anyhow::Result<u64> {
73 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
74 if secs <= 0.0 {
75 return Ok(0);
76 }
77 anyhow::ensure!(
78 secs <= MAX_SECS_FOR_NANOS,
79 "seconds {secs} exceeds maximum representable value {MAX_SECS_FOR_NANOS}"
80 );
81 let nanos = secs * NANOSECONDS_IN_SECOND as f64;
82 Ok(nanos.trunc() as u64)
83}
84
85#[allow(
88 clippy::cast_possible_truncation,
89 clippy::cast_sign_loss,
90 reason = "Intentional for unit conversion, may lose precision after clamping"
91)]
92pub fn secs_to_millis(secs: f64) -> anyhow::Result<u64> {
93 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
94 if secs <= 0.0 {
95 return Ok(0);
96 }
97 anyhow::ensure!(
98 secs <= MAX_SECS_FOR_MILLIS,
99 "seconds {secs} exceeds maximum representable value {MAX_SECS_FOR_MILLIS}"
100 );
101 let millis = secs * MILLISECONDS_IN_SECOND as f64;
102 Ok(millis.trunc() as u64)
103}
104
105#[must_use]
110pub fn secs_to_nanos_unchecked(secs: f64) -> u64 {
111 secs_to_nanos(secs).expect("secs_to_nanos_unchecked: invalid or overflowing input")
112}
113
114#[allow(
119 clippy::cast_possible_truncation,
120 clippy::cast_sign_loss,
121 reason = "Intentional for unit conversion, may lose precision after clamping"
122)]
123pub fn millis_to_nanos(millis: f64) -> anyhow::Result<u64> {
124 anyhow::ensure!(
125 millis.is_finite(),
126 "milliseconds must be finite, was {millis}"
127 );
128 if millis <= 0.0 {
129 return Ok(0);
130 }
131 anyhow::ensure!(
132 millis <= MAX_MILLIS_FOR_NANOS,
133 "milliseconds {millis} exceeds maximum representable value {MAX_MILLIS_FOR_NANOS}"
134 );
135 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
136 Ok(nanos.trunc() as u64)
137}
138
139#[must_use]
141pub fn millis_to_nanos_unchecked(millis: f64) -> u64 {
142 millis_to_nanos(millis).expect("millis_to_nanos_unchecked: invalid or overflowing input")
143}
144
145#[allow(
150 clippy::cast_possible_truncation,
151 clippy::cast_sign_loss,
152 reason = "Intentional for unit conversion, may lose precision after clamping"
153)]
154pub fn micros_to_nanos(micros: f64) -> anyhow::Result<u64> {
155 anyhow::ensure!(
156 micros.is_finite(),
157 "microseconds must be finite, was {micros}"
158 );
159 if micros <= 0.0 {
160 return Ok(0);
161 }
162 anyhow::ensure!(
163 micros <= MAX_MICROS_FOR_NANOS,
164 "microseconds {micros} exceeds maximum representable value {MAX_MICROS_FOR_NANOS}"
165 );
166 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
167 Ok(nanos.trunc() as u64)
168}
169
170#[must_use]
172pub fn micros_to_nanos_unchecked(micros: f64) -> u64 {
173 micros_to_nanos(micros).expect("micros_to_nanos_unchecked: invalid or overflowing input")
174}
175
176#[allow(
181 clippy::cast_precision_loss,
182 reason = "Precision loss acceptable for time conversion"
183)]
184#[must_use]
185pub fn nanos_to_secs(nanos: u64) -> f64 {
186 let seconds = nanos / NANOSECONDS_IN_SECOND;
187 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
188 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
189}
190
191#[must_use]
193pub const fn nanos_to_millis(nanos: u64) -> u64 {
194 nanos / NANOSECONDS_IN_MILLISECOND
195}
196
197#[must_use]
199pub const fn nanos_to_micros(nanos: u64) -> u64 {
200 nanos / NANOSECONDS_IN_MICROSECOND
201}
202
203#[inline]
205#[must_use]
206pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
207 let datetime = unix_nanos.to_datetime_utc();
208 datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
209}
210
211#[inline]
234pub fn iso8601_to_unix_nanos(date_string: String) -> anyhow::Result<UnixNanos> {
235 date_string
236 .parse::<UnixNanos>()
237 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{date_string}': {e}"))
238}
239
240#[inline]
243#[must_use]
244pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
245 let datetime = unix_nanos.to_datetime_utc();
246 datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
247}
248
249#[must_use]
251pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
252 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
253}
254
255pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
261 let date =
262 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
263 let current_weekday = date.weekday().number_from_monday();
264
265 let offset = i64::from(match current_weekday {
267 1..=5 => 0, 6 => 1, _ => 2, });
271 let last_closest = date - TimeDelta::days(offset);
273
274 let unix_timestamp_ns = last_closest
276 .and_hms_nano_opt(0, 0, 0, 0)
277 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
278
279 let raw_ns = unix_timestamp_ns
281 .and_utc()
282 .timestamp_nanos_opt()
283 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))?;
284 let ns_u64 =
285 u64::try_from(raw_ns).map_err(|_| anyhow::anyhow!("Negative timestamp: {raw_ns}"))?;
286 Ok(UnixNanos::from(ns_u64))
287}
288
289pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
295 let timestamp_ns = timestamp_ns.as_u64();
296 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
297 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
298 let secs_i64 = i64::try_from(seconds)
300 .map_err(|_| anyhow::anyhow!("Timestamp seconds overflow: {seconds}"))?;
301 let timestamp = DateTime::from_timestamp(secs_i64, nanoseconds)
302 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
303 let now = Utc::now();
304
305 if timestamp > now {
307 return Ok(false);
308 }
309
310 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
312}
313
314pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
320 match datetime.checked_sub_months(chrono::Months::new(n)) {
321 Some(result) => Ok(result),
322 None => anyhow::bail!("Failed to subtract {n} months from {datetime}"),
323 }
324}
325
326pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
332 match datetime.checked_add_months(chrono::Months::new(n)) {
333 Some(result) => Ok(result),
334 None => anyhow::bail!("Failed to add {n} months to {datetime}"),
335 }
336}
337
338pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
344 let datetime = unix_nanos.to_datetime_utc();
345 let result = subtract_n_months(datetime, n)?;
346 let timestamp = match result.timestamp_nanos_opt() {
347 Some(ts) => ts,
348 None => anyhow::bail!("Timestamp out of range after subtracting {n} months"),
349 };
350
351 if timestamp < 0 {
352 anyhow::bail!("Negative timestamp not allowed");
353 }
354
355 Ok(UnixNanos::from(timestamp as u64))
356}
357
358pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
364 let datetime = unix_nanos.to_datetime_utc();
365 let result = add_n_months(datetime, n)?;
366 let timestamp = match result.timestamp_nanos_opt() {
367 Some(ts) => ts,
368 None => anyhow::bail!("Timestamp out of range after adding {n} months"),
369 };
370
371 if timestamp < 0 {
372 anyhow::bail!("Negative timestamp not allowed");
373 }
374
375 Ok(UnixNanos::from(timestamp as u64))
376}
377
378pub fn add_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
384 let months = n.checked_mul(12).ok_or_else(|| {
385 anyhow::anyhow!("Failed to add {n} years to {datetime}: month count overflow")
386 })?;
387
388 match datetime.checked_add_months(chrono::Months::new(months)) {
389 Some(result) => Ok(result),
390 None => anyhow::bail!("Failed to add {n} years to {datetime}"),
391 }
392}
393
394pub fn subtract_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
400 let months = n.checked_mul(12).ok_or_else(|| {
401 anyhow::anyhow!("Failed to subtract {n} years from {datetime}: month count overflow")
402 })?;
403
404 match datetime.checked_sub_months(chrono::Months::new(months)) {
405 Some(result) => Ok(result),
406 None => anyhow::bail!("Failed to subtract {n} years from {datetime}"),
407 }
408}
409
410pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
416 let datetime = unix_nanos.to_datetime_utc();
417 let result = add_n_years(datetime, n)?;
418 let timestamp = match result.timestamp_nanos_opt() {
419 Some(ts) => ts,
420 None => anyhow::bail!("Timestamp out of range after adding {n} years"),
421 };
422
423 if timestamp < 0 {
424 anyhow::bail!("Negative timestamp not allowed");
425 }
426
427 Ok(UnixNanos::from(timestamp as u64))
428}
429
430pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
436 let datetime = unix_nanos.to_datetime_utc();
437 let result = subtract_n_years(datetime, n)?;
438 let timestamp = match result.timestamp_nanos_opt() {
439 Some(ts) => ts,
440 None => anyhow::bail!("Timestamp out of range after subtracting {n} years"),
441 };
442
443 if timestamp < 0 {
444 anyhow::bail!("Negative timestamp not allowed");
445 }
446
447 Ok(UnixNanos::from(timestamp as u64))
448}
449
450#[must_use]
454pub const fn last_day_of_month(year: i32, month: u32) -> Option<u32> {
455 if month < 1 || month > 12 {
457 return None;
458 }
459
460 Some(match month {
462 2 => {
463 if is_leap_year(year) {
464 29
465 } else {
466 28
467 }
468 }
469 4 | 6 | 9 | 11 => 30,
470 _ => 31, })
472}
473
474#[must_use]
476pub const fn is_leap_year(year: i32) -> bool {
477 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
478}
479
480pub fn datetime_to_unix_nanos(value: Option<DateTime<Utc>>) -> Option<UnixNanos> {
482 value
483 .and_then(|dt| dt.timestamp_nanos_opt())
484 .and_then(|nanos| u64::try_from(nanos).ok())
485 .map(UnixNanos::from)
486}
487
488#[cfg(test)]
489#[allow(
490 clippy::float_cmp,
491 reason = "Exact float comparisons acceptable in tests"
492)]
493mod tests {
494 use chrono::{DateTime, TimeDelta, TimeZone, Timelike, Utc};
495 use rstest::rstest;
496
497 use super::*;
498
499 #[rstest]
500 #[case(0.0, 0)]
501 #[case(1.0, 1_000_000_000)]
502 #[case(1.1, 1_100_000_000)]
503 #[case(42.0, 42_000_000_000)]
504 #[case(0.000_123_5, 123_500)]
505 #[case(0.000_000_01, 10)]
506 #[case(0.000_000_001, 1)]
507 #[case(9.999_999_999, 9_999_999_999)]
508 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
509 let result = secs_to_nanos(value).unwrap();
510 assert_eq!(result, expected);
511 }
512
513 #[rstest]
514 #[case(0.0, 0)]
515 #[case(1.0, 1_000)]
516 #[case(1.1, 1_100)]
517 #[case(42.0, 42_000)]
518 #[case(0.012_34, 12)]
519 #[case(0.001, 1)]
520 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
521 let result = secs_to_millis(value).unwrap();
522 assert_eq!(result, expected);
523 }
524
525 #[rstest]
526 fn test_secs_to_nanos_unchecked_matches_checked() {
527 assert_eq!(secs_to_nanos_unchecked(1.1), secs_to_nanos(1.1).unwrap());
528 }
529
530 #[rstest]
531 fn test_secs_to_nanos_non_finite_errors() {
532 let err = secs_to_nanos(f64::NAN).unwrap_err();
533 assert!(err.to_string().contains("finite"));
534 }
535
536 #[rstest]
537 fn test_secs_to_nanos_overflow_errors() {
538 let err = secs_to_nanos(MAX_SECS_FOR_NANOS + 1.0).unwrap_err();
539 assert!(err.to_string().contains("exceeds"));
540 }
541
542 #[rstest]
543 fn test_secs_to_millis_non_finite_errors() {
544 let err = secs_to_millis(f64::INFINITY).unwrap_err();
545 assert!(err.to_string().contains("finite"));
546 }
547
548 #[rstest]
549 fn test_millis_to_nanos_overflow_errors() {
550 let err = millis_to_nanos(MAX_MILLIS_FOR_NANOS + 1.0).unwrap_err();
551 assert!(err.to_string().contains("exceeds"));
552 }
553
554 #[rstest]
555 fn test_millis_to_nanos_non_finite_errors() {
556 let err = millis_to_nanos(f64::NEG_INFINITY).unwrap_err();
557 assert!(err.to_string().contains("finite"));
558 }
559
560 #[rstest]
561 fn test_micros_to_nanos_non_finite_errors() {
562 let err = micros_to_nanos(f64::NAN).unwrap_err();
563 assert!(err.to_string().contains("finite"));
564 }
565
566 #[rstest]
567 fn test_micros_to_nanos_overflow_errors() {
568 let err = micros_to_nanos(MAX_MICROS_FOR_NANOS * 2.0).unwrap_err();
570 assert!(err.to_string().contains("exceeds"));
571 }
572
573 #[rstest]
574 fn test_secs_to_nanos_negative_infinity_errors() {
575 let result = secs_to_nanos(f64::NEG_INFINITY);
576 assert!(result.is_err());
577 }
578
579 #[rstest]
580 #[case(2024, 0)] #[case(2024, 13)] fn test_last_day_of_month_invalid_month(#[case] year: i32, #[case] month: u32) {
583 assert!(last_day_of_month(year, month).is_none());
584 }
585
586 #[rstest]
587 #[case(0.0, 0)]
588 #[case(1.0, 1_000_000)]
589 #[case(1.1, 1_100_000)]
590 #[case(42.0, 42_000_000)]
591 #[case(0.000_123_4, 123)]
592 #[case(0.000_01, 10)]
593 #[case(0.000_001, 1)]
594 #[case(9.999_999, 9_999_999)]
595 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
596 let result = millis_to_nanos(value).unwrap();
597 assert_eq!(result, expected);
598 }
599
600 #[rstest]
601 fn test_millis_to_nanos_unchecked_matches_checked() {
602 assert_eq!(
603 millis_to_nanos_unchecked(1.1),
604 millis_to_nanos(1.1).unwrap()
605 );
606 }
607
608 #[rstest]
609 #[case(0.0, 0)]
610 #[case(1.0, 1_000)]
611 #[case(1.1, 1_100)]
612 #[case(42.0, 42_000)]
613 #[case(0.1234, 123)]
614 #[case(0.01, 10)]
615 #[case(0.001, 1)]
616 #[case(9.999, 9_999)]
617 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
618 let result = micros_to_nanos(value).unwrap();
619 assert_eq!(result, expected);
620 }
621
622 #[rstest]
623 fn test_micros_to_nanos_unchecked_matches_checked() {
624 assert_eq!(
625 micros_to_nanos_unchecked(1.1),
626 micros_to_nanos(1.1).unwrap()
627 );
628 }
629
630 #[rstest]
631 #[case(0, 0.0)]
632 #[case(1, 1e-09)]
633 #[case(1_000_000_000, 1.0)]
634 #[case(42_897_123_111, 42.897_123_111)]
635 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
636 let result = nanos_to_secs(value);
637 assert_eq!(result, expected);
638 }
639
640 #[rstest]
641 #[case(0, 0)]
642 #[case(1_000_000, 1)]
643 #[case(1_000_000_000, 1000)]
644 #[case(42_897_123_111, 42897)]
645 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
646 let result = nanos_to_millis(value);
647 assert_eq!(result, expected);
648 }
649
650 #[rstest]
651 #[case(0, 0)]
652 #[case(1_000, 1)]
653 #[case(1_000_000_000, 1_000_000)]
654 #[case(42_897_123, 42_897)]
655 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
656 let result = nanos_to_micros(value);
657 assert_eq!(result, expected);
658 }
659
660 #[rstest]
661 #[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) {
668 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
669 assert_eq!(result, expected);
670 }
671
672 #[rstest]
673 #[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) {
678 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
679 assert_eq!(result, expected);
680 }
681
682 #[rstest]
683 #[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(
688 #[case] year: i32,
689 #[case] month: u32,
690 #[case] day: u32,
691 #[case] expected: u64,
692 ) {
693 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
694 assert_eq!(result, expected);
695 }
696
697 #[rstest]
698 fn test_last_closest_weekday_nanos_with_invalid_date() {
699 let result = last_weekday_nanos(2023, 4, 31);
700 assert!(result.is_err());
701 }
702
703 #[rstest]
704 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
705 let result = last_weekday_nanos(2023, 2, 30);
706 assert!(result.is_err());
707 }
708
709 #[rstest]
710 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
711 let result = last_weekday_nanos(9999, 12, 31);
712 assert!(result.is_err());
713 }
714
715 #[rstest]
716 fn test_is_within_last_24_hours_when_now() {
717 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
718 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
719 }
720
721 #[rstest]
722 fn test_is_within_last_24_hours_when_two_days_ago() {
723 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
724 .timestamp_nanos_opt()
725 .unwrap();
726 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
727 }
728
729 #[rstest]
730 fn test_is_within_last_24_hours_when_future() {
731 let future_ns = (Utc::now() + TimeDelta::try_hours(1).unwrap())
733 .timestamp_nanos_opt()
734 .unwrap();
735 assert!(!is_within_last_24_hours(UnixNanos::from(future_ns as u64)).unwrap());
736
737 let future_ns = (Utc::now() + TimeDelta::try_days(1).unwrap())
739 .timestamp_nanos_opt()
740 .unwrap();
741 assert!(!is_within_last_24_hours(UnixNanos::from(future_ns as u64)).unwrap());
742 }
743
744 #[rstest]
745 #[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(
750 #[case] input: DateTime<Utc>,
751 #[case] months: u32,
752 #[case] expected: DateTime<Utc>,
753 ) {
754 let result = subtract_n_months(input, months).unwrap();
755 assert_eq!(result, expected);
756 }
757
758 #[rstest]
759 #[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(
764 #[case] input: DateTime<Utc>,
765 #[case] months: u32,
766 #[case] expected: DateTime<Utc>,
767 ) {
768 let result = add_n_months(input, months).unwrap();
769 assert_eq!(result, expected);
770 }
771
772 #[rstest]
773 fn test_add_n_years_overflow() {
774 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
775 let err = add_n_years(datetime, u32::MAX).unwrap_err();
776 assert!(err.to_string().contains("month count overflow"));
777 }
778
779 #[rstest]
780 fn test_subtract_n_years_overflow() {
781 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
782 let err = subtract_n_years(datetime, u32::MAX).unwrap_err();
783 assert!(err.to_string().contains("month count overflow"));
784 }
785
786 #[rstest]
787 fn test_add_n_years_nanos_overflow() {
788 let nanos = UnixNanos::from(0);
789 let err = add_n_years_nanos(nanos, u32::MAX).unwrap_err();
790 assert!(err.to_string().contains("month count overflow"));
791 }
792
793 #[rstest]
794 #[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) {
799 let result = last_day_of_month(year, month).unwrap();
800 assert_eq!(result, expected);
801 }
802
803 #[rstest]
804 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
809 let result = is_leap_year(year);
810 assert_eq!(result, expected);
811 }
812
813 #[rstest]
814 #[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) {
823 let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
824 assert_eq!(result.as_u64(), expected);
825 }
826
827 #[rstest]
828 #[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) {
833 let result = iso8601_to_unix_nanos(input.to_string());
834 assert!(result.is_err());
835 }
836
837 #[rstest]
838 fn test_iso8601_roundtrip() {
839 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
840 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
841 let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
842 assert_eq!(parsed_nanos, original_nanos);
843 }
844
845 #[rstest]
846 fn test_add_n_years_nanos_normal_case() {
847 let start = UnixNanos::from(Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap());
849 let result = add_n_years_nanos(start, 1).unwrap();
850 let expected = UnixNanos::from(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap());
851 assert_eq!(result, expected);
852 }
853
854 #[rstest]
855 fn test_add_n_years_nanos_prevents_negative_timestamp() {
856 let start = UnixNanos::from(0); let result = add_n_years_nanos(start, 1);
862 assert!(result.is_ok());
863 }
864
865 #[rstest]
866 fn test_datetime_to_unix_nanos_at_epoch() {
867 let epoch = Utc.timestamp_opt(0, 0).unwrap();
869 let result = datetime_to_unix_nanos(Some(epoch));
870 assert_eq!(result, Some(UnixNanos::from(0)));
871 }
872
873 #[rstest]
874 fn test_datetime_to_unix_nanos_typical_datetime() {
875 let dt = Utc
876 .with_ymd_and_hms(2024, 1, 15, 13, 30, 45)
877 .unwrap()
878 .with_nanosecond(123_456_789)
879 .unwrap();
880 let result = datetime_to_unix_nanos(Some(dt));
881
882 assert!(result.is_some());
884 assert_eq!(result.unwrap().as_u64(), 1_705_325_445_123_456_789);
885 }
886
887 #[rstest]
888 fn test_datetime_to_unix_nanos_before_epoch() {
889 let before_epoch = Utc.with_ymd_and_hms(1969, 12, 31, 23, 59, 59).unwrap();
892 let result = datetime_to_unix_nanos(Some(before_epoch));
893 assert_eq!(result, None);
894 }
895
896 #[rstest]
897 fn test_datetime_to_unix_nanos_one_second_after_epoch() {
898 let dt = Utc.timestamp_opt(1, 0).unwrap();
900 let result = datetime_to_unix_nanos(Some(dt));
901 assert_eq!(result, Some(UnixNanos::from(1_000_000_000)));
902 }
903
904 #[rstest]
905 fn test_datetime_to_unix_nanos_with_subsecond_precision() {
906 let dt = Utc.timestamp_opt(0, 1_000).unwrap(); let result = datetime_to_unix_nanos(Some(dt));
909 assert_eq!(result, Some(UnixNanos::from(1_000)));
910 }
911}