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
301#[must_use]
303pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
304 assert!(month >= 1 && month <= 12, "`month` must be in 1..=12");
306
307 match month {
309 2 => {
310 if is_leap_year(year) {
311 29
312 } else {
313 28
314 }
315 }
316 4 | 6 | 9 | 11 => 30,
317 _ => 31, }
319}
320
321#[must_use]
323pub const fn is_leap_year(year: i32) -> bool {
324 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
325}
326
327#[cfg(test)]
331#[allow(clippy::float_cmp)]
332mod tests {
333 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
334 use rstest::rstest;
335
336 use super::*;
337
338 #[rstest]
339 #[case(0.0, 0)]
340 #[case(1.0, 1_000_000_000)]
341 #[case(1.1, 1_100_000_000)]
342 #[case(42.0, 42_000_000_000)]
343 #[case(0.000_123_5, 123_500)]
344 #[case(0.000_000_01, 10)]
345 #[case(0.000_000_001, 1)]
346 #[case(9.999_999_999, 9_999_999_999)]
347 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
348 let result = secs_to_nanos(value);
349 assert_eq!(result, expected);
350 }
351
352 #[rstest]
353 #[case(0.0, 0)]
354 #[case(1.0, 1_000)]
355 #[case(1.1, 1_100)]
356 #[case(42.0, 42_000)]
357 #[case(0.012_34, 12)]
358 #[case(0.001, 1)]
359 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
360 let result = secs_to_millis(value);
361 assert_eq!(result, expected);
362 }
363
364 #[rstest]
365 #[should_panic(expected = "`month` must be in 1..=12")]
366 fn test_last_day_of_month_invalid_month() {
367 let _ = last_day_of_month(2024, 0);
368 }
369
370 #[rstest]
371 #[case(0.0, 0)]
372 #[case(1.0, 1_000_000)]
373 #[case(1.1, 1_100_000)]
374 #[case(42.0, 42_000_000)]
375 #[case(0.000_123_4, 123)]
376 #[case(0.000_01, 10)]
377 #[case(0.000_001, 1)]
378 #[case(9.999_999, 9_999_999)]
379 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
380 let result = millis_to_nanos(value);
381 assert_eq!(result, expected);
382 }
383
384 #[rstest]
385 #[case(0.0, 0)]
386 #[case(1.0, 1_000)]
387 #[case(1.1, 1_100)]
388 #[case(42.0, 42_000)]
389 #[case(0.1234, 123)]
390 #[case(0.01, 10)]
391 #[case(0.001, 1)]
392 #[case(9.999, 9_999)]
393 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
394 let result = micros_to_nanos(value);
395 assert_eq!(result, expected);
396 }
397
398 #[rstest]
399 #[case(0, 0.0)]
400 #[case(1, 1e-09)]
401 #[case(1_000_000_000, 1.0)]
402 #[case(42_897_123_111, 42.897_123_111)]
403 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
404 let result = nanos_to_secs(value);
405 assert_eq!(result, expected);
406 }
407
408 #[rstest]
409 #[case(0, 0)]
410 #[case(1_000_000, 1)]
411 #[case(1_000_000_000, 1000)]
412 #[case(42_897_123_111, 42897)]
413 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
414 let result = nanos_to_millis(value);
415 assert_eq!(result, expected);
416 }
417
418 #[rstest]
419 #[case(0, 0)]
420 #[case(1_000, 1)]
421 #[case(1_000_000_000, 1_000_000)]
422 #[case(42_897_123, 42_897)]
423 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
424 let result = nanos_to_micros(value);
425 assert_eq!(result, expected);
426 }
427
428 #[rstest]
429 #[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) {
436 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
437 assert_eq!(result, expected);
438 }
439
440 #[rstest]
441 #[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) {
446 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
447 assert_eq!(result, expected);
448 }
449
450 #[rstest]
451 #[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(
456 #[case] year: i32,
457 #[case] month: u32,
458 #[case] day: u32,
459 #[case] expected: u64,
460 ) {
461 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
462 assert_eq!(result, expected);
463 }
464
465 #[rstest]
466 fn test_last_closest_weekday_nanos_with_invalid_date() {
467 let result = last_weekday_nanos(2023, 4, 31);
468 assert!(result.is_err());
469 }
470
471 #[rstest]
472 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
473 let result = last_weekday_nanos(2023, 2, 30);
474 assert!(result.is_err());
475 }
476
477 #[rstest]
478 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
479 let result = last_weekday_nanos(9999, 12, 31);
480 assert!(result.is_err());
481 }
482
483 #[rstest]
484 fn test_is_within_last_24_hours_when_now() {
485 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
486 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
487 }
488
489 #[rstest]
490 fn test_is_within_last_24_hours_when_two_days_ago() {
491 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
492 .timestamp_nanos_opt()
493 .unwrap();
494 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
495 }
496
497 #[rstest]
498 #[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(
503 #[case] input: DateTime<Utc>,
504 #[case] months: u32,
505 #[case] expected: DateTime<Utc>,
506 ) {
507 let result = subtract_n_months(input, months).unwrap();
508 assert_eq!(result, expected);
509 }
510
511 #[rstest]
512 #[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(
517 #[case] input: DateTime<Utc>,
518 #[case] months: u32,
519 #[case] expected: DateTime<Utc>,
520 ) {
521 let result = add_n_months(input, months).unwrap();
522 assert_eq!(result, expected);
523 }
524
525 #[rstest]
526 #[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) {
531 let result = last_day_of_month(year, month);
532 assert_eq!(result, expected);
533 }
534
535 #[rstest]
536 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
541 let result = is_leap_year(year);
542 assert_eq!(result, expected);
543 }
544
545 #[rstest]
546 #[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) {
555 let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
556 assert_eq!(result.as_u64(), expected);
557 }
558
559 #[rstest]
560 #[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) {
565 let result = iso8601_to_unix_nanos(input.to_string());
566 assert!(result.is_err());
567 }
568
569 #[rstest]
570 fn test_iso8601_roundtrip() {
571 let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
572 let iso8601_string = unix_nanos_to_iso8601(original_nanos);
573 let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
574 assert_eq!(parsed_nanos, original_nanos);
575 }
576}