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]
123#[must_use]
124pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
125 let datetime = unix_nanos.to_datetime_utc();
126 datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
127}
128
129#[must_use]
131pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
132 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
133}
134
135pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
141 let date =
142 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
143 let current_weekday = date.weekday().number_from_monday();
144
145 let offset = i64::from(match current_weekday {
147 1..=5 => 0, 6 => 1, _ => 2, });
151 let last_closest = date - TimeDelta::days(offset);
153
154 let unix_timestamp_ns = last_closest
156 .and_hms_nano_opt(0, 0, 0, 0)
157 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
158
159 let raw_ns = unix_timestamp_ns
161 .and_utc()
162 .timestamp_nanos_opt()
163 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))?;
164 let ns_u64 =
165 u64::try_from(raw_ns).map_err(|_| anyhow::anyhow!("Negative timestamp: {raw_ns}"))?;
166 Ok(UnixNanos::from(ns_u64))
167}
168
169pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
175 let timestamp_ns = timestamp_ns.as_u64();
176 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
177 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
178 let secs_i64 = i64::try_from(seconds)
180 .map_err(|_| anyhow::anyhow!("Timestamp seconds overflow: {seconds}"))?;
181 let timestamp = DateTime::from_timestamp(secs_i64, nanoseconds)
182 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
183 let now = Utc::now();
184
185 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
186}
187
188pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
194 match datetime.checked_sub_months(chrono::Months::new(n)) {
195 Some(result) => Ok(result),
196 None => anyhow::bail!("Failed to subtract {n} months from {datetime}"),
197 }
198}
199
200pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> anyhow::Result<DateTime<Utc>> {
206 match datetime.checked_add_months(chrono::Months::new(n)) {
207 Some(result) => Ok(result),
208 None => anyhow::bail!("Failed to add {n} months to {datetime}"),
209 }
210}
211
212pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
218 let datetime = unix_nanos.to_datetime_utc();
219 let result = subtract_n_months(datetime, n)?;
220 let timestamp = match result.timestamp_nanos_opt() {
221 Some(ts) => ts,
222 None => anyhow::bail!("Timestamp out of range after subtracting {n} months"),
223 };
224
225 if timestamp < 0 {
226 anyhow::bail!("Negative timestamp not allowed");
227 }
228
229 Ok(UnixNanos::from(timestamp as u64))
230}
231
232pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> anyhow::Result<UnixNanos> {
238 let datetime = unix_nanos.to_datetime_utc();
239 let result = add_n_months(datetime, n)?;
240 let timestamp = match result.timestamp_nanos_opt() {
241 Some(ts) => ts,
242 None => anyhow::bail!("Timestamp out of range after adding {n} months"),
243 };
244
245 if timestamp < 0 {
246 anyhow::bail!("Negative timestamp not allowed");
247 }
248
249 Ok(UnixNanos::from(timestamp as u64))
250}
251
252#[must_use]
254pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
255 assert!(month >= 1 && month <= 12, "`month` must be in 1..=12");
257
258 match month {
260 2 => {
261 if is_leap_year(year) {
262 29
263 } else {
264 28
265 }
266 }
267 4 | 6 | 9 | 11 => 30,
268 _ => 31, }
270}
271
272#[must_use]
274pub const fn is_leap_year(year: i32) -> bool {
275 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
276}
277
278#[cfg(test)]
282#[allow(clippy::float_cmp)]
283mod tests {
284 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
285 use rstest::rstest;
286
287 use super::*;
288
289 #[rstest]
290 #[case(0.0, 0)]
291 #[case(1.0, 1_000_000_000)]
292 #[case(1.1, 1_100_000_000)]
293 #[case(42.0, 42_000_000_000)]
294 #[case(0.000_123_5, 123_500)]
295 #[case(0.000_000_01, 10)]
296 #[case(0.000_000_001, 1)]
297 #[case(9.999_999_999, 9_999_999_999)]
298 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
299 let result = secs_to_nanos(value);
300 assert_eq!(result, expected);
301 }
302
303 #[rstest]
304 #[case(0.0, 0)]
305 #[case(1.0, 1_000)]
306 #[case(1.1, 1_100)]
307 #[case(42.0, 42_000)]
308 #[case(0.012_34, 12)]
309 #[case(0.001, 1)]
310 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
311 let result = secs_to_millis(value);
312 assert_eq!(result, expected);
313 }
314
315 #[rstest]
316 #[should_panic(expected = "`month` must be in 1..=12")]
317 fn test_last_day_of_month_invalid_month() {
318 let _ = last_day_of_month(2024, 0);
319 }
320
321 #[rstest]
322 #[case(0.0, 0)]
323 #[case(1.0, 1_000_000)]
324 #[case(1.1, 1_100_000)]
325 #[case(42.0, 42_000_000)]
326 #[case(0.000_123_4, 123)]
327 #[case(0.000_01, 10)]
328 #[case(0.000_001, 1)]
329 #[case(9.999_999, 9_999_999)]
330 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
331 let result = millis_to_nanos(value);
332 assert_eq!(result, expected);
333 }
334
335 #[rstest]
336 #[case(0.0, 0)]
337 #[case(1.0, 1_000)]
338 #[case(1.1, 1_100)]
339 #[case(42.0, 42_000)]
340 #[case(0.1234, 123)]
341 #[case(0.01, 10)]
342 #[case(0.001, 1)]
343 #[case(9.999, 9_999)]
344 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
345 let result = micros_to_nanos(value);
346 assert_eq!(result, expected);
347 }
348
349 #[rstest]
350 #[case(0, 0.0)]
351 #[case(1, 1e-09)]
352 #[case(1_000_000_000, 1.0)]
353 #[case(42_897_123_111, 42.897_123_111)]
354 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
355 let result = nanos_to_secs(value);
356 assert_eq!(result, expected);
357 }
358
359 #[rstest]
360 #[case(0, 0)]
361 #[case(1_000_000, 1)]
362 #[case(1_000_000_000, 1000)]
363 #[case(42_897_123_111, 42897)]
364 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
365 let result = nanos_to_millis(value);
366 assert_eq!(result, expected);
367 }
368
369 #[rstest]
370 #[case(0, 0)]
371 #[case(1_000, 1)]
372 #[case(1_000_000_000, 1_000_000)]
373 #[case(42_897_123, 42_897)]
374 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
375 let result = nanos_to_micros(value);
376 assert_eq!(result, expected);
377 }
378
379 #[rstest]
380 #[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) {
387 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
388 assert_eq!(result, expected);
389 }
390
391 #[rstest]
392 #[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) {
397 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
398 assert_eq!(result, expected);
399 }
400
401 #[rstest]
402 #[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(
407 #[case] year: i32,
408 #[case] month: u32,
409 #[case] day: u32,
410 #[case] expected: u64,
411 ) {
412 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
413 assert_eq!(result, expected);
414 }
415
416 #[rstest]
417 fn test_last_closest_weekday_nanos_with_invalid_date() {
418 let result = last_weekday_nanos(2023, 4, 31);
419 assert!(result.is_err());
420 }
421
422 #[rstest]
423 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
424 let result = last_weekday_nanos(2023, 2, 30);
425 assert!(result.is_err());
426 }
427
428 #[rstest]
429 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
430 let result = last_weekday_nanos(9999, 12, 31);
431 assert!(result.is_err());
432 }
433
434 #[rstest]
435 fn test_is_within_last_24_hours_when_now() {
436 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
437 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
438 }
439
440 #[rstest]
441 fn test_is_within_last_24_hours_when_two_days_ago() {
442 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
443 .timestamp_nanos_opt()
444 .unwrap();
445 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
446 }
447
448 #[rstest]
449 #[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(
454 #[case] input: DateTime<Utc>,
455 #[case] months: u32,
456 #[case] expected: DateTime<Utc>,
457 ) {
458 let result = subtract_n_months(input, months).unwrap();
459 assert_eq!(result, expected);
460 }
461
462 #[rstest]
463 #[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(
468 #[case] input: DateTime<Utc>,
469 #[case] months: u32,
470 #[case] expected: DateTime<Utc>,
471 ) {
472 let result = add_n_months(input, months).unwrap();
473 assert_eq!(result, expected);
474 }
475
476 #[rstest]
477 #[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) {
482 let result = last_day_of_month(year, month);
483 assert_eq!(result, expected);
484 }
485
486 #[rstest]
487 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
492 let result = is_leap_year(year);
493 assert_eq!(result, expected);
494 }
495}