1use chrono::{DateTime, Datelike, NaiveDate, SecondsFormat, TimeDelta, Utc, Weekday};
19
20use crate::UnixNanos;
21
22pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
24
25pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
27
28pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
30
31pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
33
34pub const WEEKDAYS: [Weekday; 5] = [
36 Weekday::Mon,
37 Weekday::Tue,
38 Weekday::Wed,
39 Weekday::Thu,
40 Weekday::Fri,
41];
42
43#[unsafe(no_mangle)]
45pub extern "C" fn secs_to_nanos(secs: f64) -> u64 {
46 (secs * NANOSECONDS_IN_SECOND as f64) as u64
47}
48
49#[unsafe(no_mangle)]
51pub extern "C" fn secs_to_millis(secs: f64) -> u64 {
52 (secs * MILLISECONDS_IN_SECOND as f64) as u64
53}
54
55#[unsafe(no_mangle)]
57pub extern "C" fn millis_to_nanos(millis: f64) -> u64 {
58 (millis * NANOSECONDS_IN_MILLISECOND as f64) as u64
59}
60
61#[unsafe(no_mangle)]
63pub extern "C" fn micros_to_nanos(micros: f64) -> u64 {
64 (micros * NANOSECONDS_IN_MICROSECOND as f64) as u64
65}
66
67#[unsafe(no_mangle)]
69pub extern "C" fn nanos_to_secs(nanos: u64) -> f64 {
70 nanos as f64 / NANOSECONDS_IN_SECOND as f64
71}
72
73#[unsafe(no_mangle)]
75pub const extern "C" fn nanos_to_millis(nanos: u64) -> u64 {
76 nanos / NANOSECONDS_IN_MILLISECOND
77}
78
79#[unsafe(no_mangle)]
81pub const extern "C" fn nanos_to_micros(nanos: u64) -> u64 {
82 nanos / NANOSECONDS_IN_MICROSECOND
83}
84
85#[inline]
87#[must_use]
88pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
89 let datetime = unix_nanos.to_datetime_utc();
90 datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
91}
92
93#[inline]
96#[must_use]
97pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
98 let datetime = unix_nanos.to_datetime_utc();
99 datetime.to_rfc3339_opts(SecondsFormat::Millis, true)
100}
101
102#[must_use]
104pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
105 (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
106}
107
108pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
114 let date =
115 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
116 let current_weekday = date.weekday().number_from_monday();
117
118 let offset = i64::from(match current_weekday {
120 1..=5 => 0, 6 => 1, _ => 2, });
124 let last_closest = date - TimeDelta::days(offset);
126
127 let unix_timestamp_ns = last_closest
129 .and_hms_nano_opt(0, 0, 0, 0)
130 .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
131
132 Ok(UnixNanos::from(
133 unix_timestamp_ns
134 .and_utc()
135 .timestamp_nanos_opt()
136 .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))? as u64,
137 ))
138}
139
140pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
146 let timestamp_ns = timestamp_ns.as_u64();
147 let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
148 let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
149 let timestamp = DateTime::from_timestamp(seconds as i64, nanoseconds)
150 .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
151 let now = Utc::now();
152
153 Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
154}
155
156#[must_use]
158pub fn subtract_n_months(datetime: DateTime<Utc>, n: u32) -> DateTime<Utc> {
159 datetime
160 .checked_sub_months(chrono::Months::new(n))
161 .expect("Failed to subtract months")
162}
163
164#[must_use]
166pub fn add_n_months(datetime: DateTime<Utc>, n: u32) -> DateTime<Utc> {
167 datetime
168 .checked_add_months(chrono::Months::new(n))
169 .expect("Failed to add months")
170}
171
172#[must_use]
174pub fn subtract_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> UnixNanos {
175 let datetime = unix_nanos.to_datetime_utc();
176 (subtract_n_months(datetime, n)
177 .timestamp_nanos_opt()
178 .expect("Months should be within 584 years") as u64)
179 .into()
180}
181
182#[must_use]
184pub fn add_n_months_nanos(unix_nanos: UnixNanos, n: u32) -> UnixNanos {
185 let date = unix_nanos.to_datetime_utc();
186 (add_n_months(date, n)
187 .timestamp_nanos_opt()
188 .expect("Months should be within 584 years") as u64)
189 .into()
190}
191
192#[must_use]
194pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
195 match month {
197 1 => 31,
198 2 => {
199 if is_leap_year(year) {
200 29
201 } else {
202 28
203 }
204 }
205 3 => 31,
206 4 => 30,
207 5 => 31,
208 6 => 30,
209 7 => 31,
210 8 => 31,
211 9 => 30,
212 10 => 31,
213 11 => 30,
214 12 => 31,
215 _ => 31, }
217}
218
219#[must_use]
221pub const fn is_leap_year(year: i32) -> bool {
222 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
223}
224
225#[cfg(test)]
229mod tests {
230 use chrono::{DateTime, TimeDelta, TimeZone, Utc};
231 use rstest::rstest;
232
233 use super::*;
234
235 #[rstest]
236 #[case(0.0, 0)]
237 #[case(1.0, 1_000_000_000)]
238 #[case(1.1, 1_100_000_000)]
239 #[case(42.0, 42_000_000_000)]
240 #[case(0.000_123_5, 123_500)]
241 #[case(0.000_000_01, 10)]
242 #[case(0.000_000_001, 1)]
243 #[case(9.999_999_999, 9_999_999_999)]
244 fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
245 let result = secs_to_nanos(value);
246 assert_eq!(result, expected);
247 }
248
249 #[rstest]
250 #[case(0.0, 0)]
251 #[case(1.0, 1_000)]
252 #[case(1.1, 1_100)]
253 #[case(42.0, 42_000)]
254 #[case(0.012_34, 12)]
255 #[case(0.001, 1)]
256 fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
257 let result = secs_to_millis(value);
258 assert_eq!(result, expected);
259 }
260
261 #[rstest]
262 #[case(0.0, 0)]
263 #[case(1.0, 1_000_000)]
264 #[case(1.1, 1_100_000)]
265 #[case(42.0, 42_000_000)]
266 #[case(0.000_123_4, 123)]
267 #[case(0.000_01, 10)]
268 #[case(0.000_001, 1)]
269 #[case(9.999_999, 9_999_999)]
270 fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
271 let result = millis_to_nanos(value);
272 assert_eq!(result, expected);
273 }
274
275 #[rstest]
276 #[case(0.0, 0)]
277 #[case(1.0, 1_000)]
278 #[case(1.1, 1_100)]
279 #[case(42.0, 42_000)]
280 #[case(0.1234, 123)]
281 #[case(0.01, 10)]
282 #[case(0.001, 1)]
283 #[case(9.999, 9_999)]
284 fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
285 let result = micros_to_nanos(value);
286 assert_eq!(result, expected);
287 }
288
289 #[rstest]
290 #[case(0, 0.0)]
291 #[case(1, 1e-09)]
292 #[case(1_000_000_000, 1.0)]
293 #[case(42_897_123_111, 42.897_123_111)]
294 fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
295 let result = nanos_to_secs(value);
296 assert_eq!(result, expected);
297 }
298
299 #[rstest]
300 #[case(0, 0)]
301 #[case(1_000_000, 1)]
302 #[case(1_000_000_000, 1000)]
303 #[case(42_897_123_111, 42897)]
304 fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
305 let result = nanos_to_millis(value);
306 assert_eq!(result, expected);
307 }
308
309 #[rstest]
310 #[case(0, 0)]
311 #[case(1_000, 1)]
312 #[case(1_000_000_000, 1_000_000)]
313 #[case(42_897_123, 42_897)]
314 fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
315 let result = nanos_to_micros(value);
316 assert_eq!(result, expected);
317 }
318
319 #[rstest]
320 #[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) {
327 let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
328 assert_eq!(result, expected);
329 }
330
331 #[rstest]
332 #[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) {
337 let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
338 assert_eq!(result, expected);
339 }
340
341 #[rstest]
342 #[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(
347 #[case] year: i32,
348 #[case] month: u32,
349 #[case] day: u32,
350 #[case] expected: u64,
351 ) {
352 let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
353 assert_eq!(result, expected);
354 }
355
356 #[rstest]
357 fn test_last_closest_weekday_nanos_with_invalid_date() {
358 let result = last_weekday_nanos(2023, 4, 31);
359 assert!(result.is_err());
360 }
361
362 #[rstest]
363 fn test_last_closest_weekday_nanos_with_nonexistent_date() {
364 let result = last_weekday_nanos(2023, 2, 30);
365 assert!(result.is_err());
366 }
367
368 #[rstest]
369 fn test_last_closest_weekday_nanos_with_invalid_conversion() {
370 let result = last_weekday_nanos(9999, 12, 31);
371 assert!(result.is_err());
372 }
373
374 #[rstest]
375 fn test_is_within_last_24_hours_when_now() {
376 let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
377 assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
378 }
379
380 #[rstest]
381 fn test_is_within_last_24_hours_when_two_days_ago() {
382 let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
383 .timestamp_nanos_opt()
384 .unwrap();
385 assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
386 }
387
388 #[rstest]
389 #[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(
394 #[case] input: DateTime<Utc>,
395 #[case] months: u32,
396 #[case] expected: DateTime<Utc>,
397 ) {
398 let result = subtract_n_months(input, months);
399 assert_eq!(result, expected);
400 }
401
402 #[rstest]
403 #[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(
408 #[case] input: DateTime<Utc>,
409 #[case] months: u32,
410 #[case] expected: DateTime<Utc>,
411 ) {
412 let result = add_n_months(input, months);
413 assert_eq!(result, expected);
414 }
415
416 #[rstest]
417 #[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) {
422 let result = last_day_of_month(year, month);
423 assert_eq!(result, expected);
424 }
425
426 #[rstest]
427 #[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
432 let result = is_leap_year(year);
433 assert_eq!(result, expected);
434 }
435}