1use std::convert::TryFrom;
18
19use chrono::{DateTime, Datelike, NaiveDate, SecondsFormat, TimeDelta, Utc, Weekday};
20
21use crate::UnixNanos;
22
23pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
25
26pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
28
29pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
31
32pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
34
35pub const WEEKDAYS: [Weekday; 5] = [
37 Weekday::Mon,
38 Weekday::Tue,
39 Weekday::Wed,
40 Weekday::Thu,
41 Weekday::Fri,
42];
43
44#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
49#[must_use]
50pub fn secs_to_nanos(secs: f64) -> u64 {
51 let nanos = secs * NANOSECONDS_IN_SECOND as f64;
52 nanos.max(0.0).trunc() as u64
53}
54
55#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
60#[must_use]
61pub fn secs_to_millis(secs: f64) -> u64 {
62 let millis = secs * MILLISECONDS_IN_SECOND as f64;
63 millis.max(0.0).trunc() as u64
64}
65
66#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
71#[must_use]
72pub fn millis_to_nanos(millis: f64) -> u64 {
73 let nanos = millis * NANOSECONDS_IN_MILLISECOND as f64;
74 nanos.max(0.0).trunc() as u64
75}
76
77#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
82#[must_use]
83pub fn micros_to_nanos(micros: f64) -> u64 {
84 let nanos = micros * NANOSECONDS_IN_MICROSECOND as f64;
85 nanos.max(0.0).trunc() as u64
86}
87
88#[allow(clippy::cast_precision_loss)]
93#[must_use]
94pub fn nanos_to_secs(nanos: u64) -> f64 {
95 let seconds = nanos / NANOSECONDS_IN_SECOND;
96 let rem_nanos = nanos % NANOSECONDS_IN_SECOND;
97 (seconds as f64) + (rem_nanos as f64) / (NANOSECONDS_IN_SECOND as f64)
98}
99
100#[must_use]
102pub const fn nanos_to_millis(nanos: u64) -> u64 {
103 nanos / NANOSECONDS_IN_MILLISECOND
104}
105
106#[must_use]
108pub const fn nanos_to_micros(nanos: u64) -> u64 {
109 nanos / NANOSECONDS_IN_MICROSECOND
110}
111
112#[inline]
114#[must_use]
115pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
116 let datetime = unix_nanos.to_datetime_utc();
117 datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
118}
119
120#[inline]
163pub fn iso8601_to_unix_nanos(date_string: String) -> anyhow::Result<UnixNanos> {
164 date_string
165 .parse::<UnixNanos>()
166 .map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{}': {}", date_string, e))
167}
168
169#[inline]
172#[must_use]
173pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
174 let datetime = unix_nanos.to_datetime_utc();
175 datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
176}
177
178#[must_use]
180pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
181 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
182}
183
184pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
190 let date =
191 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
192 let current_weekday = date.weekday().number_from_monday();
193
194 let offset = i64::from(match current_weekday {
196 1..=5 => 0, 6 => 1, _ => 2, });
200 let last_closest = date - TimeDelta::days(offset);
202
203 let unix_timestamp_ns = last_closest
205 .and_hms_nano_opt(0, 0, 0, 0)
206 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
207
208 let raw_ns = unix_timestamp_ns
210 .and_utc()
211 .timestamp_nanos_opt()
212 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))?;
213 let ns_u64 =
214 u64::try_from(raw_ns).map_err(|_| anyhow::anyhow!("Negative timestamp: {raw_ns}"))?;
215 Ok(UnixNanos::from(ns_u64))
216}
217
218pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
224 let timestamp_ns = timestamp_ns.as_u64();
225 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
226 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
227 let secs_i64 = i64::try_from(seconds)
229 .map_err(|_| anyhow::anyhow!("Timestamp seconds overflow: {seconds}"))?;
230 let timestamp = DateTime::from_timestamp(secs_i64, nanoseconds)
231 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
232 let now = Utc::now();
233
234 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
235}
236
237pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
243 match datetime.checked_sub_months(chrono::Months::new(n)) {
244 Some(result) => Ok(result),
245 None => anyhow::bail!("Failed to subtract {n} months from {datetime}"),
246 }
247}
248
249pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
255 match datetime.checked_add_months(chrono::Months::new(n)) {
256 Some(result) => Ok(result),
257 None => anyhow::bail!("Failed to add {n} months to {datetime}"),
258 }
259}
260
261pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
267 let datetime = unix_nanos.to_datetime_utc();
268 let result = subtract_n_months(datetime, n)?;
269 let timestamp = match result.timestamp_nanos_opt() {
270 Some(ts) => ts,
271 None => anyhow::bail!("Timestamp out of range after subtracting {n} months"),
272 };
273
274 if timestamp < 0 {
275 anyhow::bail!("Negative timestamp not allowed");
276 }
277
278 Ok(UnixNanos::from(timestamp as u64))
279}
280
281pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
287 let datetime = unix_nanos.to_datetime_utc();
288 let result = add_n_months(datetime, n)?;
289 let timestamp = match result.timestamp_nanos_opt() {
290 Some(ts) => ts,
291 None => anyhow::bail!("Timestamp out of range after adding {n} months"),
292 };
293
294 if timestamp < 0 {
295 anyhow::bail!("Negative timestamp not allowed");
296 }
297
298 Ok(UnixNanos::from(timestamp as u64))
299}
300
301pub fn add_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
307 match datetime.checked_add_months(chrono::Months::new(n * 12)) {
309 Some(result) => Ok(result),
310 None => anyhow::bail!("Failed to add {n} years to {datetime}"),
311 }
312}
313
314pub fn subtract_n_years(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
320 match datetime.checked_sub_months(chrono::Months::new(n * 12)) {
322 Some(result) => Ok(result),
323 None => anyhow::bail!("Failed to subtract {n} years from {datetime}"),
324 }
325}
326
327pub fn add_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
333 let datetime = unix_nanos.to_datetime_utc();
334 let result = add_n_years(datetime, n)?;
335 let timestamp = match result.timestamp_nanos_opt() {
336 Some(ts) => ts,
337 None => anyhow::bail!("Timestamp out of range after adding {n} years"),
338 };
339
340 Ok(UnixNanos::from(timestamp as u64))
341}
342
343pub fn subtract_n_years_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
349 let datetime = unix_nanos.to_datetime_utc();
350 let result = subtract_n_years(datetime, n)?;
351 let timestamp = match result.timestamp_nanos_opt() {
352 Some(ts) => ts,
353 None => anyhow::bail!("Timestamp out of range after subtracting {n} years"),
354 };
355
356 if timestamp < 0 {
357 anyhow::bail!("Negative timestamp not allowed");
358 }
359
360 Ok(UnixNanos::from(timestamp as u64))
361}
362
363#[must_use]
365pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
366 assert!(month >= 1 && month <= 12, "`month` must be in 1..=12");
368
369 match month {
371 2 => {
372 if is_leap_year(year) {
373 29
374 } else {
375 28
376 }
377 }
378 4 | 6 | 9 | 11 => 30,
379 _ => 31, }
381}
382
383#[must_use]
385pub const fn is_leap_year(year: i32) -> bool {
386 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
387}
388
389#[cfg(test)]
393#[allow(clippy::float_cmp)]
394mod tests {
395 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
396 use rstest::rstest;
397
398 use super::*;
399
400 #[rstest]
401 #[case(0.0, 0)]
402 #[case(1.0, 1_000_000_000)]
403 #[case(1.1, 1_100_000_000)]
404 #[case(42.0, 42_000_000_000)]
405 #[case(0.000_123_5, 123_500)]
406 #[case(0.000_000_01, 10)]
407 #[case(0.000_000_001, 1)]
408 #[case(9.999_999_999, 9_999_999_999)]
409 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
410 let result = secs_to_nanos(value);
411 assert_eq!(result, expected);
412 }
413
414 #[rstest]
415 #[case(0.0, 0)]
416 #[case(1.0, 1_000)]
417 #[case(1.1, 1_100)]
418 #[case(42.0, 42_000)]
419 #[case(0.012_34, 12)]
420 #[case(0.001, 1)]
421 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
422 let result = secs_to_millis(value);
423 assert_eq!(result, expected);
424 }
425
426 #[rstest]
427 #[should_panic(expected = "`month` must be in 1..=12")]
428 fn test_last_day_of_month_invalid_month() {
429 let _ = last_day_of_month(2024, 0);
430 }
431
432 #[rstest]
433 #[case(0.0, 0)]
434 #[case(1.0, 1_000_000)]
435 #[case(1.1, 1_100_000)]
436 #[case(42.0, 42_000_000)]
437 #[case(0.000_123_4, 123)]
438 #[case(0.000_01, 10)]
439 #[case(0.000_001, 1)]
440 #[case(9.999_999, 9_999_999)]
441 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
442 let result = millis_to_nanos(value);
443 assert_eq!(result, expected);
444 }
445
446 #[rstest]
447 #[case(0.0, 0)]
448 #[case(1.0, 1_000)]
449 #[case(1.1, 1_100)]
450 #[case(42.0, 42_000)]
451 #[case(0.1234, 123)]
452 #[case(0.01, 10)]
453 #[case(0.001, 1)]
454 #[case(9.999, 9_999)]
455 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
456 let result = micros_to_nanos(value);
457 assert_eq!(result, expected);
458 }
459
460 #[rstest]
461 #[case(0, 0.0)]
462 #[case(1, 1e-09)]
463 #[case(1_000_000_000, 1.0)]
464 #[case(42_897_123_111, 42.897_123_111)]
465 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
466 let result = nanos_to_secs(value);
467 assert_eq!(result, expected);
468 }
469
470 #[rstest]
471 #[case(0, 0)]
472 #[case(1_000_000, 1)]
473 #[case(1_000_000_000, 1000)]
474 #[case(42_897_123_111, 42897)]
475 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
476 let result = nanos_to_millis(value);
477 assert_eq!(result, expected);
478 }
479
480 #[rstest]
481 #[case(0, 0)]
482 #[case(1_000, 1)]
483 #[case(1_000_000_000, 1_000_000)]
484 #[case(42_897_123, 42_897)]
485 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
486 let result = nanos_to_micros(value);
487 assert_eq!(result, expected);
488 }
489
490 #[rstest]
491 #[case(0, "1970-01-01T00:00:00.000000000Z")] #[case(1, "1970-01-01T00:00:00.000000001Z")] #[case(1_000, "1970-01-01T00:00:00.000001000Z")] #[case(1_000_000, "1970-01-01T00:00:00.001000000Z")] #[case(1_000_000_000, "1970-01-01T00:00:01.000000000Z")] #[case(1_702_857_600_000_000_000, "2023-12-18T00:00:00.000000000Z")] fn test_unix_nanos_to_iso8601(#[case] nanos: u64, #[case] expected: &str) {
498 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
499 assert_eq!(result, expected);
500 }
501
502 #[rstest]
503 #[case(0, "1970-01-01T00:00:00.000Z")] #[case(1_000_000, "1970-01-01T00:00:00.001Z")] #[case(1_000_000_000, "1970-01-01T00:00:01.000Z")] #[case(1_702_857_600_123_456_789, "2023-12-18T00:00:00.123Z")] fn test_unix_nanos_to_iso8601_millis(#[case] nanos: u64, #[case] expected: &str) {
508 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
509 assert_eq!(result, expected);
510 }
511
512 #[rstest]
513 #[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(
518 #[case] year: i32,
519 #[case] month: u32,
520 #[case] day: u32,
521 #[case] expected: u64,
522 ) {
523 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
524 assert_eq!(result, expected);
525 }
526
527 #[rstest]
528 fn test_last_closest_weekday_nanos_with_invalid_date() {
529 let result = last_weekday_nanos(2023, 4, 31);
530 assert!(result.is_err());
531 }
532
533 #[rstest]
534 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
535 let result = last_weekday_nanos(2023, 2, 30);
536 assert!(result.is_err());
537 }
538
539 #[rstest]
540 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
541 let result = last_weekday_nanos(9999, 12, 31);
542 assert!(result.is_err());
543 }
544
545 #[rstest]
546 fn test_is_within_last_24_hours_when_now() {
547 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
548 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
549 }
550
551 #[rstest]
552 fn test_is_within_last_24_hours_when_two_days_ago() {
553 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
554 .timestamp_nanos_opt()
555 .unwrap();
556 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
557 }
558
559 #[rstest]
560 #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 12, Utc.with_ymd_and_hms(2023, 3, 31, 12, 0, 0).unwrap())] #[case(Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2023, 12, 31, 12, 0, 0).unwrap())] #[case(Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap(), 2, Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap())] fn test_subtract_n_months(
565 #[case] input: DateTime<Utc>,
566 #[case] months: u32,
567 #[case] expected: DateTime<Utc>,
568 ) {
569 let result = subtract_n_months(input, months).unwrap();
570 assert_eq!(result, expected);
571 }
572
573 #[rstest]
574 #[case(Utc.with_ymd_and_hms(2023, 2, 28, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2023, 3, 28, 12, 0, 0).unwrap())] #[case(Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] #[case(Utc.with_ymd_and_hms(2023, 12, 31, 12, 0, 0).unwrap(), 1, Utc.with_ymd_and_hms(2024, 1, 31, 12, 0, 0).unwrap())] #[case(Utc.with_ymd_and_hms(2023, 1, 31, 12, 0, 0).unwrap(), 13, Utc.with_ymd_and_hms(2024, 2, 29, 12, 0, 0).unwrap())] fn test_add_n_months(
579 #[case] input: DateTime<Utc>,
580 #[case] months: u32,
581 #[case] expected: DateTime<Utc>,
582 ) {
583 let result = add_n_months(input, months).unwrap();
584 assert_eq!(result, expected);
585 }
586
587 #[rstest]
588 #[case(2024, 2, 29)] #[case(2023, 2, 28)] #[case(2024, 12, 31)] #[case(2023, 11, 30)] fn test_last_day_of_month(#[case] year: i32, #[case] month: u32, #[case] expected: u32) {
593 let result = last_day_of_month(year, month);
594 assert_eq!(result, expected);
595 }
596
597 #[rstest]
598 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
603 let result = is_leap_year(year);
604 assert_eq!(result, expected);
605 }
606
607 #[rstest]
608 #[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) {
617 let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
618 assert_eq!(result.as_u64(), expected);
619 }
620
621 #[rstest]
622 #[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) {
627 let result = iso8601_to_unix_nanos(input.to_string());
628 assert!(result.is_err());
629 }
630
631 #[rstest]
632 fn test_iso8601_roundtrip() {
633 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
634 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
635 let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
636 assert_eq!(parsed_nanos, original_nanos);
637 }
638}