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
35#[cfg(test)]
37mod compile_time_checks {
38 use static_assertions::const_assert_eq;
39
40 use super::*;
41
42 const_assert_eq!(NANOSECONDS_IN_SECOND, 1_000_000_000);
44 const_assert_eq!(NANOSECONDS_IN_MILLISECOND, 1_000_000);
45 const_assert_eq!(NANOSECONDS_IN_MICROSECOND, 1_000);
46 const_assert_eq!(MILLISECONDS_IN_SECOND, 1_000);
47
48 const_assert_eq!(
50 NANOSECONDS_IN_SECOND,
51 MILLISECONDS_IN_SECOND * NANOSECONDS_IN_MILLISECOND
52 );
53 const_assert_eq!(
54 NANOSECONDS_IN_MILLISECOND,
55 NANOSECONDS_IN_MICROSECOND * 1_000
56 );
57 const_assert_eq!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MILLISECOND, 1_000);
58 const_assert_eq!(
59 NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MICROSECOND,
60 1_000_000
61 );
62}
63
64pub const WEEKDAYS: [Weekday; 5] = [
66 Weekday::Mon,
67 Weekday::Tue,
68 Weekday::Wed,
69 Weekday::Thu,
70 Weekday::Fri,
71];
72
73#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
78#[must_use]
79pub fn secs_to_nanos(secs: f64) -> u64 {
80 let nanos = secs * NANOSECONDS_IN_SECOND as f64;
81 nanos.max(0.0).trunc() as u64
82}
83
84#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
89#[must_use]
90pub fn secs_to_millis(secs: f64) -> u64 {
91 let millis = secs * MILLISECONDS_IN_SECOND as f64;
92 millis.max(0.0).trunc() as u64
93}
94
95#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
100#[must_use]
101pub fn millis_to_nanos(millis: f64) -> u64 {
102 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
103 nanos.max(0.0).trunc() as u64
104}
105
106#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
111#[must_use]
112pub fn micros_to_nanos(micros: f64) -> u64 {
113 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
114 nanos.max(0.0).trunc() as u64
115}
116
117#[allow(clippy::cast_precision_loss)]
122#[must_use]
123pub fn nanos_to_secs(nanos: u64) -> f64 {
124 let seconds = nanos / NANOSECONDS_IN_SECOND;
125 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
126 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
127}
128
129#[must_use]
131pub const fn nanos_to_millis(nanos: u64) -> u64 {
132 nanos / NANOSECONDS_IN_MILLISECOND
133}
134
135#[must_use]
137pub const fn nanos_to_micros(nanos: u64) -> u64 {
138 nanos / NANOSECONDS_IN_MICROSECOND
139}
140
141#[inline]
143#[must_use]
144pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
145 let datetime = unix_nanos.to_datetime_utc();
146 datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
147}
148
149#[inline]
192pub fn iso8601_to_unix_nanos(date_string: String) -> anyhow::Result<UnixNanos> {
193 date_string
194 .parse::<UnixNanos>()
195 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{date_string}': {e}"))
196}
197
198#[inline]
201#[must_use]
202pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
203 let datetime = unix_nanos.to_datetime_utc();
204 datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
205}
206
207#[must_use]
209pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
210 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
211}
212
213pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
219 let date =
220 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
221 let current_weekday = date.weekday().number_from_monday();
222
223 let offset = i64::from(match current_weekday {
225 1..=5 => 0, 6 => 1, _ => 2, });
229 let last_closest = date - TimeDelta::days(offset);
231
232 let unix_timestamp_ns = last_closest
234 .and_hms_nano_opt(0, 0, 0, 0)
235 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
236
237 let raw_ns = unix_timestamp_ns
239 .and_utc()
240 .timestamp_nanos_opt()
241 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))?;
242 let ns_u64 =
243 u64::try_from(raw_ns).map_err(|_| anyhow::anyhow!("Negative timestamp: {raw_ns}"))?;
244 Ok(UnixNanos::from(ns_u64))
245}
246
247pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
253 let timestamp_ns = timestamp_ns.as_u64();
254 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
255 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
256 let secs_i64 = i64::try_from(seconds)
258 .map_err(|_| anyhow::anyhow!("Timestamp seconds overflow: {seconds}"))?;
259 let timestamp = DateTime::from_timestamp(secs_i64, nanoseconds)
260 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
261 let now = Utc::now();
262
263 if timestamp > now {
265 return Ok(false);
266 }
267
268 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
270}
271
272pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
278 match datetime.checked_sub_months(chrono::Months::new(n)) {
279 Some(result) => Ok(result),
280 None => anyhow::bail!("Failed to subtract {n} months from {datetime}"),
281 }
282}
283
284pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
290 match datetime.checked_add_months(chrono::Months::new(n)) {
291 Some(result) => Ok(result),
292 None => anyhow::bail!("Failed to add {n} months to {datetime}"),
293 }
294}
295
296pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
302 let datetime = unix_nanos.to_datetime_utc();
303 let result = subtract_n_months(datetime, n)?;
304 let timestamp = match result.timestamp_nanos_opt() {
305 Some(ts) => ts,
306 None => anyhow::bail!("Timestamp out of range after subtracting {n} months"),
307 };
308
309 if timestamp < 0 {
310 anyhow::bail!("Negative timestamp not allowed");
311 }
312
313 Ok(UnixNanos::from(timestamp as u64))
314}
315
316pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
322 let datetime = unix_nanos.to_datetime_utc();
323 let result = add_n_months(datetime, n)?;
324 let timestamp = match result.timestamp_nanos_opt() {
325 Some(ts) => ts,
326 None => anyhow::bail!("Timestamp out of range after adding {n} months"),
327 };
328
329 if timestamp < 0 {
330 anyhow::bail!("Negative timestamp not allowed");
331 }
332
333 Ok(UnixNanos::from(timestamp as u64))
334}
335
336pub fn add_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
342 let months = n.checked_mul(12).ok_or_else(|| {
343 anyhow::anyhow!("Failed to add {n} years to {datetime}: month count overflow")
344 })?;
345
346 match datetime.checked_add_months(chrono::Months::new(months)) {
347 Some(result) => Ok(result),
348 None => anyhow::bail!("Failed to add {n} years to {datetime}"),
349 }
350}
351
352pub fn subtract_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
358 let months = n.checked_mul(12).ok_or_else(|| {
359 anyhow::anyhow!("Failed to subtract {n} years from {datetime}: month count overflow")
360 })?;
361
362 match datetime.checked_sub_months(chrono::Months::new(months)) {
363 Some(result) => Ok(result),
364 None => anyhow::bail!("Failed to subtract {n} years from {datetime}"),
365 }
366}
367
368pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
374 let datetime = unix_nanos.to_datetime_utc();
375 let result = add_n_years(datetime, n)?;
376 let timestamp = match result.timestamp_nanos_opt() {
377 Some(ts) => ts,
378 None => anyhow::bail!("Timestamp out of range after adding {n} years"),
379 };
380
381 if timestamp < 0 {
382 anyhow::bail!("Negative timestamp not allowed");
383 }
384
385 Ok(UnixNanos::from(timestamp as u64))
386}
387
388pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
394 let datetime = unix_nanos.to_datetime_utc();
395 let result = subtract_n_years(datetime, n)?;
396 let timestamp = match result.timestamp_nanos_opt() {
397 Some(ts) => ts,
398 None => anyhow::bail!("Timestamp out of range after subtracting {n} years"),
399 };
400
401 if timestamp < 0 {
402 anyhow::bail!("Negative timestamp not allowed");
403 }
404
405 Ok(UnixNanos::from(timestamp as u64))
406}
407
408#[must_use]
410pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
411 assert!(month >= 1 && month <= 12, "`month` must be in 1..=12");
413
414 match month {
416 2 => {
417 if is_leap_year(year) {
418 29
419 } else {
420 28
421 }
422 }
423 4 | 6 | 9 | 11 => 30,
424 _ => 31, }
426}
427
428#[must_use]
430pub const fn is_leap_year(year: i32) -> bool {
431 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
432}
433
434#[cfg(test)]
438#[allow(clippy::float_cmp)]
439mod tests {
440 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
441 use rstest::rstest;
442
443 use super::*;
444
445 #[rstest]
446 #[case(0.0, 0)]
447 #[case(1.0, 1_000_000_000)]
448 #[case(1.1, 1_100_000_000)]
449 #[case(42.0, 42_000_000_000)]
450 #[case(0.000_123_5, 123_500)]
451 #[case(0.000_000_01, 10)]
452 #[case(0.000_000_001, 1)]
453 #[case(9.999_999_999, 9_999_999_999)]
454 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
455 let result = secs_to_nanos(value);
456 assert_eq!(result, expected);
457 }
458
459 #[rstest]
460 #[case(0.0, 0)]
461 #[case(1.0, 1_000)]
462 #[case(1.1, 1_100)]
463 #[case(42.0, 42_000)]
464 #[case(0.012_34, 12)]
465 #[case(0.001, 1)]
466 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
467 let result = secs_to_millis(value);
468 assert_eq!(result, expected);
469 }
470
471 #[rstest]
472 #[should_panic(expected = "`month` must be in 1..=12")]
473 fn test_last_day_of_month_invalid_month() {
474 let _ = last_day_of_month(2024, 0);
475 }
476
477 #[rstest]
478 #[case(0.0, 0)]
479 #[case(1.0, 1_000_000)]
480 #[case(1.1, 1_100_000)]
481 #[case(42.0, 42_000_000)]
482 #[case(0.000_123_4, 123)]
483 #[case(0.000_01, 10)]
484 #[case(0.000_001, 1)]
485 #[case(9.999_999, 9_999_999)]
486 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
487 let result = millis_to_nanos(value);
488 assert_eq!(result, expected);
489 }
490
491 #[rstest]
492 #[case(0.0, 0)]
493 #[case(1.0, 1_000)]
494 #[case(1.1, 1_100)]
495 #[case(42.0, 42_000)]
496 #[case(0.1234, 123)]
497 #[case(0.01, 10)]
498 #[case(0.001, 1)]
499 #[case(9.999, 9_999)]
500 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
501 let result = micros_to_nanos(value);
502 assert_eq!(result, expected);
503 }
504
505 #[rstest]
506 #[case(0, 0.0)]
507 #[case(1, 1e-09)]
508 #[case(1_000_000_000, 1.0)]
509 #[case(42_897_123_111, 42.897_123_111)]
510 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
511 let result = nanos_to_secs(value);
512 assert_eq!(result, expected);
513 }
514
515 #[rstest]
516 #[case(0, 0)]
517 #[case(1_000_000, 1)]
518 #[case(1_000_000_000, 1000)]
519 #[case(42_897_123_111, 42897)]
520 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
521 let result = nanos_to_millis(value);
522 assert_eq!(result, expected);
523 }
524
525 #[rstest]
526 #[case(0, 0)]
527 #[case(1_000, 1)]
528 #[case(1_000_000_000, 1_000_000)]
529 #[case(42_897_123, 42_897)]
530 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
531 let result = nanos_to_micros(value);
532 assert_eq!(result, expected);
533 }
534
535 #[rstest]
536 #[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) {
543 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
544 assert_eq!(result, expected);
545 }
546
547 #[rstest]
548 #[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) {
553 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
554 assert_eq!(result, expected);
555 }
556
557 #[rstest]
558 #[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(
563 #[case] year: i32,
564 #[case] month: u32,
565 #[case] day: u32,
566 #[case] expected: u64,
567 ) {
568 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
569 assert_eq!(result, expected);
570 }
571
572 #[rstest]
573 fn test_last_closest_weekday_nanos_with_invalid_date() {
574 let result = last_weekday_nanos(2023, 4, 31);
575 assert!(result.is_err());
576 }
577
578 #[rstest]
579 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
580 let result = last_weekday_nanos(2023, 2, 30);
581 assert!(result.is_err());
582 }
583
584 #[rstest]
585 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
586 let result = last_weekday_nanos(9999, 12, 31);
587 assert!(result.is_err());
588 }
589
590 #[rstest]
591 fn test_is_within_last_24_hours_when_now() {
592 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
593 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
594 }
595
596 #[rstest]
597 fn test_is_within_last_24_hours_when_two_days_ago() {
598 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
599 .timestamp_nanos_opt()
600 .unwrap();
601 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
602 }
603
604 #[rstest]
605 fn test_is_within_last_24_hours_when_future() {
606 let future_ns = (Utc::now() + TimeDelta::try_hours(1).unwrap())
608 .timestamp_nanos_opt()
609 .unwrap();
610 assert!(!is_within_last_24_hours(UnixNanos::from(future_ns as u64)).unwrap());
611
612 let future_ns = (Utc::now() + TimeDelta::try_days(1).unwrap())
614 .timestamp_nanos_opt()
615 .unwrap();
616 assert!(!is_within_last_24_hours(UnixNanos::from(future_ns as u64)).unwrap());
617 }
618
619 #[rstest]
620 #[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(
625 #[case] input: DateTime<Utc>,
626 #[case] months: u32,
627 #[case] expected: DateTime<Utc>,
628 ) {
629 let result = subtract_n_months(input, months).unwrap();
630 assert_eq!(result, expected);
631 }
632
633 #[rstest]
634 #[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(
639 #[case] input: DateTime<Utc>,
640 #[case] months: u32,
641 #[case] expected: DateTime<Utc>,
642 ) {
643 let result = add_n_months(input, months).unwrap();
644 assert_eq!(result, expected);
645 }
646
647 #[rstest]
648 fn test_add_n_years_overflow() {
649 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
650 let err = add_n_years(datetime, u32::MAX).unwrap_err();
651 assert!(err.to_string().contains("month count overflow"));
652 }
653
654 #[rstest]
655 fn test_subtract_n_years_overflow() {
656 let datetime = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
657 let err = subtract_n_years(datetime, u32::MAX).unwrap_err();
658 assert!(err.to_string().contains("month count overflow"));
659 }
660
661 #[rstest]
662 fn test_add_n_years_nanos_overflow() {
663 let nanos = UnixNanos::from(0);
664 let err = add_n_years_nanos(nanos, u32::MAX).unwrap_err();
665 assert!(err.to_string().contains("month count overflow"));
666 }
667
668 #[rstest]
669 #[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) {
674 let result = last_day_of_month(year, month);
675 assert_eq!(result, expected);
676 }
677
678 #[rstest]
679 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
684 let result = is_leap_year(year);
685 assert_eq!(result, expected);
686 }
687
688 #[rstest]
689 #[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) {
698 let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
699 assert_eq!(result.as_u64(), expected);
700 }
701
702 #[rstest]
703 #[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) {
708 let result = iso8601_to_unix_nanos(input.to_string());
709 assert!(result.is_err());
710 }
711
712 #[rstest]
713 fn test_iso8601_roundtrip() {
714 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
715 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
716 let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
717 assert_eq!(parsed_nanos, original_nanos);
718 }
719
720 #[rstest]
721 fn test_add_n_years_nanos_normal_case() {
722 let start = UnixNanos::from(Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap());
724 let result = add_n_years_nanos(start, 1).unwrap();
725 let expected = UnixNanos::from(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap());
726 assert_eq!(result, expected);
727 }
728
729 #[rstest]
730 fn test_add_n_years_nanos_prevents_negative_timestamp() {
731 let start = UnixNanos::from(0); let result = add_n_years_nanos(start, 1);
737 assert!(result.is_ok());
738 }
739}