1use std::{convert::TryFrom, sync::LazyLock};
18
19use jiff::{
20 Span, Timestamp,
21 civil::{Date, Weekday},
22 tz::{TimeZone, TimeZoneDatabase},
23};
24
25use crate::{UnixNanos, time::nanos_since_unix_epoch};
26
27pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
29
30pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
32
33pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
35const NANOSECONDS_IN_MILLISECOND_U32: u32 = 1_000_000;
36
37pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
39
40pub const NANOSECONDS_IN_MINUTE: u64 = 60 * NANOSECONDS_IN_SECOND;
42
43pub const NANOSECONDS_IN_DAY: u64 = 24 * 60 * NANOSECONDS_IN_MINUTE;
45
46pub const SECONDS_IN_MINUTE: u64 = 60;
48
49pub const SECONDS_IN_HOUR: u64 = 60 * SECONDS_IN_MINUTE;
51
52pub const SECONDS_IN_DAY: u64 = 24 * SECONDS_IN_HOUR;
54
55#[expect(
56 clippy::cast_precision_loss,
57 reason = "u64::MAX rounds to the exact exclusive 2^64 upper bound"
58)]
59pub(crate) const U64_UPPER_BOUND_F64: f64 = u64::MAX as f64;
60
61static BUNDLED_TIME_ZONE_DATABASE: LazyLock<TimeZoneDatabase> =
62 LazyLock::new(TimeZoneDatabase::bundled);
63
64const _: () = {
66 assert!(NANOSECONDS_IN_SECOND == 1_000_000_000);
67 assert!(NANOSECONDS_IN_MILLISECOND == 1_000_000);
68 assert!(NANOSECONDS_IN_MICROSECOND == 1_000);
69 assert!(MILLISECONDS_IN_SECOND == 1_000);
70 assert!(NANOSECONDS_IN_SECOND == MILLISECONDS_IN_SECOND * NANOSECONDS_IN_MILLISECOND);
71 assert!(NANOSECONDS_IN_MILLISECOND == NANOSECONDS_IN_MICROSECOND * 1_000);
72 assert!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MILLISECOND == 1_000);
73 assert!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MICROSECOND == 1_000_000);
74 assert!(SECONDS_IN_MINUTE == 60);
75 assert!(SECONDS_IN_HOUR == 3_600);
76 assert!(SECONDS_IN_DAY == 86_400);
77 assert!(NANOSECONDS_IN_MINUTE == 60 * NANOSECONDS_IN_SECOND);
78 assert!(NANOSECONDS_IN_DAY == 24 * 60 * NANOSECONDS_IN_MINUTE);
79};
80
81pub fn get_timezone(name: &str) -> Result<TimeZone, jiff::Error> {
90 BUNDLED_TIME_ZONE_DATABASE.get(name)
91}
92
93fn civil_from_days(days_since_epoch: i64) -> (i32, u32, u32) {
94 let z = days_since_epoch + 719_468;
98 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
99 let day_of_era = z - era * 146_097;
100 let year_of_era =
101 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
102 let year = year_of_era + era * 400;
103 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
104 let month_prime = (5 * day_of_year + 2) / 153;
105 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
106 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
107 let year = year + i64::from(month <= 2);
108
109 (
110 i32::try_from(year).expect("year fits in i32"),
111 u32::try_from(month).expect("month is positive"),
112 u32::try_from(day).expect("day is positive"),
113 )
114}
115
116struct DateTimeParts {
117 year: i32,
118 month: u32,
119 day: u32,
120 hour: u32,
121 minute: u32,
122 second: u32,
123 subsec_nanos: u32,
124}
125
126#[expect(
127 clippy::cast_possible_truncation,
128 reason = "digit helpers only receive values in 0..=9"
129)]
130fn push_digit(out: &mut String, digit: u32) {
131 out.push(char::from(b'0' + digit as u8));
132}
133
134fn push_2_digits(out: &mut String, value: u32) {
135 debug_assert!(value < 100);
136 push_digit(out, value / 10);
137 push_digit(out, value % 10);
138}
139
140fn push_3_digits(out: &mut String, value: u32) {
141 debug_assert!(value < 1_000);
142 push_digit(out, value / 100);
143 push_2_digits(out, value % 100);
144}
145
146fn push_4_digits(out: &mut String, value: i32) {
147 debug_assert!((0..=9_999).contains(&value));
148 let value = u32::try_from(value).expect("year is non-negative");
149 push_digit(out, value / 1_000);
150 push_digit(out, (value / 100) % 10);
151 push_2_digits(out, value % 100);
152}
153
154fn push_9_digits(out: &mut String, value: u32) {
155 debug_assert!(value < 1_000_000_000);
156 let mut divisor = 100_000_000;
157 while divisor > 0 {
158 push_digit(out, value / divisor % 10);
159 divisor /= 10;
160 }
161}
162
163fn split_unix_nanos(unix_nanos: UnixNanos) -> DateTimeParts {
164 let nanos = unix_nanos.as_u64();
165 let total_seconds = nanos / NANOSECONDS_IN_SECOND;
166 let subsec_nanos = u32::try_from(nanos % NANOSECONDS_IN_SECOND).expect("subsecond fits u32");
167 let days = total_seconds / SECONDS_IN_DAY;
168 let seconds_of_day = total_seconds % SECONDS_IN_DAY;
169 let (year, month, day) =
170 civil_from_days(i64::try_from(days).expect("days since epoch fits i64"));
171 let hour = u32::try_from(seconds_of_day / SECONDS_IN_HOUR).expect("hour fits u32");
172 let minute =
173 u32::try_from((seconds_of_day % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE).expect("minute fits");
174 let second = u32::try_from(seconds_of_day % SECONDS_IN_MINUTE).expect("second fits");
175
176 DateTimeParts {
177 year,
178 month,
179 day,
180 hour,
181 minute,
182 second,
183 subsec_nanos,
184 }
185}
186
187fn push_iso8601_prefix(
188 out: &mut String,
189 year: i32,
190 month: u32,
191 day: u32,
192 hour: u32,
193 minute: u32,
194 second: u32,
195) {
196 push_4_digits(out, year);
197 out.push('-');
198 push_2_digits(out, month);
199 out.push('-');
200 push_2_digits(out, day);
201 out.push('T');
202 push_2_digits(out, hour);
203 out.push(':');
204 push_2_digits(out, minute);
205 out.push(':');
206 push_2_digits(out, second);
207 out.push('.');
208}
209
210pub const WEEKDAYS: [Weekday; 5] = [
212 Weekday::Monday,
213 Weekday::Tuesday,
214 Weekday::Wednesday,
215 Weekday::Thursday,
216 Weekday::Friday,
217];
218
219#[expect(
225 clippy::cast_possible_truncation,
226 clippy::cast_sign_loss,
227 clippy::cast_precision_loss,
228 reason = "Intentional for unit conversion, may lose precision after clamping"
229)]
230pub fn secs_to_nanos(secs: f64) -> anyhow::Result<u64> {
231 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
232 if secs <= 0.0 {
233 return Ok(0);
234 }
235 let nanos = secs * NANOSECONDS_IN_SECOND as f64;
236 anyhow::ensure!(
237 nanos < U64_UPPER_BOUND_F64,
238 "seconds {secs} is out of range for `u64` nanoseconds"
239 );
240 Ok(nanos.trunc() as u64)
241}
242
243#[expect(
249 clippy::cast_possible_truncation,
250 clippy::cast_sign_loss,
251 clippy::cast_precision_loss,
252 reason = "Intentional for unit conversion, may lose precision after clamping"
253)]
254pub fn secs_to_millis(secs: f64) -> anyhow::Result<u64> {
255 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
256 if secs <= 0.0 {
257 return Ok(0);
258 }
259 let millis = secs * MILLISECONDS_IN_SECOND as f64;
260 anyhow::ensure!(
261 millis < U64_UPPER_BOUND_F64,
262 "seconds {secs} is out of range for `u64` milliseconds"
263 );
264 Ok(millis.trunc() as u64)
265}
266
267#[must_use]
276pub fn secs_to_nanos_unchecked(secs: f64) -> u64 {
277 secs_to_nanos(secs).expect("secs_to_nanos_unchecked: invalid or overflowing input")
278}
279
280#[must_use]
286pub const fn mins_to_secs(mins: u64) -> u64 {
287 checked_mins_to_secs(mins).expect("minutes to seconds conversion overflow")
288}
289
290#[must_use]
292pub const fn checked_mins_to_secs(mins: u64) -> Option<u64> {
293 mins.checked_mul(SECONDS_IN_MINUTE)
294}
295
296#[must_use]
302pub const fn mins_to_nanos(mins: u64) -> u64 {
303 checked_mins_to_nanos(mins).expect("minutes to nanoseconds conversion overflow")
304}
305
306#[must_use]
308pub const fn checked_mins_to_nanos(mins: u64) -> Option<u64> {
309 mins.checked_mul(NANOSECONDS_IN_MINUTE)
310}
311
312#[expect(
321 clippy::cast_possible_truncation,
322 clippy::cast_sign_loss,
323 clippy::cast_precision_loss,
324 reason = "Intentional for unit conversion, may lose precision after clamping"
325)]
326pub fn millis_to_nanos(millis: f64) -> anyhow::Result<u64> {
327 anyhow::ensure!(
328 millis.is_finite(),
329 "milliseconds must be finite, was {millis}"
330 );
331
332 if millis <= 0.0 {
333 return Ok(0);
334 }
335 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
336 anyhow::ensure!(
337 nanos < U64_UPPER_BOUND_F64,
338 "milliseconds {millis} is out of range for `u64` nanoseconds"
339 );
340 Ok(nanos.trunc() as u64)
341}
342
343#[must_use]
349pub fn millis_to_nanos_unchecked(millis: f64) -> u64 {
350 millis_to_nanos(millis).expect("millis_to_nanos_unchecked: invalid or overflowing input")
351}
352
353#[expect(
362 clippy::cast_possible_truncation,
363 clippy::cast_sign_loss,
364 clippy::cast_precision_loss,
365 reason = "Intentional for unit conversion, may lose precision after clamping"
366)]
367pub fn micros_to_nanos(micros: f64) -> anyhow::Result<u64> {
368 anyhow::ensure!(
369 micros.is_finite(),
370 "microseconds must be finite, was {micros}"
371 );
372
373 if micros <= 0.0 {
374 return Ok(0);
375 }
376 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
377 anyhow::ensure!(
378 nanos < U64_UPPER_BOUND_F64,
379 "microseconds {micros} is out of range for `u64` nanoseconds"
380 );
381 Ok(nanos.trunc() as u64)
382}
383
384#[must_use]
390pub fn micros_to_nanos_unchecked(micros: f64) -> u64 {
391 micros_to_nanos(micros).expect("micros_to_nanos_unchecked: invalid or overflowing input")
392}
393
394#[expect(
399 clippy::cast_precision_loss,
400 reason = "Precision loss acceptable for time conversion"
401)]
402#[must_use]
403pub fn nanos_to_secs(nanos: u64) -> f64 {
404 let seconds = nanos / NANOSECONDS_IN_SECOND;
405 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
406 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
407}
408
409#[must_use]
411pub const fn nanos_to_millis(nanos: u64) -> u64 {
412 nanos / NANOSECONDS_IN_MILLISECOND
413}
414
415#[must_use]
417pub const fn nanos_to_micros(nanos: u64) -> u64 {
418 nanos / NANOSECONDS_IN_MICROSECOND
419}
420
421#[inline]
425#[must_use]
426pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
427 let parts = split_unix_nanos(unix_nanos);
428
429 let mut out = String::with_capacity(30);
430 push_iso8601_prefix(
431 &mut out,
432 parts.year,
433 parts.month,
434 parts.day,
435 parts.hour,
436 parts.minute,
437 parts.second,
438 );
439 push_9_digits(&mut out, parts.subsec_nanos);
440 out.push('Z');
441 out
442}
443
444#[inline]
467pub fn iso8601_to_unix_nanos(date_string: &str) -> anyhow::Result<UnixNanos> {
468 date_string
469 .parse::<UnixNanos>()
470 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{date_string}': {e}"))
471}
472
473#[inline]
478#[must_use]
479pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
480 let parts = split_unix_nanos(unix_nanos);
481
482 let mut out = String::with_capacity(24);
483 push_iso8601_prefix(
484 &mut out,
485 parts.year,
486 parts.month,
487 parts.day,
488 parts.hour,
489 parts.minute,
490 parts.second,
491 );
492 push_3_digits(
493 &mut out,
494 parts.subsec_nanos / NANOSECONDS_IN_MILLISECOND_U32,
495 );
496 out.push('Z');
497 out
498}
499
500#[must_use]
502pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
503 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
504}
505
506pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
512 let date = Date::new(
513 i16::try_from(year).map_err(|_| anyhow::anyhow!("Invalid date"))?,
514 i8::try_from(month).map_err(|_| anyhow::anyhow!("Invalid date"))?,
515 i8::try_from(day).map_err(|_| anyhow::anyhow!("Invalid date"))?,
516 )
517 .map_err(|_| anyhow::anyhow!("Invalid date"))?;
518 let current_weekday = date.weekday().to_monday_one_offset();
519
520 let offset = match current_weekday {
522 1..=5 => 0, 6 => 1, _ => 2, };
526 let last_closest = date.checked_sub(Span::new().days(offset))?;
528
529 let unix_timestamp_ns = last_closest
531 .at(0, 0, 0, 0)
532 .to_zoned(TimeZone::UTC)?
533 .timestamp()
534 .as_nanosecond();
535
536 let ns_u64 = u64::try_from(unix_timestamp_ns)
537 .map_err(|_| anyhow::anyhow!("Negative timestamp: {unix_timestamp_ns}"))?;
538 Ok(UnixNanos::from(ns_u64))
539}
540
541pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
547 let timestamp_ns = timestamp_ns.as_u64();
551 let now_ns = nanos_since_unix_epoch();
552
553 if timestamp_ns > now_ns {
555 return Ok(false);
556 }
557
558 Ok(now_ns - timestamp_ns <= NANOSECONDS_IN_DAY)
559}
560
561fn shift_months(datetime: Timestamp, months: i64) -> anyhow::Result<Timestamp> {
562 let span = Span::new().try_months(months)?;
563 let result = datetime.to_zoned(TimeZone::UTC).checked_add(span)?;
564 Ok(result.timestamp())
565}
566
567pub fn subtract_n_months(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
573 shift_months(datetime, -i64::from(n))
574 .map_err(|_| anyhow::anyhow!("Failed to subtract {n} months from {datetime}"))
575}
576
577pub fn add_n_months(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
583 shift_months(datetime, i64::from(n))
584 .map_err(|_| anyhow::anyhow!("Failed to add {n} months to {datetime}"))
585}
586
587pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
593 let datetime = unix_nanos.to_datetime_utc();
594 let result = subtract_n_months(datetime, n)?;
595 let timestamp = result.as_nanosecond();
596
597 let nanos =
598 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
599 Ok(UnixNanos::from(nanos))
600}
601
602pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
608 let datetime = unix_nanos.to_datetime_utc();
609 let result = add_n_months(datetime, n)?;
610 let timestamp = result.as_nanosecond();
611
612 let nanos =
613 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
614 Ok(UnixNanos::from(nanos))
615}
616
617pub fn add_n_years(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
623 let months = n.checked_mul(12).ok_or_else(|| {
624 anyhow::anyhow!("Failed to add {n} years to {datetime}: month count overflow")
625 })?;
626
627 shift_months(datetime, i64::from(months))
628 .map_err(|_| anyhow::anyhow!("Failed to add {n} years to {datetime}"))
629}
630
631pub fn subtract_n_years(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
637 let months = n.checked_mul(12).ok_or_else(|| {
638 anyhow::anyhow!("Failed to subtract {n} years from {datetime}: month count overflow")
639 })?;
640
641 shift_months(datetime, -i64::from(months))
642 .map_err(|_| anyhow::anyhow!("Failed to subtract {n} years from {datetime}"))
643}
644
645pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
651 let datetime = unix_nanos.to_datetime_utc();
652 let result = add_n_years(datetime, n)?;
653 let timestamp = result.as_nanosecond();
654
655 let nanos =
656 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
657 Ok(UnixNanos::from(nanos))
658}
659
660pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
666 let datetime = unix_nanos.to_datetime_utc();
667 let result = subtract_n_years(datetime, n)?;
668 let timestamp = result.as_nanosecond();
669
670 let nanos =
671 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
672 Ok(UnixNanos::from(nanos))
673}
674
675pub fn datetime_to_unix_nanos(value: Option<Timestamp>) -> Option<UnixNanos> {
677 value
678 .map(Timestamp::as_nanosecond)
679 .and_then(|nanos| u64::try_from(nanos).ok())
680 .map(UnixNanos::from)
681}
682
683pub fn try_datetime_to_unix_nanos(value: Timestamp) -> anyhow::Result<UnixNanos> {
691 let nanos = value.as_nanosecond();
692
693 if nanos < 0 {
694 anyhow::bail!("DateTime timestamp cannot be negative: {nanos}");
695 }
696 let nanos = u64::try_from(nanos)
697 .map_err(|_| anyhow::anyhow!("DateTime timestamp out of range for UnixNanos: {nanos}"))?;
698
699 Ok(UnixNanos::from(nanos))
700}
701
702#[cfg(test)]
703#[allow(
705 clippy::float_cmp,
706 reason = "Exact float comparisons acceptable in tests"
707)]
708mod tests {
709 use jiff::SignedDuration;
710 use proptest::prelude::*;
711 use rstest::rstest;
712
713 use super::*;
714
715 fn timestamp(value: &str) -> Timestamp {
716 value.parse().unwrap()
717 }
718
719 #[rstest]
720 #[case(0.0, 0)]
721 #[case(1.0, 1_000_000_000)]
722 #[case(1.1, 1_100_000_000)]
723 #[case(42.0, 42_000_000_000)]
724 #[case(0.000_123_5, 123_500)]
725 #[case(0.000_000_01, 10)]
726 #[case(0.000_000_001, 1)]
727 #[case(9.999_999_999, 9_999_999_999)]
728 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
729 let result = secs_to_nanos(value).unwrap();
730 assert_eq!(result, expected);
731 }
732
733 #[rstest]
734 #[case(0.0, 0)]
735 #[case(1.0, 1_000)]
736 #[case(1.1, 1_100)]
737 #[case(42.0, 42_000)]
738 #[case(0.012_34, 12)]
739 #[case(0.001, 1)]
740 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
741 let result = secs_to_millis(value).unwrap();
742 assert_eq!(result, expected);
743 }
744
745 #[rstest]
746 fn test_secs_to_nanos_unchecked_matches_checked() {
747 assert_eq!(secs_to_nanos_unchecked(1.1), secs_to_nanos(1.1).unwrap());
748 }
749
750 #[rstest]
751 fn test_secs_to_nanos_non_finite_errors() {
752 let err = secs_to_nanos(f64::NAN).unwrap_err();
753 assert!(err.to_string().contains("finite"));
754 }
755
756 #[rstest]
757 fn test_secs_to_millis_non_finite_errors() {
758 let err = secs_to_millis(f64::INFINITY).unwrap_err();
759 assert!(err.to_string().contains("finite"));
760 }
761
762 #[rstest]
763 fn test_millis_to_nanos_non_finite_errors() {
764 let err = millis_to_nanos(f64::NEG_INFINITY).unwrap_err();
765 assert!(err.to_string().contains("finite"));
766 }
767
768 #[rstest]
769 fn test_micros_to_nanos_non_finite_errors() {
770 let err = micros_to_nanos(f64::NAN).unwrap_err();
771 assert!(err.to_string().contains("finite"));
772 }
773
774 #[rstest]
775 #[case(0, 0)]
776 #[case(1, 60)]
777 #[case(5, 300)]
778 #[case(60, 3600)]
779 #[case(1440, 86400)]
780 fn test_mins_to_secs(#[case] mins: u64, #[case] expected: u64) {
781 assert_eq!(mins_to_secs(mins), expected);
782 }
783
784 #[rstest]
785 #[case(0, 0)]
786 #[case(1, 60_000_000_000)]
787 #[case(5, 300_000_000_000)]
788 #[case(60, 3_600_000_000_000)]
789 fn test_mins_to_nanos(#[case] mins: u64, #[case] expected: u64) {
790 assert_eq!(mins_to_nanos(mins), expected);
791 }
792
793 #[rstest]
794 #[case(
795 checked_mins_to_secs,
796 307_445_734_561_825_860,
797 18_446_744_073_709_551_600
798 )]
799 #[case(checked_mins_to_nanos, 307_445_734, 18_446_744_040_000_000_000)]
800 fn test_checked_minutes_conversion_boundary(
801 #[case] convert: fn(u64) -> Option<u64>,
802 #[case] max: u64,
803 #[case] expected: u64,
804 ) {
805 assert_eq!(convert(max), Some(expected));
806 assert_eq!(convert(max + 1), None);
807 }
808
809 #[rstest]
810 #[should_panic(expected = "minutes to seconds conversion overflow")]
811 fn test_mins_to_secs_overflow_panics() {
812 let _ = mins_to_secs(307_445_734_561_825_861);
813 }
814
815 #[rstest]
816 #[should_panic(expected = "minutes to nanoseconds conversion overflow")]
817 fn test_mins_to_nanos_overflow_panics() {
818 let _ = mins_to_nanos(307_445_735);
819 }
820
821 #[rstest]
822 #[case(
823 secs_to_nanos,
824 18_446_744_073.709_553,
825 18_446_744_073.709_55,
826 18_446_744_073_709_549_568
827 )]
828 #[case(
829 secs_to_millis,
830 18_446_744_073_709_550.0,
831 18_446_744_073_709_548.0,
832 18_446_744_073_709_547_520
833 )]
834 #[case(
835 millis_to_nanos,
836 18_446_744_073_709.55,
837 18_446_744_073_709.547,
838 18_446_744_073_709_547_520
839 )]
840 #[case(
841 micros_to_nanos,
842 18_446_744_073_709_550.0,
843 18_446_744_073_709_548.0,
844 18_446_744_073_709_547_520
845 )]
846 fn test_float_conversion_u64_boundary(
847 #[case] convert: fn(f64) -> anyhow::Result<u64>,
848 #[case] invalid: f64,
849 #[case] previous: f64,
850 #[case] expected: u64,
851 ) {
852 let err = convert(invalid).unwrap_err();
853 assert!(err.to_string().contains("out of range"));
854 assert_eq!(convert(previous).unwrap(), expected);
855 }
856
857 #[rstest]
858 fn test_secs_to_nanos_negative_infinity_errors() {
859 let result = secs_to_nanos(f64::NEG_INFINITY);
860 assert!(result.is_err());
861 }
862
863 #[rstest]
864 #[case(0.0, 0)]
865 #[case(1.0, 1_000_000)]
866 #[case(1.1, 1_100_000)]
867 #[case(42.0, 42_000_000)]
868 #[case(0.000_123_4, 123)]
869 #[case(0.000_01, 10)]
870 #[case(0.000_001, 1)]
871 #[case(9.999_999, 9_999_999)]
872 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
873 let result = millis_to_nanos(value).unwrap();
874 assert_eq!(result, expected);
875 }
876
877 #[rstest]
878 fn test_millis_to_nanos_unchecked_matches_checked() {
879 assert_eq!(
880 millis_to_nanos_unchecked(1.1),
881 millis_to_nanos(1.1).unwrap()
882 );
883 }
884
885 #[rstest]
886 #[case(0.0, 0)]
887 #[case(1.0, 1_000)]
888 #[case(1.1, 1_100)]
889 #[case(42.0, 42_000)]
890 #[case(0.1234, 123)]
891 #[case(0.01, 10)]
892 #[case(0.001, 1)]
893 #[case(9.999, 9_999)]
894 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
895 let result = micros_to_nanos(value).unwrap();
896 assert_eq!(result, expected);
897 }
898
899 #[rstest]
900 fn test_micros_to_nanos_unchecked_matches_checked() {
901 assert_eq!(
902 micros_to_nanos_unchecked(1.1),
903 micros_to_nanos(1.1).unwrap()
904 );
905 }
906
907 #[rstest]
908 #[case(0, 0.0)]
909 #[case(1, 1e-09)]
910 #[case(1_000_000_000, 1.0)]
911 #[case(42_897_123_111, 42.897_123_111)]
912 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
913 let result = nanos_to_secs(value);
914 assert_eq!(result, expected);
915 }
916
917 #[rstest]
918 #[case(0, 0)]
919 #[case(1_000_000, 1)]
920 #[case(1_000_000_000, 1000)]
921 #[case(42_897_123_111, 42897)]
922 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
923 let result = nanos_to_millis(value);
924 assert_eq!(result, expected);
925 }
926
927 #[rstest]
928 #[case(0, 0)]
929 #[case(1_000, 1)]
930 #[case(1_000_000_000, 1_000_000)]
931 #[case(42_897_123, 42_897)]
932 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
933 let result = nanos_to_micros(value);
934 assert_eq!(result, expected);
935 }
936
937 #[rstest]
938 #[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(951_782_400_000_000_000, "2000-02-29T00:00:00.000000000Z")] #[case(1_609_459_199_999_999_999, "2020-12-31T23:59:59.999999999Z")] #[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) {
947 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
948 assert_eq!(result, expected);
949 }
950
951 #[rstest]
952 #[case(0)]
953 #[case(1)]
954 #[case(951_782_400_123_456_789)]
955 #[case(1_609_459_199_999_999_999)]
956 #[case(i64::MAX as u64)]
957 fn test_unix_nanos_to_iso8601_matches_jiff_oracle(#[case] nanos: u64) {
958 let expected = format!(
959 "{:.9}",
960 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
961 );
962 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
963 assert_eq!(result, expected);
964 }
965
966 #[rstest]
967 #[case((i64::MAX as u64) + 1)]
968 #[case(u64::MAX)]
969 fn test_unix_nanos_to_iso8601_supports_full_unix_nanos_range(#[case] nanos: u64) {
970 let expected = format!(
971 "{:.9}",
972 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
973 );
974 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
975 assert_eq!(result, expected);
976 }
977
978 #[rstest]
979 #[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(951_782_400_123_456_789, "2000-02-29T00:00:00.123Z")] #[case(1_609_459_199_999_999_999, "2020-12-31T23:59:59.999Z")] #[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) {
986 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
987 assert_eq!(result, expected);
988 }
989
990 #[rstest]
991 #[case(0)]
992 #[case(951_782_400_123_456_789)]
993 #[case(1_609_459_199_999_999_999)]
994 #[case(i64::MAX as u64)]
995 fn test_unix_nanos_to_iso8601_millis_matches_jiff_oracle(#[case] nanos: u64) {
996 let expected = format!(
997 "{:.3}",
998 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
999 );
1000 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
1001 assert_eq!(result, expected);
1002 }
1003
1004 #[rstest]
1005 #[case((i64::MAX as u64) + 1)]
1006 #[case(u64::MAX)]
1007 fn test_unix_nanos_to_iso8601_millis_supports_full_unix_nanos_range(#[case] nanos: u64) {
1008 let expected = format!(
1009 "{:.3}",
1010 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
1011 );
1012 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
1013 assert_eq!(result, expected);
1014 }
1015
1016 proptest! {
1019 #[rstest]
1020 fn prop_unix_nanos_to_iso8601_matches_jiff(nanos in any::<u64>()) {
1021 let expected = format!(
1022 "{:.9}",
1023 Timestamp::from_nanosecond(i128::from(nanos)).unwrap(),
1024 );
1025 let actual = unix_nanos_to_iso8601(UnixNanos::from(nanos));
1026 prop_assert_eq!(actual, expected);
1027 }
1028
1029 #[rstest]
1030 fn prop_unix_nanos_to_iso8601_millis_matches_jiff(nanos in any::<u64>()) {
1031 let expected = format!(
1032 "{:.3}",
1033 Timestamp::from_nanosecond(i128::from(nanos)).unwrap(),
1034 );
1035 let actual = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
1036 prop_assert_eq!(actual, expected);
1037 }
1038 }
1039
1040 #[rstest]
1041 #[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(
1046 #[case] year: i32,
1047 #[case] month: u32,
1048 #[case] day: u32,
1049 #[case] expected: u64,
1050 ) {
1051 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
1052 assert_eq!(result, expected);
1053 }
1054
1055 #[rstest]
1056 fn test_last_closest_weekday_nanos_with_invalid_date() {
1057 let result = last_weekday_nanos(2023, 4, 31);
1058 assert!(result.is_err());
1059 }
1060
1061 #[rstest]
1062 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
1063 let result = last_weekday_nanos(2023, 2, 30);
1064 assert!(result.is_err());
1065 }
1066
1067 #[rstest]
1068 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
1069 let result = last_weekday_nanos(9999, 12, 31);
1070 assert!(result.is_err());
1071 }
1072
1073 #[rstest]
1074 fn test_is_within_last_24_hours_when_now() {
1075 let now_ns = Timestamp::now().as_nanosecond();
1076 assert!(is_within_last_24_hours(UnixNanos::from(u64::try_from(now_ns).unwrap())).unwrap());
1077 }
1078
1079 #[rstest]
1080 fn test_is_within_last_24_hours_when_two_days_ago() {
1081 let past_ns = (Timestamp::now() - SignedDuration::from_hours(48)).as_nanosecond();
1082 assert!(
1083 !is_within_last_24_hours(UnixNanos::from(u64::try_from(past_ns).unwrap())).unwrap()
1084 );
1085 }
1086
1087 #[rstest]
1088 fn test_is_within_last_24_hours_when_future() {
1089 let future_ns = (Timestamp::now() + SignedDuration::from_hours(1)).as_nanosecond();
1091 assert!(
1092 !is_within_last_24_hours(UnixNanos::from(u64::try_from(future_ns).unwrap())).unwrap()
1093 );
1094
1095 let future_ns = (Timestamp::now() + SignedDuration::from_hours(24)).as_nanosecond();
1097 assert!(
1098 !is_within_last_24_hours(UnixNanos::from(u64::try_from(future_ns).unwrap())).unwrap()
1099 );
1100 }
1101
1102 #[rstest]
1103 #[case(
1104 timestamp("2024-03-31T12:00:00Z"),
1105 1,
1106 timestamp("2024-02-29T12:00:00Z")
1107 )]
1108 #[case(
1109 timestamp("2024-03-31T12:00:00Z"),
1110 12,
1111 timestamp("2023-03-31T12:00:00Z")
1112 )]
1113 #[case(
1114 timestamp("2024-01-31T12:00:00Z"),
1115 1,
1116 timestamp("2023-12-31T12:00:00Z")
1117 )]
1118 #[case(
1119 timestamp("2024-03-31T12:00:00Z"),
1120 2,
1121 timestamp("2024-01-31T12:00:00Z")
1122 )]
1123 fn test_subtract_n_months(
1124 #[case] input: Timestamp,
1125 #[case] months: u32,
1126 #[case] expected: Timestamp,
1127 ) {
1128 let result = subtract_n_months(input, months).unwrap();
1129 assert_eq!(result, expected);
1130 }
1131
1132 #[rstest]
1133 #[case(
1134 timestamp("2023-02-28T12:00:00Z"),
1135 1,
1136 timestamp("2023-03-28T12:00:00Z")
1137 )]
1138 #[case(
1139 timestamp("2024-01-31T12:00:00Z"),
1140 1,
1141 timestamp("2024-02-29T12:00:00Z")
1142 )]
1143 #[case(
1144 timestamp("2023-12-31T12:00:00Z"),
1145 1,
1146 timestamp("2024-01-31T12:00:00Z")
1147 )]
1148 #[case(
1149 timestamp("2023-01-31T12:00:00Z"),
1150 13,
1151 timestamp("2024-02-29T12:00:00Z")
1152 )]
1153 fn test_add_n_months(
1154 #[case] input: Timestamp,
1155 #[case] months: u32,
1156 #[case] expected: Timestamp,
1157 ) {
1158 let result = add_n_months(input, months).unwrap();
1159 assert_eq!(result, expected);
1160 }
1161
1162 #[rstest]
1163 fn test_add_n_years_overflow() {
1164 let datetime = timestamp("2024-01-01T00:00:00Z");
1165 let err = add_n_years(datetime, u32::MAX).unwrap_err();
1166 assert!(err.to_string().contains("month count overflow"));
1167 }
1168
1169 #[rstest]
1170 fn test_subtract_n_years_overflow() {
1171 let datetime = timestamp("2024-01-01T00:00:00Z");
1172 let err = subtract_n_years(datetime, u32::MAX).unwrap_err();
1173 assert!(err.to_string().contains("month count overflow"));
1174 }
1175
1176 #[rstest]
1177 fn test_add_n_years_nanos_overflow() {
1178 let nanos = UnixNanos::from(0);
1179 let err = add_n_years_nanos(nanos, u32::MAX).unwrap_err();
1180 assert!(err.to_string().contains("month count overflow"));
1181 }
1182
1183 #[rstest]
1184 #[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) {
1193 let result = iso8601_to_unix_nanos(input).unwrap();
1194 assert_eq!(result.as_u64(), expected);
1195 }
1196
1197 #[rstest]
1198 #[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) {
1203 let result = iso8601_to_unix_nanos(input);
1204 assert!(result.is_err());
1205 }
1206
1207 #[rstest]
1208 fn test_iso8601_roundtrip() {
1209 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
1210 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
1211 let parsed_nanos = iso8601_to_unix_nanos(&iso8601_string).unwrap();
1212 assert_eq!(parsed_nanos, original_nanos);
1213 }
1214
1215 #[rstest]
1216 fn test_add_n_years_nanos_normal_case() {
1217 let start = UnixNanos::from(timestamp("2020-01-01T00:00:00Z"));
1219 let result = add_n_years_nanos(start, 1).unwrap();
1220 let expected = UnixNanos::from(timestamp("2021-01-01T00:00:00Z"));
1221 assert_eq!(result, expected);
1222 }
1223
1224 #[rstest]
1225 fn test_add_n_years_nanos_prevents_negative_timestamp() {
1226 let start = UnixNanos::from(0); let result = add_n_years_nanos(start, 1);
1232 assert!(result.is_ok());
1233 }
1234
1235 #[rstest]
1236 fn test_datetime_to_unix_nanos_at_epoch() {
1237 let epoch = Timestamp::UNIX_EPOCH;
1239 let result = datetime_to_unix_nanos(Some(epoch));
1240 assert_eq!(result, Some(UnixNanos::from(0)));
1241 }
1242
1243 #[rstest]
1244 fn test_datetime_to_unix_nanos_typical_datetime() {
1245 let dt = timestamp("2024-01-15T13:30:45.123456789Z");
1246 let result = datetime_to_unix_nanos(Some(dt));
1247
1248 assert!(result.is_some());
1250 assert_eq!(result.unwrap().as_u64(), 1_705_325_445_123_456_789);
1251 }
1252
1253 #[rstest]
1254 fn test_datetime_to_unix_nanos_before_epoch() {
1255 let before_epoch = timestamp("1969-12-31T23:59:59Z");
1258 let result = datetime_to_unix_nanos(Some(before_epoch));
1259 assert_eq!(result, None);
1260 }
1261
1262 #[rstest]
1263 fn test_datetime_to_unix_nanos_one_second_after_epoch() {
1264 let dt = Timestamp::from_second(1).unwrap();
1266 let result = datetime_to_unix_nanos(Some(dt));
1267 assert_eq!(result, Some(UnixNanos::from(1_000_000_000)));
1268 }
1269
1270 #[rstest]
1271 fn test_datetime_to_unix_nanos_with_subsecond_precision() {
1272 let dt = Timestamp::new(0, 1_000).unwrap(); let result = datetime_to_unix_nanos(Some(dt));
1275 assert_eq!(result, Some(UnixNanos::from(1_000)));
1276 }
1277
1278 #[rstest]
1279 fn test_try_datetime_to_unix_nanos_valid() {
1280 let dt = Timestamp::new(0, 1_000).unwrap();
1281 assert_eq!(
1282 try_datetime_to_unix_nanos(dt).unwrap(),
1283 UnixNanos::from(1_000)
1284 );
1285 }
1286
1287 #[rstest]
1288 fn test_try_datetime_to_unix_nanos_before_epoch_errors() {
1289 let before_epoch = timestamp("1969-12-31T23:59:59Z");
1290 let err = try_datetime_to_unix_nanos(before_epoch).unwrap_err();
1291 assert!(
1292 err.to_string().contains("cannot be negative"),
1293 "unexpected error: {err}"
1294 );
1295 }
1296
1297 #[rstest]
1298 fn test_try_datetime_to_unix_nanos_out_of_range_errors() {
1299 let err = try_datetime_to_unix_nanos(Timestamp::MAX).unwrap_err();
1300 assert!(
1301 err.to_string().contains("out of range"),
1302 "unexpected error: {err}"
1303 );
1304 }
1305
1306 #[rstest]
1307 fn test_nanos_helpers_support_values_above_i64_max() {
1308 let large = UnixNanos::from(u64::MAX);
1309 assert!(subtract_n_months_nanos(large, 1).is_ok());
1310 assert!(add_n_months_nanos(large, 1).is_err());
1311 assert!(add_n_years_nanos(large, 1).is_err());
1312 assert!(subtract_n_years_nanos(large, 1).is_ok());
1313 }
1314
1315 #[rstest]
1316 fn test_subtract_n_months_nanos_pre_epoch_result_errors() {
1317 let epoch = UnixNanos::from(0);
1318 let err = subtract_n_months_nanos(epoch, 1).unwrap_err();
1319 assert_eq!(err.to_string(), "Negative timestamp not allowed");
1320 }
1321
1322 #[rstest]
1323 fn test_subtract_n_years_nanos_pre_epoch_result_errors() {
1324 let epoch = UnixNanos::from(0);
1325 let err = subtract_n_years_nanos(epoch, 1).unwrap_err();
1326 assert_eq!(err.to_string(), "Negative timestamp not allowed");
1327 }
1328
1329 #[rstest]
1330 fn test_subtract_n_months_nanos_at_epoch_boundary() {
1331 let epoch = UnixNanos::from(0);
1332 assert_eq!(subtract_n_months_nanos(epoch, 0).unwrap(), epoch);
1333 }
1334}