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 writers 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#[expect(
305 clippy::cast_possible_truncation,
306 clippy::cast_sign_loss,
307 clippy::cast_precision_loss,
308 reason = "Intentional for unit conversion, may lose precision after clamping"
309)]
310pub fn millis_to_nanos(millis: f64) -> anyhow::Result<u64> {
311 anyhow::ensure!(
312 millis.is_finite(),
313 "milliseconds must be finite, was {millis}"
314 );
315
316 if millis <= 0.0 {
317 return Ok(0);
318 }
319 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
320 anyhow::ensure!(
321 nanos < U64_UPPER_BOUND_F64,
322 "milliseconds {millis} is out of range for `u64` nanoseconds"
323 );
324 Ok(nanos.trunc() as u64)
325}
326
327#[must_use]
333pub fn millis_to_nanos_unchecked(millis: f64) -> u64 {
334 millis_to_nanos(millis).expect("millis_to_nanos_unchecked: invalid or overflowing input")
335}
336
337#[expect(
346 clippy::cast_possible_truncation,
347 clippy::cast_sign_loss,
348 clippy::cast_precision_loss,
349 reason = "Intentional for unit conversion, may lose precision after clamping"
350)]
351pub fn micros_to_nanos(micros: f64) -> anyhow::Result<u64> {
352 anyhow::ensure!(
353 micros.is_finite(),
354 "microseconds must be finite, was {micros}"
355 );
356
357 if micros <= 0.0 {
358 return Ok(0);
359 }
360 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
361 anyhow::ensure!(
362 nanos < U64_UPPER_BOUND_F64,
363 "microseconds {micros} is out of range for `u64` nanoseconds"
364 );
365 Ok(nanos.trunc() as u64)
366}
367
368#[must_use]
374pub fn micros_to_nanos_unchecked(micros: f64) -> u64 {
375 micros_to_nanos(micros).expect("micros_to_nanos_unchecked: invalid or overflowing input")
376}
377
378#[expect(
383 clippy::cast_precision_loss,
384 reason = "Precision loss acceptable for time conversion"
385)]
386#[must_use]
387pub fn nanos_to_secs(nanos: u64) -> f64 {
388 let seconds = nanos / NANOSECONDS_IN_SECOND;
389 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
390 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
391}
392
393#[must_use]
395pub const fn nanos_to_millis(nanos: u64) -> u64 {
396 nanos / NANOSECONDS_IN_MILLISECOND
397}
398
399#[must_use]
401pub const fn nanos_to_micros(nanos: u64) -> u64 {
402 nanos / NANOSECONDS_IN_MICROSECOND
403}
404
405#[inline]
409#[must_use]
410pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
411 let parts = split_unix_nanos(unix_nanos);
412
413 let mut out = String::with_capacity(30);
414 push_iso8601_prefix(
415 &mut out,
416 parts.year,
417 parts.month,
418 parts.day,
419 parts.hour,
420 parts.minute,
421 parts.second,
422 );
423 push_9_digits(&mut out, parts.subsec_nanos);
424 out.push('Z');
425 out
426}
427
428#[inline]
451pub fn iso8601_to_unix_nanos(date_string: &str) -> anyhow::Result<UnixNanos> {
452 date_string
453 .parse::<UnixNanos>()
454 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{date_string}': {e}"))
455}
456
457#[inline]
462#[must_use]
463pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
464 let parts = split_unix_nanos(unix_nanos);
465
466 let mut out = String::with_capacity(24);
467 push_iso8601_prefix(
468 &mut out,
469 parts.year,
470 parts.month,
471 parts.day,
472 parts.hour,
473 parts.minute,
474 parts.second,
475 );
476 push_3_digits(
477 &mut out,
478 parts.subsec_nanos / NANOSECONDS_IN_MILLISECOND_U32,
479 );
480 out.push('Z');
481 out
482}
483
484#[must_use]
486pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
487 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
488}
489
490pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
496 let date = Date::new(
497 i16::try_from(year).map_err(|_| anyhow::anyhow!("Invalid date"))?,
498 i8::try_from(month).map_err(|_| anyhow::anyhow!("Invalid date"))?,
499 i8::try_from(day).map_err(|_| anyhow::anyhow!("Invalid date"))?,
500 )
501 .map_err(|_| anyhow::anyhow!("Invalid date"))?;
502 let current_weekday = date.weekday().to_monday_one_offset();
503
504 let offset = match current_weekday {
506 1..=5 => 0, 6 => 1, _ => 2, };
510 let last_closest = date.checked_sub(Span::new().days(offset))?;
512
513 let unix_timestamp_ns = last_closest
515 .at(0, 0, 0, 0)
516 .to_zoned(TimeZone::UTC)?
517 .timestamp()
518 .as_nanosecond();
519
520 let ns_u64 = u64::try_from(unix_timestamp_ns)
521 .map_err(|_| anyhow::anyhow!("Negative timestamp: {unix_timestamp_ns}"))?;
522 Ok(UnixNanos::from(ns_u64))
523}
524
525pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
531 let timestamp_ns = timestamp_ns.as_u64();
535 let now_ns = nanos_since_unix_epoch();
536
537 if timestamp_ns > now_ns {
539 return Ok(false);
540 }
541
542 Ok(now_ns - timestamp_ns <= NANOSECONDS_IN_DAY)
543}
544
545fn shift_months(datetime: Timestamp, months: i64) -> anyhow::Result<Timestamp> {
546 let span = Span::new().try_months(months)?;
547 let result = datetime.to_zoned(TimeZone::UTC).checked_add(span)?;
548 Ok(result.timestamp())
549}
550
551pub fn subtract_n_months(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
557 shift_months(datetime, -i64::from(n))
558 .map_err(|_| anyhow::anyhow!("Failed to subtract {n} months from {datetime}"))
559}
560
561pub fn add_n_months(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
567 shift_months(datetime, i64::from(n))
568 .map_err(|_| anyhow::anyhow!("Failed to add {n} months to {datetime}"))
569}
570
571pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
577 let datetime = unix_nanos.to_datetime_utc();
578 let result = subtract_n_months(datetime, n)?;
579 let timestamp = result.as_nanosecond();
580
581 let nanos =
582 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
583 Ok(UnixNanos::from(nanos))
584}
585
586pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
592 let datetime = unix_nanos.to_datetime_utc();
593 let result = add_n_months(datetime, n)?;
594 let timestamp = result.as_nanosecond();
595
596 let nanos =
597 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
598 Ok(UnixNanos::from(nanos))
599}
600
601pub fn add_n_years(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
607 let months = n.checked_mul(12).ok_or_else(|| {
608 anyhow::anyhow!("Failed to add {n} years to {datetime}: month count overflow")
609 })?;
610
611 shift_months(datetime, i64::from(months))
612 .map_err(|_| anyhow::anyhow!("Failed to add {n} years to {datetime}"))
613}
614
615pub fn subtract_n_years(datetime: Timestamp, n: u32) -> anyhow::Result<Timestamp> {
621 let months = n.checked_mul(12).ok_or_else(|| {
622 anyhow::anyhow!("Failed to subtract {n} years from {datetime}: month count overflow")
623 })?;
624
625 shift_months(datetime, -i64::from(months))
626 .map_err(|_| anyhow::anyhow!("Failed to subtract {n} years from {datetime}"))
627}
628
629pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
635 let datetime = unix_nanos.to_datetime_utc();
636 let result = add_n_years(datetime, n)?;
637 let timestamp = result.as_nanosecond();
638
639 let nanos =
640 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
641 Ok(UnixNanos::from(nanos))
642}
643
644pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
650 let datetime = unix_nanos.to_datetime_utc();
651 let result = subtract_n_years(datetime, n)?;
652 let timestamp = result.as_nanosecond();
653
654 let nanos =
655 u64::try_from(timestamp).map_err(|_| anyhow::anyhow!("Negative timestamp not allowed"))?;
656 Ok(UnixNanos::from(nanos))
657}
658
659pub fn datetime_to_unix_nanos(value: Option<Timestamp>) -> Option<UnixNanos> {
661 value
662 .map(Timestamp::as_nanosecond)
663 .and_then(|nanos| u64::try_from(nanos).ok())
664 .map(UnixNanos::from)
665}
666
667pub fn try_datetime_to_unix_nanos(value: Timestamp) -> anyhow::Result<UnixNanos> {
675 let nanos = value.as_nanosecond();
676
677 if nanos < 0 {
678 anyhow::bail!("DateTime timestamp cannot be negative: {nanos}");
679 }
680 let nanos = u64::try_from(nanos)
681 .map_err(|_| anyhow::anyhow!("DateTime timestamp out of range for UnixNanos: {nanos}"))?;
682
683 Ok(UnixNanos::from(nanos))
684}
685
686#[cfg(test)]
687#[allow(
689 clippy::float_cmp,
690 reason = "Exact float comparisons acceptable in tests"
691)]
692mod tests {
693 use jiff::SignedDuration;
694 use proptest::prelude::*;
695 use rstest::rstest;
696
697 use super::*;
698
699 fn timestamp(value: &str) -> Timestamp {
700 value.parse().unwrap()
701 }
702
703 #[rstest]
704 #[case(0.0, 0)]
705 #[case(1.0, 1_000_000_000)]
706 #[case(1.1, 1_100_000_000)]
707 #[case(42.0, 42_000_000_000)]
708 #[case(0.000_123_5, 123_500)]
709 #[case(0.000_000_01, 10)]
710 #[case(0.000_000_001, 1)]
711 #[case(9.999_999_999, 9_999_999_999)]
712 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
713 let result = secs_to_nanos(value).unwrap();
714 assert_eq!(result, expected);
715 }
716
717 #[rstest]
718 #[case(0.0, 0)]
719 #[case(1.0, 1_000)]
720 #[case(1.1, 1_100)]
721 #[case(42.0, 42_000)]
722 #[case(0.012_34, 12)]
723 #[case(0.001, 1)]
724 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
725 let result = secs_to_millis(value).unwrap();
726 assert_eq!(result, expected);
727 }
728
729 #[rstest]
730 fn test_secs_to_nanos_unchecked_matches_checked() {
731 assert_eq!(secs_to_nanos_unchecked(1.1), secs_to_nanos(1.1).unwrap());
732 }
733
734 #[rstest]
735 fn test_secs_to_nanos_non_finite_errors() {
736 let err = secs_to_nanos(f64::NAN).unwrap_err();
737 assert!(err.to_string().contains("finite"));
738 }
739
740 #[rstest]
741 fn test_secs_to_millis_non_finite_errors() {
742 let err = secs_to_millis(f64::INFINITY).unwrap_err();
743 assert!(err.to_string().contains("finite"));
744 }
745
746 #[rstest]
747 fn test_millis_to_nanos_non_finite_errors() {
748 let err = millis_to_nanos(f64::NEG_INFINITY).unwrap_err();
749 assert!(err.to_string().contains("finite"));
750 }
751
752 #[rstest]
753 fn test_micros_to_nanos_non_finite_errors() {
754 let err = micros_to_nanos(f64::NAN).unwrap_err();
755 assert!(err.to_string().contains("finite"));
756 }
757
758 #[rstest]
759 #[case(0, 0)]
760 #[case(1, 60)]
761 #[case(5, 300)]
762 #[case(60, 3600)]
763 #[case(1440, 86400)]
764 fn test_mins_to_secs(#[case] mins: u64, #[case] expected: u64) {
765 assert_eq!(mins_to_secs(mins), expected);
766 }
767
768 #[rstest]
769 #[case(
770 checked_mins_to_secs,
771 307_445_734_561_825_860,
772 18_446_744_073_709_551_600
773 )]
774 fn test_checked_minutes_conversion_boundary(
775 #[case] convert: fn(u64) -> Option<u64>,
776 #[case] max: u64,
777 #[case] expected: u64,
778 ) {
779 assert_eq!(convert(max), Some(expected));
780 assert_eq!(convert(max + 1), None);
781 }
782
783 #[rstest]
784 #[should_panic(expected = "minutes to seconds conversion overflow")]
785 fn test_mins_to_secs_overflow_panics() {
786 let _ = mins_to_secs(307_445_734_561_825_861);
787 }
788
789 #[rstest]
790 #[case(
791 secs_to_nanos,
792 18_446_744_073.709_553,
793 18_446_744_073.709_55,
794 18_446_744_073_709_549_568
795 )]
796 #[case(
797 secs_to_millis,
798 18_446_744_073_709_550.0,
799 18_446_744_073_709_548.0,
800 18_446_744_073_709_547_520
801 )]
802 #[case(
803 millis_to_nanos,
804 18_446_744_073_709.55,
805 18_446_744_073_709.547,
806 18_446_744_073_709_547_520
807 )]
808 #[case(
809 micros_to_nanos,
810 18_446_744_073_709_550.0,
811 18_446_744_073_709_548.0,
812 18_446_744_073_709_547_520
813 )]
814 fn test_float_conversion_u64_boundary(
815 #[case] convert: fn(f64) -> anyhow::Result<u64>,
816 #[case] invalid: f64,
817 #[case] previous: f64,
818 #[case] expected: u64,
819 ) {
820 let err = convert(invalid).unwrap_err();
821 assert!(err.to_string().contains("out of range"));
822 assert_eq!(convert(previous).unwrap(), expected);
823 }
824
825 #[rstest]
826 fn test_secs_to_nanos_negative_infinity_errors() {
827 let result = secs_to_nanos(f64::NEG_INFINITY);
828 assert!(result.is_err());
829 }
830
831 #[rstest]
832 #[case(0.0, 0)]
833 #[case(1.0, 1_000_000)]
834 #[case(1.1, 1_100_000)]
835 #[case(42.0, 42_000_000)]
836 #[case(0.000_123_4, 123)]
837 #[case(0.000_01, 10)]
838 #[case(0.000_001, 1)]
839 #[case(9.999_999, 9_999_999)]
840 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
841 let result = millis_to_nanos(value).unwrap();
842 assert_eq!(result, expected);
843 }
844
845 #[rstest]
846 fn test_millis_to_nanos_unchecked_matches_checked() {
847 assert_eq!(
848 millis_to_nanos_unchecked(1.1),
849 millis_to_nanos(1.1).unwrap()
850 );
851 }
852
853 #[rstest]
854 #[case(0.0, 0)]
855 #[case(1.0, 1_000)]
856 #[case(1.1, 1_100)]
857 #[case(42.0, 42_000)]
858 #[case(0.1234, 123)]
859 #[case(0.01, 10)]
860 #[case(0.001, 1)]
861 #[case(9.999, 9_999)]
862 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
863 let result = micros_to_nanos(value).unwrap();
864 assert_eq!(result, expected);
865 }
866
867 #[rstest]
868 fn test_micros_to_nanos_unchecked_matches_checked() {
869 assert_eq!(
870 micros_to_nanos_unchecked(1.1),
871 micros_to_nanos(1.1).unwrap()
872 );
873 }
874
875 #[rstest]
876 #[case(0, 0.0)]
877 #[case(1, 1e-09)]
878 #[case(1_000_000_000, 1.0)]
879 #[case(42_897_123_111, 42.897_123_111)]
880 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
881 let result = nanos_to_secs(value);
882 assert_eq!(result, expected);
883 }
884
885 #[rstest]
886 #[case(0, 0)]
887 #[case(1_000_000, 1)]
888 #[case(1_000_000_000, 1000)]
889 #[case(42_897_123_111, 42897)]
890 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
891 let result = nanos_to_millis(value);
892 assert_eq!(result, expected);
893 }
894
895 #[rstest]
896 #[case(0, 0)]
897 #[case(1_000, 1)]
898 #[case(1_000_000_000, 1_000_000)]
899 #[case(42_897_123, 42_897)]
900 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
901 let result = nanos_to_micros(value);
902 assert_eq!(result, expected);
903 }
904
905 #[rstest]
906 #[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) {
915 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
916 assert_eq!(result, expected);
917 }
918
919 #[rstest]
920 #[case(0)]
921 #[case(1)]
922 #[case(951_782_400_123_456_789)]
923 #[case(1_609_459_199_999_999_999)]
924 #[case(i64::MAX as u64)]
925 #[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) {
928 let expected = format!(
929 "{:.9}",
930 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
931 );
932 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
933 assert_eq!(result, expected);
934 }
935
936 #[rstest]
937 #[case((i64::MAX as u64) + 1)]
938 #[case(u64::MAX)]
939 fn test_unix_nanos_to_iso8601_supports_full_unix_nanos_range(#[case] nanos: u64) {
940 let expected = format!(
941 "{:.9}",
942 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
943 );
944 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
945 assert_eq!(result, expected);
946 }
947
948 #[rstest]
949 #[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) {
956 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
957 assert_eq!(result, expected);
958 }
959
960 #[rstest]
961 #[case(0)]
962 #[case(951_782_400_123_456_789)]
963 #[case(1_609_459_199_999_999_999)]
964 #[case(i64::MAX as u64)]
965 fn test_unix_nanos_to_iso8601_millis_matches_jiff_oracle(#[case] nanos: u64) {
966 let expected = format!(
967 "{:.3}",
968 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
969 );
970 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
971 assert_eq!(result, expected);
972 }
973
974 #[rstest]
975 #[case((i64::MAX as u64) + 1)]
976 #[case(u64::MAX)]
977 fn test_unix_nanos_to_iso8601_millis_supports_full_unix_nanos_range(#[case] nanos: u64) {
978 let expected = format!(
979 "{:.3}",
980 Timestamp::from_nanosecond(i128::from(nanos)).unwrap()
981 );
982 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
983 assert_eq!(result, expected);
984 }
985
986 proptest! {
989 #[rstest]
990 fn prop_unix_nanos_to_iso8601_matches_jiff(nanos in any::<u64>()) {
991 let expected = format!(
992 "{:.9}",
993 Timestamp::from_nanosecond(i128::from(nanos)).unwrap(),
994 );
995 let actual = unix_nanos_to_iso8601(UnixNanos::from(nanos));
996 prop_assert_eq!(actual, expected);
997 }
998
999 #[rstest]
1000 fn prop_unix_nanos_to_iso8601_millis_matches_jiff(nanos in any::<u64>()) {
1001 let expected = format!(
1002 "{:.3}",
1003 Timestamp::from_nanosecond(i128::from(nanos)).unwrap(),
1004 );
1005 let actual = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
1006 prop_assert_eq!(actual, expected);
1007 }
1008 }
1009
1010 #[rstest]
1011 #[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(
1016 #[case] year: i32,
1017 #[case] month: u32,
1018 #[case] day: u32,
1019 #[case] expected: u64,
1020 ) {
1021 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
1022 assert_eq!(result, expected);
1023 }
1024
1025 #[rstest]
1026 fn test_last_closest_weekday_nanos_with_invalid_date() {
1027 let result = last_weekday_nanos(2023, 4, 31);
1028 assert!(result.is_err());
1029 }
1030
1031 #[rstest]
1032 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
1033 let result = last_weekday_nanos(2023, 2, 30);
1034 assert!(result.is_err());
1035 }
1036
1037 #[rstest]
1038 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
1039 let result = last_weekday_nanos(9999, 12, 31);
1040 assert!(result.is_err());
1041 }
1042
1043 #[rstest]
1044 fn test_is_within_last_24_hours_when_now() {
1045 let now_ns = Timestamp::now().as_nanosecond();
1046 assert!(is_within_last_24_hours(UnixNanos::from(u64::try_from(now_ns).unwrap())).unwrap());
1047 }
1048
1049 #[rstest]
1050 fn test_is_within_last_24_hours_when_two_days_ago() {
1051 let past_ns = (Timestamp::now() - SignedDuration::from_hours(48)).as_nanosecond();
1052 assert!(
1053 !is_within_last_24_hours(UnixNanos::from(u64::try_from(past_ns).unwrap())).unwrap()
1054 );
1055 }
1056
1057 #[rstest]
1058 fn test_is_within_last_24_hours_when_future() {
1059 let future_ns = (Timestamp::now() + SignedDuration::from_hours(1)).as_nanosecond();
1061 assert!(
1062 !is_within_last_24_hours(UnixNanos::from(u64::try_from(future_ns).unwrap())).unwrap()
1063 );
1064
1065 let future_ns = (Timestamp::now() + SignedDuration::from_hours(24)).as_nanosecond();
1067 assert!(
1068 !is_within_last_24_hours(UnixNanos::from(u64::try_from(future_ns).unwrap())).unwrap()
1069 );
1070 }
1071
1072 #[rstest]
1073 #[case(
1074 timestamp("2024-03-31T12:00:00Z"),
1075 1,
1076 timestamp("2024-02-29T12:00:00Z")
1077 )]
1078 #[case(
1079 timestamp("2024-03-31T12:00:00Z"),
1080 12,
1081 timestamp("2023-03-31T12:00:00Z")
1082 )]
1083 #[case(
1084 timestamp("2024-01-31T12:00:00Z"),
1085 1,
1086 timestamp("2023-12-31T12:00:00Z")
1087 )]
1088 #[case(
1089 timestamp("2024-03-31T12:00:00Z"),
1090 2,
1091 timestamp("2024-01-31T12:00:00Z")
1092 )]
1093 fn test_subtract_n_months(
1094 #[case] input: Timestamp,
1095 #[case] months: u32,
1096 #[case] expected: Timestamp,
1097 ) {
1098 let result = subtract_n_months(input, months).unwrap();
1099 assert_eq!(result, expected);
1100 }
1101
1102 #[rstest]
1103 #[case(
1104 timestamp("2023-02-28T12:00:00Z"),
1105 1,
1106 timestamp("2023-03-28T12:00:00Z")
1107 )]
1108 #[case(
1109 timestamp("2024-01-31T12:00:00Z"),
1110 1,
1111 timestamp("2024-02-29T12:00:00Z")
1112 )]
1113 #[case(
1114 timestamp("2023-12-31T12:00:00Z"),
1115 1,
1116 timestamp("2024-01-31T12:00:00Z")
1117 )]
1118 #[case(
1119 timestamp("2023-01-31T12:00:00Z"),
1120 13,
1121 timestamp("2024-02-29T12:00:00Z")
1122 )]
1123 fn test_add_n_months(
1124 #[case] input: Timestamp,
1125 #[case] months: u32,
1126 #[case] expected: Timestamp,
1127 ) {
1128 let result = add_n_months(input, months).unwrap();
1129 assert_eq!(result, expected);
1130 }
1131
1132 #[rstest]
1133 fn test_add_n_years_overflow() {
1134 let datetime = timestamp("2024-01-01T00:00:00Z");
1135 let err = add_n_years(datetime, u32::MAX).unwrap_err();
1136 assert!(err.to_string().contains("month count overflow"));
1137 }
1138
1139 #[rstest]
1140 fn test_subtract_n_years_overflow() {
1141 let datetime = timestamp("2024-01-01T00:00:00Z");
1142 let err = subtract_n_years(datetime, u32::MAX).unwrap_err();
1143 assert!(err.to_string().contains("month count overflow"));
1144 }
1145
1146 #[rstest]
1147 fn test_add_n_years_nanos_overflow() {
1148 let nanos = UnixNanos::from(0);
1149 let err = add_n_years_nanos(nanos, u32::MAX).unwrap_err();
1150 assert!(err.to_string().contains("month count overflow"));
1151 }
1152
1153 #[rstest]
1154 #[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) {
1163 let result = iso8601_to_unix_nanos(input).unwrap();
1164 assert_eq!(result.as_u64(), expected);
1165 }
1166
1167 #[rstest]
1168 #[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) {
1173 let result = iso8601_to_unix_nanos(input);
1174 assert!(result.is_err());
1175 }
1176
1177 #[rstest]
1178 fn test_iso8601_roundtrip() {
1179 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
1180 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
1181 let parsed_nanos = iso8601_to_unix_nanos(&iso8601_string).unwrap();
1182 assert_eq!(parsed_nanos, original_nanos);
1183 }
1184
1185 #[rstest]
1186 fn test_add_n_years_nanos_normal_case() {
1187 let start = UnixNanos::from(timestamp("2020-01-01T00:00:00Z"));
1189 let result = add_n_years_nanos(start, 1).unwrap();
1190 let expected = UnixNanos::from(timestamp("2021-01-01T00:00:00Z"));
1191 assert_eq!(result, expected);
1192 }
1193
1194 #[rstest]
1195 fn test_add_n_months_nanos_normal_case() {
1196 let start = UnixNanos::from(timestamp("2020-01-15T00:00:00Z"));
1198 let result = add_n_months_nanos(start, 1).unwrap();
1199 let expected = UnixNanos::from(timestamp("2020-02-15T00:00:00Z"));
1200 assert_eq!(result, expected);
1201 }
1202
1203 #[rstest]
1204 fn test_add_n_years_nanos_from_epoch() {
1205 let start = UnixNanos::from(0);
1207 let result = add_n_years_nanos(start, 1).unwrap();
1208 assert_eq!(result.as_u64(), 31_536_000_000_000_000);
1209 }
1210
1211 #[rstest]
1212 fn test_datetime_to_unix_nanos_at_epoch() {
1213 let epoch = Timestamp::UNIX_EPOCH;
1215 let result = datetime_to_unix_nanos(Some(epoch));
1216 assert_eq!(result, Some(UnixNanos::from(0)));
1217 }
1218
1219 #[rstest]
1220 fn test_datetime_to_unix_nanos_typical_datetime() {
1221 let dt = timestamp("2024-01-15T13:30:45.123456789Z");
1222 let result = datetime_to_unix_nanos(Some(dt));
1223
1224 assert!(result.is_some());
1226 assert_eq!(result.unwrap().as_u64(), 1_705_325_445_123_456_789);
1227 }
1228
1229 #[rstest]
1230 fn test_datetime_to_unix_nanos_before_epoch() {
1231 let before_epoch = timestamp("1969-12-31T23:59:59Z");
1234 let result = datetime_to_unix_nanos(Some(before_epoch));
1235 assert_eq!(result, None);
1236 }
1237
1238 #[rstest]
1239 fn test_datetime_to_unix_nanos_one_second_after_epoch() {
1240 let dt = Timestamp::from_second(1).unwrap();
1242 let result = datetime_to_unix_nanos(Some(dt));
1243 assert_eq!(result, Some(UnixNanos::from(1_000_000_000)));
1244 }
1245
1246 #[rstest]
1247 fn test_datetime_to_unix_nanos_with_subsecond_precision() {
1248 let dt = Timestamp::new(0, 1_000).unwrap(); let result = datetime_to_unix_nanos(Some(dt));
1251 assert_eq!(result, Some(UnixNanos::from(1_000)));
1252 }
1253
1254 #[rstest]
1255 fn test_try_datetime_to_unix_nanos_valid() {
1256 let dt = Timestamp::new(0, 1_000).unwrap();
1257 assert_eq!(
1258 try_datetime_to_unix_nanos(dt).unwrap(),
1259 UnixNanos::from(1_000)
1260 );
1261 }
1262
1263 #[rstest]
1264 fn test_try_datetime_to_unix_nanos_at_epoch() {
1265 assert_eq!(
1266 try_datetime_to_unix_nanos(Timestamp::UNIX_EPOCH).unwrap(),
1267 UnixNanos::from(0)
1268 );
1269 }
1270
1271 #[rstest]
1272 fn test_try_datetime_to_unix_nanos_before_epoch_errors() {
1273 let before_epoch = timestamp("1969-12-31T23:59:59Z");
1274 let err = try_datetime_to_unix_nanos(before_epoch).unwrap_err();
1275 assert!(
1276 err.to_string().contains("cannot be negative"),
1277 "unexpected error: {err}"
1278 );
1279 }
1280
1281 #[rstest]
1282 fn test_try_datetime_to_unix_nanos_out_of_range_errors() {
1283 let err = try_datetime_to_unix_nanos(Timestamp::MAX).unwrap_err();
1284 assert!(
1285 err.to_string().contains("out of range"),
1286 "unexpected error: {err}"
1287 );
1288 }
1289
1290 #[rstest]
1291 fn test_month_and_year_arithmetic_support_values_above_i64_max() {
1292 let large = UnixNanos::from(u64::MAX);
1293 assert!(subtract_n_months_nanos(large, 1).is_ok());
1294 assert!(add_n_months_nanos(large, 1).is_err());
1295 assert!(add_n_years_nanos(large, 1).is_err());
1296 assert!(subtract_n_years_nanos(large, 1).is_ok());
1297 }
1298
1299 #[rstest]
1300 fn test_subtract_n_months_nanos_pre_epoch_result_errors() {
1301 let epoch = UnixNanos::from(0);
1302 let err = subtract_n_months_nanos(epoch, 1).unwrap_err();
1303 assert_eq!(err.to_string(), "Negative timestamp not allowed");
1304 }
1305
1306 #[rstest]
1307 fn test_subtract_n_years_nanos_pre_epoch_result_errors() {
1308 let epoch = UnixNanos::from(0);
1309 let err = subtract_n_years_nanos(epoch, 1).unwrap_err();
1310 assert_eq!(err.to_string(), "Negative timestamp not allowed");
1311 }
1312
1313 #[rstest]
1314 fn test_subtract_n_months_nanos_at_epoch_boundary() {
1315 let epoch = UnixNanos::from(0);
1316 assert_eq!(subtract_n_months_nanos(epoch, 0).unwrap(), epoch);
1317 }
1318
1319 #[rstest]
1320 fn test_get_timezone_with_valid_name() {
1321 let tz = get_timezone("UTC").unwrap();
1322 assert_eq!(tz.iana_name(), Some("UTC"));
1323 }
1324
1325 #[rstest]
1326 fn test_get_timezone_with_unknown_name_errors() {
1327 assert!(get_timezone("Not/A_Zone").is_err());
1328 }
1329
1330 #[rstest]
1331 #[case(0, 0)]
1332 #[case(999, 0)]
1333 #[case(1_000, 1_000)]
1334 #[case(1_000_001, 1_000_000)]
1335 #[case(u64::MAX, 18_446_744_073_709_551_000)]
1336 fn test_floor_to_nearest_microsecond(#[case] input: u64, #[case] expected: u64) {
1337 assert_eq!(floor_to_nearest_microsecond(input), expected);
1338 }
1339}