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 = SECONDS_IN_MINUTE_U32 as u64;
48const SECONDS_IN_MINUTE_U32: u32 = 60;
49
50pub const SECONDS_IN_HOUR: u64 = SECONDS_IN_HOUR_U32 as u64;
52const SECONDS_IN_HOUR_U32: u32 = 60 * SECONDS_IN_MINUTE_U32;
53
54pub const SECONDS_IN_DAY: u64 = 24 * SECONDS_IN_HOUR;
56
57#[expect(
58 clippy::cast_precision_loss,
59 reason = "u64::MAX rounds to the exact exclusive 2^64 upper bound"
60)]
61pub(crate) const U64_UPPER_BOUND_F64: f64 = u64::MAX as f64;
62
63static BUNDLED_TIME_ZONE_DATABASE: LazyLock<TimeZoneDatabase> =
64 LazyLock::new(TimeZoneDatabase::bundled);
65
66const _: () = {
68 assert!(NANOSECONDS_IN_SECOND == 1_000_000_000);
69 assert!(NANOSECONDS_IN_MILLISECOND == 1_000_000);
70 assert!(NANOSECONDS_IN_MICROSECOND == 1_000);
71 assert!(MILLISECONDS_IN_SECOND == 1_000);
72 assert!(NANOSECONDS_IN_SECOND == MILLISECONDS_IN_SECOND * NANOSECONDS_IN_MILLISECOND);
73 assert!(NANOSECONDS_IN_MILLISECOND == NANOSECONDS_IN_MICROSECOND * 1_000);
74 assert!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MILLISECOND == 1_000);
75 assert!(NANOSECONDS_IN_SECOND / NANOSECONDS_IN_MICROSECOND == 1_000_000);
76 assert!(SECONDS_IN_MINUTE == 60);
77 assert!(SECONDS_IN_HOUR == 3_600);
78 assert!(SECONDS_IN_DAY == 86_400);
79 assert!(NANOSECONDS_IN_MINUTE == 60 * NANOSECONDS_IN_SECOND);
80 assert!(NANOSECONDS_IN_DAY == 24 * 60 * NANOSECONDS_IN_MINUTE);
81};
82
83pub fn get_timezone(name: &str) -> Result<TimeZone, jiff::Error> {
92 BUNDLED_TIME_ZONE_DATABASE.get(name)
93}
94
95fn civil_from_days(days_since_epoch: u32) -> (i32, u32, u32) {
96 let z = days_since_epoch + 719_468;
101 let era = z / 146_097;
102 let day_of_era = z - era * 146_097;
103 let year_of_era =
104 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
105 let year = year_of_era + era * 400;
106 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
107 let month_prime = (5 * day_of_year + 2) / 153;
108 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
109 let month = if month_prime < 10 {
110 month_prime + 3
111 } else {
112 month_prime - 9
113 };
114 let year = year + u32::from(month <= 2);
115
116 (i32::try_from(year).expect("year fits in i32"), month, day)
117}
118
119struct DateTimeParts {
120 year: i32,
121 month: u32,
122 day: u32,
123 hour: u32,
124 minute: u32,
125 second: u32,
126 subsec_nanos: u32,
127}
128
129#[expect(
130 clippy::cast_possible_truncation,
131 reason = "digit writers only receive values in 0..=9"
132)]
133fn push_digit(out: &mut String, digit: u32) {
134 out.push(char::from(b'0' + digit as u8));
135}
136
137const DIGIT_PAIRS: &str = concat!(
138 "0001020304050607080910111213141516171819",
139 "2021222324252627282930313233343536373839",
140 "4041424344454647484950515253545556575859",
141 "6061626364656667686970717273747576777879",
142 "8081828384858687888990919293949596979899",
143);
144
145fn push_2_digits(out: &mut String, value: u32) {
146 debug_assert!(value < 100);
147 let offset = value as usize * 2;
148 out.push_str(&DIGIT_PAIRS[offset..offset + 2]);
149}
150
151fn push_3_digits(out: &mut String, value: u32) {
152 debug_assert!(value < 1_000);
153 push_digit(out, value / 100);
154 push_2_digits(out, value % 100);
155}
156
157fn push_4_digits(out: &mut String, value: i32) {
158 debug_assert!((0..=9_999).contains(&value));
159 let value = u32::try_from(value).expect("year is non-negative");
160 push_2_digits(out, value / 100);
161 push_2_digits(out, value % 100);
162}
163
164fn push_9_digits(out: &mut String, value: u32) {
165 debug_assert!(value < 1_000_000_000);
166 push_3_digits(out, value / 1_000_000);
167 push_3_digits(out, value / 1_000 % 1_000);
168 push_3_digits(out, value % 1_000);
169}
170
171fn split_unix_nanos(unix_nanos: UnixNanos) -> DateTimeParts {
172 let nanos = unix_nanos.as_u64();
173 let total_seconds = nanos / NANOSECONDS_IN_SECOND;
174 let subsec_nanos = u32::try_from(nanos % NANOSECONDS_IN_SECOND).expect("subsecond fits u32");
175 let days = u32::try_from(total_seconds / SECONDS_IN_DAY).expect("days since epoch fits u32");
176 let seconds_of_day =
177 u32::try_from(total_seconds % SECONDS_IN_DAY).expect("seconds of day fits u32");
178 let (year, month, day) = civil_from_days(days);
179 let hour = seconds_of_day / SECONDS_IN_HOUR_U32;
180 let minute = (seconds_of_day % SECONDS_IN_HOUR_U32) / SECONDS_IN_MINUTE_U32;
181 let second = seconds_of_day % SECONDS_IN_MINUTE_U32;
182
183 DateTimeParts {
184 year,
185 month,
186 day,
187 hour,
188 minute,
189 second,
190 subsec_nanos,
191 }
192}
193
194fn push_iso8601_prefix(
195 out: &mut String,
196 year: i32,
197 month: u32,
198 day: u32,
199 hour: u32,
200 minute: u32,
201 second: u32,
202) {
203 push_4_digits(out, year);
204 out.push('-');
205 push_2_digits(out, month);
206 out.push('-');
207 push_2_digits(out, day);
208 out.push('T');
209 push_2_digits(out, hour);
210 out.push(':');
211 push_2_digits(out, minute);
212 out.push(':');
213 push_2_digits(out, second);
214 out.push('.');
215}
216
217pub const WEEKDAYS: [Weekday; 5] = [
219 Weekday::Monday,
220 Weekday::Tuesday,
221 Weekday::Wednesday,
222 Weekday::Thursday,
223 Weekday::Friday,
224];
225
226#[expect(
232 clippy::cast_possible_truncation,
233 clippy::cast_sign_loss,
234 clippy::cast_precision_loss,
235 reason = "Intentional for unit conversion, may lose precision after clamping"
236)]
237pub fn secs_to_nanos(secs: f64) -> anyhow::Result<u64> {
238 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
239 if secs <= 0.0 {
240 return Ok(0);
241 }
242 let nanos = secs * NANOSECONDS_IN_SECOND as f64;
243 anyhow::ensure!(
244 nanos < U64_UPPER_BOUND_F64,
245 "seconds {secs} is out of range for `u64` nanoseconds"
246 );
247 Ok(nanos.trunc() as u64)
248}
249
250#[expect(
256 clippy::cast_possible_truncation,
257 clippy::cast_sign_loss,
258 clippy::cast_precision_loss,
259 reason = "Intentional for unit conversion, may lose precision after clamping"
260)]
261pub fn secs_to_millis(secs: f64) -> anyhow::Result<u64> {
262 anyhow::ensure!(secs.is_finite(), "seconds must be finite, was {secs}");
263 if secs <= 0.0 {
264 return Ok(0);
265 }
266 let millis = secs * MILLISECONDS_IN_SECOND as f64;
267 anyhow::ensure!(
268 millis < U64_UPPER_BOUND_F64,
269 "seconds {secs} is out of range for `u64` milliseconds"
270 );
271 Ok(millis.trunc() as u64)
272}
273
274#[must_use]
283pub fn secs_to_nanos_unchecked(secs: f64) -> u64 {
284 secs_to_nanos(secs).expect("secs_to_nanos_unchecked: invalid or overflowing input")
285}
286
287#[must_use]
293pub const fn mins_to_secs(mins: u64) -> u64 {
294 checked_mins_to_secs(mins).expect("minutes to seconds conversion overflow")
295}
296
297#[must_use]
299pub const fn checked_mins_to_secs(mins: u64) -> Option<u64> {
300 mins.checked_mul(SECONDS_IN_MINUTE)
301}
302
303#[expect(
312 clippy::cast_possible_truncation,
313 clippy::cast_sign_loss,
314 clippy::cast_precision_loss,
315 reason = "Intentional for unit conversion, may lose precision after clamping"
316)]
317pub fn millis_to_nanos(millis: f64) -> anyhow::Result<u64> {
318 anyhow::ensure!(
319 millis.is_finite(),
320 "milliseconds must be finite, was {millis}"
321 );
322
323 if millis <= 0.0 {
324 return Ok(0);
325 }
326 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
327 anyhow::ensure!(
328 nanos < U64_UPPER_BOUND_F64,
329 "milliseconds {millis} is out of range for `u64` nanoseconds"
330 );
331 Ok(nanos.trunc() as u64)
332}
333
334#[must_use]
340pub fn millis_to_nanos_unchecked(millis: f64) -> u64 {
341 millis_to_nanos(millis).expect("millis_to_nanos_unchecked: invalid or overflowing input")
342}
343
344#[expect(
353 clippy::cast_possible_truncation,
354 clippy::cast_sign_loss,
355 clippy::cast_precision_loss,
356 reason = "Intentional for unit conversion, may lose precision after clamping"
357)]
358pub fn micros_to_nanos(micros: f64) -> anyhow::Result<u64> {
359 anyhow::ensure!(
360 micros.is_finite(),
361 "microseconds must be finite, was {micros}"
362 );
363
364 if micros <= 0.0 {
365 return Ok(0);
366 }
367 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
368 anyhow::ensure!(
369 nanos < U64_UPPER_BOUND_F64,
370 "microseconds {micros} is out of range for `u64` nanoseconds"
371 );
372 Ok(nanos.trunc() as u64)
373}
374
375#[must_use]
381pub fn micros_to_nanos_unchecked(micros: f64) -> u64 {
382 micros_to_nanos(micros).expect("micros_to_nanos_unchecked: invalid or overflowing input")
383}
384
385#[expect(
390 clippy::cast_precision_loss,
391 reason = "Precision loss acceptable for time conversion"
392)]
393#[must_use]
394pub fn nanos_to_secs(nanos: u64) -> f64 {
395 let seconds = nanos / NANOSECONDS_IN_SECOND;
396 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
397 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
398}
399
400#[must_use]
402pub const fn nanos_to_millis(nanos: u64) -> u64 {
403 nanos / NANOSECONDS_IN_MILLISECOND
404}
405
406#[must_use]
408pub const fn nanos_to_micros(nanos: u64) -> u64 {
409 nanos / NANOSECONDS_IN_MICROSECOND
410}
411
412#[inline]
416#[must_use]
417pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
418 let parts = split_unix_nanos(unix_nanos);
419
420 let mut out = String::with_capacity(30);
421 push_iso8601_prefix(
422 &mut out,
423 parts.year,
424 parts.month,
425 parts.day,
426 parts.hour,
427 parts.minute,
428 parts.second,
429 );
430 push_9_digits(&mut out, parts.subsec_nanos);
431 out.push('Z');
432 out
433}
434
435#[inline]
458pub fn iso8601_to_unix_nanos(date_string: &str) -> anyhow::Result<UnixNanos> {
459 date_string
460 .parse::<UnixNanos>()
461 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{date_string}': {e}"))
462}
463
464#[inline]
469#[must_use]
470pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
471 let parts = split_unix_nanos(unix_nanos);
472
473 let mut out = String::with_capacity(24);
474 push_iso8601_prefix(
475 &mut out,
476 parts.year,
477 parts.month,
478 parts.day,
479 parts.hour,
480 parts.minute,
481 parts.second,
482 );
483 push_3_digits(
484 &mut out,
485 parts.subsec_nanos / NANOSECONDS_IN_MILLISECOND_U32,
486 );
487 out.push('Z');
488 out
489}
490
491#[must_use]
493pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
494 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
495}
496
497pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
503 let date = Date::new(
504 i16::try_from(year).map_err(|_| anyhow::anyhow!("Invalid date"))?,
505 i8::try_from(month).map_err(|_| anyhow::anyhow!("Invalid date"))?,
506 i8::try_from(day).map_err(|_| anyhow::anyhow!("Invalid date"))?,
507 )
508 .map_err(|_| anyhow::anyhow!("Invalid date"))?;
509 let current_weekday = date.weekday().to_monday_one_offset();
510
511 let offset = match current_weekday {
513 1..=5 => 0, 6 => 1, _ => 2, };
517 let last_closest = date.checked_sub(Span::new().days(offset))?;
519
520 let unix_timestamp_ns = last_closest
522 .at(0, 0, 0, 0)
523 .to_zoned(TimeZone::UTC)?
524 .timestamp()
525 .as_nanosecond();
526
527 let ns_u64 = u64::try_from(unix_timestamp_ns)
528 .map_err(|_| anyhow::anyhow!("Negative timestamp: {unix_timestamp_ns}"))?;
529 Ok(UnixNanos::from(ns_u64))
530}
531
532pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
538 let timestamp_ns = timestamp_ns.as_u64();
542 let now_ns = nanos_since_unix_epoch();
543
544 if timestamp_ns > now_ns {
546 return Ok(false);
547 }
548
549 Ok(now_ns - timestamp_ns <= NANOSECONDS_IN_DAY)
550}
551
552fn shift_months(datetime: Timestamp, months: i64) -> anyhow::Result<Timestamp> {
553 let span = Span::new().try_months(months)?;
554 let result = datetime.to_zoned(TimeZone::UTC).checked_add(span)?;
555 Ok(result.timestamp())
556}
557
558pub fn subtract_n_months(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
564 shift_months(datetime, -i64::from(n))
565 .map_err(|_| anyhow::anyhow!("Failed to subtract {n} months from {datetime}"))
566}
567
568pub fn add_n_months(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
574 shift_months(datetime, i64::from(n))
575 .map_err(|_| anyhow::anyhow!("Failed to add {n} months to {datetime}"))
576}
577
578pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
584 let datetime = unix_nanos.to_datetime_utc();
585 let result = subtract_n_months(datetime, n)?;
586 let timestamp = result.as_nanosecond();
587
588 let nanos =
589 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
590 Ok(UnixNanos::from(nanos))
591}
592
593pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
599 let datetime = unix_nanos.to_datetime_utc();
600 let result = add_n_months(datetime, n)?;
601 let timestamp = result.as_nanosecond();
602
603 let nanos =
604 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
605 Ok(UnixNanos::from(nanos))
606}
607
608pub fn add_n_years(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
614 let months = n.checked_mul(12).ok_or_else(|| {
615 anyhow::anyhow!("Failed to add {n} years to {datetime}: month count overflow")
616 })?;
617
618 shift_months(datetime, i64::from(months))
619 .map_err(|_| anyhow::anyhow!("Failed to add {n} years to {datetime}"))
620}
621
622pub fn subtract_n_years(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
628 let months = n.checked_mul(12).ok_or_else(|| {
629 anyhow::anyhow!("Failed to subtract {n} years from {datetime}: month count overflow")
630 })?;
631
632 shift_months(datetime, -i64::from(months))
633 .map_err(|_| anyhow::anyhow!("Failed to subtract {n} years from {datetime}"))
634}
635
636pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
642 let datetime = unix_nanos.to_datetime_utc();
643 let result = add_n_years(datetime, n)?;
644 let timestamp = result.as_nanosecond();
645
646 let nanos =
647 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
648 Ok(UnixNanos::from(nanos))
649}
650
651pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
657 let datetime = unix_nanos.to_datetime_utc();
658 let result = subtract_n_years(datetime, n)?;
659 let timestamp = result.as_nanosecond();
660
661 let nanos =
662 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
663 Ok(UnixNanos::from(nanos))
664}
665
666pub fn datetime_to_unix_nanos(value: Option<Timestamp>) -> Option<UnixNanos> {
668 value
669 .map(Timestamp::as_nanosecond)
670 .and_then(|nanos| u64::try_from(nanos).ok())
671 .map(UnixNanos::from)
672}
673
674pub fn try_datetime_to_unix_nanos(value: Timestamp) -> anyhow::Result<UnixNanos> {
682 let nanos = value.as_nanosecond();
683
684 if nanos < 0 {
685 anyhow::bail!("DateTime timestamp cannot be negative: {nanos}");
686 }
687 let nanos = u64::try_from(nanos)
688 .map_err(|_| anyhow::anyhow!("DateTime timestamp out of range for UnixNanos: {nanos}"))?;
689
690 Ok(UnixNanos::from(nanos))
691}
692
693#[cfg(test)]
694#[allow(
696 clippy::float_cmp,
697 reason = "Exact float comparisons acceptable in tests"
698)]
699mod tests {
700 use jiff::SignedDuration;
701 use proptest::prelude::*;
702 use rstest::rstest;
703
704 use super::*;
705
706 fn timestamp(value: &str) -> Timestamp {
707 value.parse().unwrap()
708 }
709
710 #[rstest]
711 #[case(0.0, 0)]
712 #[case(1.0, 1_000_000_000)]
713 #[case(1.1, 1_100_000_000)]
714 #[case(42.0, 42_000_000_000)]
715 #[case(0.000_123_5, 123_500)]
716 #[case(0.000_000_01, 10)]
717 #[case(0.000_000_001, 1)]
718 #[case(9.999_999_999, 9_999_999_999)]
719 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
720 let result = secs_to_nanos(value).unwrap();
721 assert_eq!(result, expected);
722 }
723
724 #[rstest]
725 #[case(0.0, 0)]
726 #[case(1.0, 1_000)]
727 #[case(1.1, 1_100)]
728 #[case(42.0, 42_000)]
729 #[case(0.012_34, 12)]
730 #[case(0.001, 1)]
731 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
732 let result = secs_to_millis(value).unwrap();
733 assert_eq!(result, expected);
734 }
735
736 #[rstest]
737 fn test_secs_to_nanos_unchecked_matches_checked() {
738 assert_eq!(secs_to_nanos_unchecked(1.1), secs_to_nanos(1.1).unwrap());
739 }
740
741 #[rstest]
742 fn test_secs_to_nanos_non_finite_errors() {
743 let err = secs_to_nanos(f64::NAN).unwrap_err();
744 assert!(err.to_string().contains("finite"));
745 }
746
747 #[rstest]
748 fn test_secs_to_millis_non_finite_errors() {
749 let err = secs_to_millis(f64::INFINITY).unwrap_err();
750 assert!(err.to_string().contains("finite"));
751 }
752
753 #[rstest]
754 fn test_millis_to_nanos_non_finite_errors() {
755 let err = millis_to_nanos(f64::NEG_INFINITY).unwrap_err();
756 assert!(err.to_string().contains("finite"));
757 }
758
759 #[rstest]
760 fn test_micros_to_nanos_non_finite_errors() {
761 let err = micros_to_nanos(f64::NAN).unwrap_err();
762 assert!(err.to_string().contains("finite"));
763 }
764
765 #[rstest]
766 #[case(0, 0)]
767 #[case(1, 60)]
768 #[case(5, 300)]
769 #[case(60, 3600)]
770 #[case(1440, 86400)]
771 fn test_mins_to_secs(#[case] mins: u64, #[case] expected: u64) {
772 assert_eq!(mins_to_secs(mins), expected);
773 }
774
775 #[rstest]
776 #[case(
777 checked_mins_to_secs,
778 307_445_734_561_825_860,
779 18_446_744_073_709_551_600
780 )]
781 fn test_checked_minutes_conversion_boundary(
782 #[case] convert: fn(u64) -> Option<u64>,
783 #[case] max: u64,
784 #[case] expected: u64,
785 ) {
786 assert_eq!(convert(max), Some(expected));
787 assert_eq!(convert(max + 1), None);
788 }
789
790 #[rstest]
791 #[should_panic(expected = "minutes to seconds conversion overflow")]
792 fn test_mins_to_secs_overflow_panics() {
793 let _ = mins_to_secs(307_445_734_561_825_861);
794 }
795
796 #[rstest]
797 #[case(
798 secs_to_nanos,
799 18_446_744_073.709_553,
800 18_446_744_073.709_55,
801 18_446_744_073_709_549_568
802 )]
803 #[case(
804 secs_to_millis,
805 18_446_744_073_709_550.0,
806 18_446_744_073_709_548.0,
807 18_446_744_073_709_547_520
808 )]
809 #[case(
810 millis_to_nanos,
811 18_446_744_073_709.55,
812 18_446_744_073_709.547,
813 18_446_744_073_709_547_520
814 )]
815 #[case(
816 micros_to_nanos,
817 18_446_744_073_709_550.0,
818 18_446_744_073_709_548.0,
819 18_446_744_073_709_547_520
820 )]
821 fn test_float_conversion_u64_boundary(
822 #[case] convert: fn(f64) -> anyhow::Result<u64>,
823 #[case] invalid: f64,
824 #[case] previous: f64,
825 #[case] expected: u64,
826 ) {
827 let err = convert(invalid).unwrap_err();
828 assert!(err.to_string().contains("out of range"));
829 assert_eq!(convert(previous).unwrap(), expected);
830 }
831
832 #[rstest]
833 fn test_secs_to_nanos_negative_infinity_errors() {
834 let result = secs_to_nanos(f64::NEG_INFINITY);
835 assert!(result.is_err());
836 }
837
838 #[rstest]
839 #[case(0.0, 0)]
840 #[case(1.0, 1_000_000)]
841 #[case(1.1, 1_100_000)]
842 #[case(42.0, 42_000_000)]
843 #[case(0.000_123_4, 123)]
844 #[case(0.000_01, 10)]
845 #[case(0.000_001, 1)]
846 #[case(9.999_999, 9_999_999)]
847 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
848 let result = millis_to_nanos(value).unwrap();
849 assert_eq!(result, expected);
850 }
851
852 #[rstest]
853 fn test_millis_to_nanos_unchecked_matches_checked() {
854 assert_eq!(
855 millis_to_nanos_unchecked(1.1),
856 millis_to_nanos(1.1).unwrap()
857 );
858 }
859
860 #[rstest]
861 #[case(0.0, 0)]
862 #[case(1.0, 1_000)]
863 #[case(1.1, 1_100)]
864 #[case(42.0, 42_000)]
865 #[case(0.1234, 123)]
866 #[case(0.01, 10)]
867 #[case(0.001, 1)]
868 #[case(9.999, 9_999)]
869 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
870 let result = micros_to_nanos(value).unwrap();
871 assert_eq!(result, expected);
872 }
873
874 #[rstest]
875 fn test_micros_to_nanos_unchecked_matches_checked() {
876 assert_eq!(
877 micros_to_nanos_unchecked(1.1),
878 micros_to_nanos(1.1).unwrap()
879 );
880 }
881
882 #[rstest]
883 #[case(0, 0.0)]
884 #[case(1, 1e-09)]
885 #[case(1_000_000_000, 1.0)]
886 #[case(42_897_123_111, 42.897_123_111)]
887 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
888 let result = nanos_to_secs(value);
889 assert_eq!(result, expected);
890 }
891
892 #[rstest]
893 #[case(0, 0)]
894 #[case(1_000_000, 1)]
895 #[case(1_000_000_000, 1000)]
896 #[case(42_897_123_111, 42897)]
897 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
898 let result = nanos_to_millis(value);
899 assert_eq!(result, expected);
900 }
901
902 #[rstest]
903 #[case(0, 0)]
904 #[case(1_000, 1)]
905 #[case(1_000_000_000, 1_000_000)]
906 #[case(42_897_123, 42_897)]
907 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
908 let result = nanos_to_micros(value);
909 assert_eq!(result, expected);
910 }
911
912 #[rstest]
913 #[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) {
922 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
923 assert_eq!(result, expected);
924 }
925
926 #[rstest]
927 #[case(0)]
928 #[case(1)]
929 #[case(951_782_400_123_456_789)]
930 #[case(1_609_459_199_999_999_999)]
931 #[case(i64::MAX as u64)]
932 #[case(5_097_600_000_000_000)] #[case(5_356_800_000_000_000)] fn test_unix_nanos_to_iso8601_matches_jiff_oracle(#[case] nanos: u64) {
935 let expected = format!(
936 "{:.9}",
937 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
938 );
939 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
940 assert_eq!(result, expected);
941 }
942
943 #[rstest]
944 #[case((i64::MAX as u64) + 1)]
945 #[case(u64::MAX)]
946 fn test_unix_nanos_to_iso8601_supports_full_unix_nanos_range(#[case] nanos: u64) {
947 let expected = format!(
948 "{:.9}",
949 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
950 );
951 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
952 assert_eq!(result, expected);
953 }
954
955 #[rstest]
956 #[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) {
963 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
964 assert_eq!(result, expected);
965 }
966
967 #[rstest]
968 #[case(0)]
969 #[case(951_782_400_123_456_789)]
970 #[case(1_609_459_199_999_999_999)]
971 #[case(i64::MAX as u64)]
972 fn test_unix_nanos_to_iso8601_millis_matches_jiff_oracle(#[case] nanos: u64) {
973 let expected = format!(
974 "{:.3}",
975 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
976 );
977 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
978 assert_eq!(result, expected);
979 }
980
981 #[rstest]
982 #[case((i64::MAX as u64) + 1)]
983 #[case(u64::MAX)]
984 fn test_unix_nanos_to_iso8601_millis_supports_full_unix_nanos_range(#[case] nanos: u64) {
985 let expected = format!(
986 "{:.3}",
987 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
988 );
989 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
990 assert_eq!(result, expected);
991 }
992
993 proptest! {
996 #[rstest]
997 fn prop_unix_nanos_to_iso8601_matches_jiff(nanos in any::<u64>()) {
998 let expected = format!(
999 "{:.9}",
1000 Timestamp::from_nanosecond(i128::from(nanos)).unwrap(),
1001 );
1002 let actual = unix_nanos_to_iso8601(UnixNanos::from(nanos));
1003 prop_assert_eq!(actual, expected);
1004 }
1005
1006 #[rstest]
1007 fn prop_unix_nanos_to_iso8601_millis_matches_jiff(nanos in any::<u64>()) {
1008 let expected = format!(
1009 "{:.3}",
1010 Timestamp::from_nanosecond(i128::from(nanos)).unwrap(),
1011 );
1012 let actual = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
1013 prop_assert_eq!(actual, expected);
1014 }
1015 }
1016
1017 #[rstest]
1018 #[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(
1023 #[case] year: i32,
1024 #[case] month: u32,
1025 #[case] day: u32,
1026 #[case] expected: u64,
1027 ) {
1028 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
1029 assert_eq!(result, expected);
1030 }
1031
1032 #[rstest]
1033 fn test_last_closest_weekday_nanos_with_invalid_date() {
1034 let result = last_weekday_nanos(2023, 4, 31);
1035 assert!(result.is_err());
1036 }
1037
1038 #[rstest]
1039 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
1040 let result = last_weekday_nanos(2023, 2, 30);
1041 assert!(result.is_err());
1042 }
1043
1044 #[rstest]
1045 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
1046 let result = last_weekday_nanos(9999, 12, 31);
1047 assert!(result.is_err());
1048 }
1049
1050 #[rstest]
1051 fn test_is_within_last_24_hours_when_now() {
1052 let now_ns = Timestamp::now().as_nanosecond();
1053 assert!(is_within_last_24_hours(UnixNanos::from(u64::try_from(now_ns).unwrap())).unwrap());
1054 }
1055
1056 #[rstest]
1057 fn test_is_within_last_24_hours_when_two_days_ago() {
1058 let past_ns = (Timestamp::now() - SignedDuration::from_hours(48)).as_nanosecond();
1059 assert!(
1060 !is_within_last_24_hours(UnixNanos::from(u64::try_from(past_ns).unwrap())).unwrap()
1061 );
1062 }
1063
1064 #[rstest]
1065 fn test_is_within_last_24_hours_when_future() {
1066 let future_ns = (Timestamp::now() + SignedDuration::from_hours(1)).as_nanosecond();
1068 assert!(
1069 !is_within_last_24_hours(UnixNanos::from(u64::try_from(future_ns).unwrap())).unwrap()
1070 );
1071
1072 let future_ns = (Timestamp::now() + SignedDuration::from_hours(24)).as_nanosecond();
1074 assert!(
1075 !is_within_last_24_hours(UnixNanos::from(u64::try_from(future_ns).unwrap())).unwrap()
1076 );
1077 }
1078
1079 #[rstest]
1080 #[case(
1081 timestamp("2024-03-31T12:00:00Z"),
1082 1,
1083 timestamp("2024-02-29T12:00:00Z")
1084 )]
1085 #[case(
1086 timestamp("2024-03-31T12:00:00Z"),
1087 12,
1088 timestamp("2023-03-31T12:00:00Z")
1089 )]
1090 #[case(
1091 timestamp("2024-01-31T12:00:00Z"),
1092 1,
1093 timestamp("2023-12-31T12:00:00Z")
1094 )]
1095 #[case(
1096 timestamp("2024-03-31T12:00:00Z"),
1097 2,
1098 timestamp("2024-01-31T12:00:00Z")
1099 )]
1100 fn test_subtract_n_months(
1101 #[case] input: Timestamp,
1102 #[case] months: u32,
1103 #[case] expected: Timestamp,
1104 ) {
1105 let result = subtract_n_months(input, months).unwrap();
1106 assert_eq!(result, expected);
1107 }
1108
1109 #[rstest]
1110 #[case(
1111 timestamp("2023-02-28T12:00:00Z"),
1112 1,
1113 timestamp("2023-03-28T12:00:00Z")
1114 )]
1115 #[case(
1116 timestamp("2024-01-31T12:00:00Z"),
1117 1,
1118 timestamp("2024-02-29T12:00:00Z")
1119 )]
1120 #[case(
1121 timestamp("2023-12-31T12:00:00Z"),
1122 1,
1123 timestamp("2024-01-31T12:00:00Z")
1124 )]
1125 #[case(
1126 timestamp("2023-01-31T12:00:00Z"),
1127 13,
1128 timestamp("2024-02-29T12:00:00Z")
1129 )]
1130 fn test_add_n_months(
1131 #[case] input: Timestamp,
1132 #[case] months: u32,
1133 #[case] expected: Timestamp,
1134 ) {
1135 let result = add_n_months(input, months).unwrap();
1136 assert_eq!(result, expected);
1137 }
1138
1139 #[rstest]
1140 fn test_add_n_years_overflow() {
1141 let datetime = timestamp("2024-01-01T00:00:00Z");
1142 let err = add_n_years(datetime, u32::MAX).unwrap_err();
1143 assert!(err.to_string().contains("month count overflow"));
1144 }
1145
1146 #[rstest]
1147 fn test_subtract_n_years_overflow() {
1148 let datetime = timestamp("2024-01-01T00:00:00Z");
1149 let err = subtract_n_years(datetime, u32::MAX).unwrap_err();
1150 assert!(err.to_string().contains("month count overflow"));
1151 }
1152
1153 #[rstest]
1154 fn test_add_n_years_nanos_overflow() {
1155 let nanos = UnixNanos::from(0);
1156 let err = add_n_years_nanos(nanos, u32::MAX).unwrap_err();
1157 assert!(err.to_string().contains("month count overflow"));
1158 }
1159
1160 #[rstest]
1161 #[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) {
1170 let result = iso8601_to_unix_nanos(input).unwrap();
1171 assert_eq!(result.as_u64(), expected);
1172 }
1173
1174 #[rstest]
1175 #[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) {
1180 let result = iso8601_to_unix_nanos(input);
1181 assert!(result.is_err());
1182 }
1183
1184 #[rstest]
1185 fn test_iso8601_roundtrip() {
1186 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
1187 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
1188 let parsed_nanos = iso8601_to_unix_nanos(&iso8601_string).unwrap();
1189 assert_eq!(parsed_nanos, original_nanos);
1190 }
1191
1192 #[rstest]
1193 fn test_add_n_years_nanos_normal_case() {
1194 let start = UnixNanos::from(timestamp("2020-01-01T00:00:00Z"));
1196 let result = add_n_years_nanos(start, 1).unwrap();
1197 let expected = UnixNanos::from(timestamp("2021-01-01T00:00:00Z"));
1198 assert_eq!(result, expected);
1199 }
1200
1201 #[rstest]
1202 fn test_add_n_months_nanos_normal_case() {
1203 let start = UnixNanos::from(timestamp("2020-01-15T00:00:00Z"));
1205 let result = add_n_months_nanos(start, 1).unwrap();
1206 let expected = UnixNanos::from(timestamp("2020-02-15T00:00:00Z"));
1207 assert_eq!(result, expected);
1208 }
1209
1210 #[rstest]
1211 fn test_add_n_years_nanos_from_epoch() {
1212 let start = UnixNanos::from(0);
1214 let result = add_n_years_nanos(start, 1).unwrap();
1215 assert_eq!(result.as_u64(), 31_536_000_000_000_000);
1216 }
1217
1218 #[rstest]
1219 fn test_datetime_to_unix_nanos_at_epoch() {
1220 let epoch = Timestamp::UNIX_EPOCH;
1222 let result = datetime_to_unix_nanos(Some(epoch));
1223 assert_eq!(result, Some(UnixNanos::from(0)));
1224 }
1225
1226 #[rstest]
1227 fn test_datetime_to_unix_nanos_typical_datetime() {
1228 let dt = timestamp("2024-01-15T13:30:45.123456789Z");
1229 let result = datetime_to_unix_nanos(Some(dt));
1230
1231 assert!(result.is_some());
1233 assert_eq!(result.unwrap().as_u64(), 1_705_325_445_123_456_789);
1234 }
1235
1236 #[rstest]
1237 fn test_datetime_to_unix_nanos_before_epoch() {
1238 let before_epoch = timestamp("1969-12-31T23:59:59Z");
1241 let result = datetime_to_unix_nanos(Some(before_epoch));
1242 assert_eq!(result, None);
1243 }
1244
1245 #[rstest]
1246 fn test_datetime_to_unix_nanos_one_second_after_epoch() {
1247 let dt = Timestamp::from_second(1).unwrap();
1249 let result = datetime_to_unix_nanos(Some(dt));
1250 assert_eq!(result, Some(UnixNanos::from(1_000_000_000)));
1251 }
1252
1253 #[rstest]
1254 fn test_datetime_to_unix_nanos_with_subsecond_precision() {
1255 let dt = Timestamp::new(0, 1_000).unwrap(); let result = datetime_to_unix_nanos(Some(dt));
1258 assert_eq!(result, Some(UnixNanos::from(1_000)));
1259 }
1260
1261 #[rstest]
1262 fn test_try_datetime_to_unix_nanos_valid() {
1263 let dt = Timestamp::new(0, 1_000).unwrap();
1264 assert_eq!(
1265 try_datetime_to_unix_nanos(dt).unwrap(),
1266 UnixNanos::from(1_000)
1267 );
1268 }
1269
1270 #[rstest]
1271 fn test_try_datetime_to_unix_nanos_at_epoch() {
1272 assert_eq!(
1273 try_datetime_to_unix_nanos(Timestamp::UNIX_EPOCH).unwrap(),
1274 UnixNanos::from(0)
1275 );
1276 }
1277
1278 #[rstest]
1279 fn test_try_datetime_to_unix_nanos_before_epoch_errors() {
1280 let before_epoch = timestamp("1969-12-31T23:59:59Z");
1281 let err = try_datetime_to_unix_nanos(before_epoch).unwrap_err();
1282 assert!(
1283 err.to_string().contains("cannot be negative"),
1284 "unexpected error: {err}"
1285 );
1286 }
1287
1288 #[rstest]
1289 fn test_try_datetime_to_unix_nanos_out_of_range_errors() {
1290 let err = try_datetime_to_unix_nanos(Timestamp::MAX).unwrap_err();
1291 assert!(
1292 err.to_string().contains("out of range"),
1293 "unexpected error: {err}"
1294 );
1295 }
1296
1297 #[rstest]
1298 fn test_month_and_year_arithmetic_support_values_above_i64_max() {
1299 let large = UnixNanos::from(u64::MAX);
1300 assert!(subtract_n_months_nanos(large, 1).is_ok());
1301 assert!(add_n_months_nanos(large, 1).is_err());
1302 assert!(add_n_years_nanos(large, 1).is_err());
1303 assert!(subtract_n_years_nanos(large, 1).is_ok());
1304 }
1305
1306 #[rstest]
1307 fn test_subtract_n_months_nanos_pre_epoch_result_errors() {
1308 let epoch = UnixNanos::from(0);
1309 let err = subtract_n_months_nanos(epoch, 1).unwrap_err();
1310 assert_eq!(err.to_string(), "Negative timestamp not allowed");
1311 }
1312
1313 #[rstest]
1314 fn test_subtract_n_years_nanos_pre_epoch_result_errors() {
1315 let epoch = UnixNanos::from(0);
1316 let err = subtract_n_years_nanos(epoch, 1).unwrap_err();
1317 assert_eq!(err.to_string(), "Negative timestamp not allowed");
1318 }
1319
1320 #[rstest]
1321 fn test_subtract_n_months_nanos_at_epoch_boundary() {
1322 let epoch = UnixNanos::from(0);
1323 assert_eq!(subtract_n_months_nanos(epoch, 0).unwrap(), epoch);
1324 }
1325
1326 #[rstest]
1327 fn test_get_timezone_with_valid_name() {
1328 let tz = get_timezone("UTC").unwrap();
1329 assert_eq!(tz.iana_name(), Some("UTC"));
1330 }
1331
1332 #[rstest]
1333 fn test_get_timezone_with_unknown_name_errors() {
1334 assert!(get_timezone("Not/A_Zone").is_err());
1335 }
1336
1337 #[rstest]
1338 #[case(0, 0)]
1339 #[case(999, 0)]
1340 #[case(1_000, 1_000)]
1341 #[case(1_000_001, 1_000_000)]
1342 #[case(u64::MAX, 18_446_744_073_709_551_000)]
1343 fn test_floor_to_nearest_microsecond(#[case] input: u64, #[case] expected: u64) {
1344 assert_eq!(floor_to_nearest_microsecond(input), expected);
1345 }
1346
1347 #[rstest]
1348 fn test_formatters_match_jiff_on_every_representable_utc_day() {
1349 for day in 0..=u64::MAX / NANOSECONDS_IN_DAY {
1350 for nanos in [
1351 day * NANOSECONDS_IN_DAY,
1352 (day * NANOSECONDS_IN_DAY).saturating_add(NANOSECONDS_IN_DAY - 1),
1353 ] {
1354 let timestamp = Timestamp::from_nanosecond(i128::from(nanos)).unwrap();
1355 assert_eq!(
1356 unix_nanos_to_iso8601(UnixNanos::from(nanos)),
1357 format!("{timestamp:.9}")
1358 );
1359 assert_eq!(
1360 unix_nanos_to_iso8601_millis(UnixNanos::from(nanos)),
1361 format!("{timestamp:.3}")
1362 );
1363 }
1364 }
1365 }
1366}