use std::time::{Duration, UNIX_EPOCH};
use chrono::{DateTime, Datelike, NaiveDate, SecondsFormat, TimeDelta, Timelike, Utc, Weekday};
use crate::UnixNanos;
pub const MILLISECONDS_IN_SECOND: u64 = 1_000;
pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
pub const NANOSECONDS_IN_MILLISECOND: u64 = 1_000_000;
pub const NANOSECONDS_IN_MICROSECOND: u64 = 1_000;
pub const WEEKDAYS: [Weekday; 5] = [
Weekday::Mon,
Weekday::Tue,
Weekday::Wed,
Weekday::Thu,
Weekday::Fri,
];
#[inline]
#[no_mangle]
pub extern "C" fn secs_to_nanos(secs: f64) -> u64 {
(secs * NANOSECONDS_IN_SECOND as f64) as u64
}
#[inline]
#[no_mangle]
pub extern "C" fn secs_to_millis(secs: f64) -> u64 {
(secs * MILLISECONDS_IN_SECOND as f64) as u64
}
#[inline]
#[no_mangle]
pub extern "C" fn millis_to_nanos(millis: f64) -> u64 {
(millis * NANOSECONDS_IN_MILLISECOND as f64) as u64
}
#[inline]
#[no_mangle]
pub extern "C" fn micros_to_nanos(micros: f64) -> u64 {
(micros * NANOSECONDS_IN_MICROSECOND as f64) as u64
}
#[inline]
#[no_mangle]
pub extern "C" fn nanos_to_secs(nanos: u64) -> f64 {
nanos as f64 / NANOSECONDS_IN_SECOND as f64
}
#[inline]
#[no_mangle]
pub const extern "C" fn nanos_to_millis(nanos: u64) -> u64 {
nanos / NANOSECONDS_IN_MILLISECOND
}
#[inline]
#[no_mangle]
pub const extern "C" fn nanos_to_micros(nanos: u64) -> u64 {
nanos / NANOSECONDS_IN_MICROSECOND
}
#[inline]
#[must_use]
pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
let dt = DateTime::<Utc>::from(UNIX_EPOCH + Duration::from_nanos(unix_nanos.as_u64()));
dt.to_rfc3339_opts(SecondsFormat::Nanos, true)
}
#[inline]
#[must_use]
pub fn unix_nanos_to_iso8601_millis(unix_nanos: UnixNanos) -> String {
let dt = DateTime::<Utc>::from(UNIX_EPOCH + Duration::from_nanos(unix_nanos.as_u64()));
dt.to_rfc3339_opts(SecondsFormat::Millis, true)
}
#[must_use]
pub const fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 {
(unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND
}
pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result<UnixNanos> {
let date =
NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
let current_weekday = date.weekday().number_from_monday();
let offset = i64::from(match current_weekday {
1..=5 => 0, 6 => 1, _ => 2, });
let last_closest = date - TimeDelta::days(offset);
let unix_timestamp_ns = last_closest
.and_hms_nano_opt(0, 0, 0, 0)
.ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?;
Ok(UnixNanos::from(
unix_timestamp_ns
.and_utc()
.timestamp_nanos_opt()
.ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))? as u64,
))
}
pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result<bool> {
let timestamp_ns = timestamp_ns.as_u64();
let seconds = timestamp_ns / NANOSECONDS_IN_SECOND;
let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32;
let timestamp = DateTime::from_timestamp(seconds as i64, nanoseconds)
.ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?;
let now = Utc::now();
Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
}
#[must_use]
pub fn subtract_n_months(dt: DateTime<Utc>, n: isize) -> Option<DateTime<Utc>> {
let year = dt.year();
let month = dt.month() as isize; let day = dt.day(); let mut new_month = month - n;
let mut new_year = year;
while new_month <= 0 {
new_month += 12;
new_year -= 1;
}
let last_day_of_new_month = last_day_of_month(new_year, new_month as u32);
let new_day = day.min(last_day_of_new_month);
let new_date = chrono::NaiveDate::from_ymd_opt(new_year, new_month as u32, new_day)?;
let new_naive_datetime =
new_date.and_hms_micro_opt(dt.hour(), dt.minute(), dt.second(), dt.nanosecond() / 1000)?;
let new_dt = DateTime::<Utc>::from_naive_utc_and_offset(new_naive_datetime, chrono::Utc);
Some(new_dt)
}
#[must_use]
pub fn add_n_months(dt: DateTime<Utc>, n: isize) -> Option<DateTime<Utc>> {
let year = dt.year();
let month = dt.month() as isize;
let day = dt.day();
let mut new_month = month + n;
let mut new_year = year;
while new_month > 12 {
new_month -= 12;
new_year += 1;
}
let last_day_of_new_month = last_day_of_month(new_year, new_month as u32);
let new_day = day.min(last_day_of_new_month);
let new_date = chrono::NaiveDate::from_ymd_opt(new_year, new_month as u32, new_day)?;
let new_naive_datetime =
new_date.and_hms_micro_opt(dt.hour(), dt.minute(), dt.second(), dt.nanosecond() / 1000)?;
Some(DateTime::<Utc>::from_naive_utc_and_offset(
new_naive_datetime,
chrono::Utc,
))
}
#[must_use]
pub const fn last_day_of_month(year: i32, month: u32) -> u32 {
match month {
1 => 31,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
3 => 31,
4 => 30,
5 => 31,
6 => 30,
7 => 31,
8 => 31,
9 => 30,
10 => 31,
11 => 30,
12 => 31,
_ => 31, }
}
#[must_use]
pub const fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, TimeDelta, TimeZone, Utc};
use rstest::rstest;
use super::*;
#[rstest]
#[case(0.0, 0)]
#[case(1.0, 1_000_000_000)]
#[case(1.1, 1_100_000_000)]
#[case(42.0, 42_000_000_000)]
#[case(0.000_123_5, 123_500)]
#[case(0.000_000_01, 10)]
#[case(0.000_000_001, 1)]
#[case(9.999_999_999, 9_999_999_999)]
fn test_secs_to_nanos(#[case] value: f64, #[case] expected: u64) {
let result = secs_to_nanos(value);
assert_eq!(result, expected);
}
#[rstest]
#[case(0.0, 0)]
#[case(1.0, 1_000)]
#[case(1.1, 1_100)]
#[case(42.0, 42_000)]
#[case(0.012_34, 12)]
#[case(0.001, 1)]
fn test_secs_to_millis(#[case] value: f64, #[case] expected: u64) {
let result = secs_to_millis(value);
assert_eq!(result, expected);
}
#[rstest]
#[case(0.0, 0)]
#[case(1.0, 1_000_000)]
#[case(1.1, 1_100_000)]
#[case(42.0, 42_000_000)]
#[case(0.000_123_4, 123)]
#[case(0.000_01, 10)]
#[case(0.000_001, 1)]
#[case(9.999_999, 9_999_999)]
fn test_millis_to_nanos(#[case] value: f64, #[case] expected: u64) {
let result = millis_to_nanos(value);
assert_eq!(result, expected);
}
#[rstest]
#[case(0.0, 0)]
#[case(1.0, 1_000)]
#[case(1.1, 1_100)]
#[case(42.0, 42_000)]
#[case(0.1234, 123)]
#[case(0.01, 10)]
#[case(0.001, 1)]
#[case(9.999, 9_999)]
fn test_micros_to_nanos(#[case] value: f64, #[case] expected: u64) {
let result = micros_to_nanos(value);
assert_eq!(result, expected);
}
#[rstest]
#[case(0, 0.0)]
#[case(1, 1e-09)]
#[case(1_000_000_000, 1.0)]
#[case(42_897_123_111, 42.897_123_111)]
fn test_nanos_to_secs(#[case] value: u64, #[case] expected: f64) {
let result = nanos_to_secs(value);
assert_eq!(result, expected);
}
#[rstest]
#[case(0, 0)]
#[case(1_000_000, 1)]
#[case(1_000_000_000, 1000)]
#[case(42_897_123_111, 42897)]
fn test_nanos_to_millis(#[case] value: u64, #[case] expected: u64) {
let result = nanos_to_millis(value);
assert_eq!(result, expected);
}
#[rstest]
#[case(0, 0)]
#[case(1_000, 1)]
#[case(1_000_000_000, 1_000_000)]
#[case(42_897_123, 42_897)]
fn test_nanos_to_micros(#[case] value: u64, #[case] expected: u64) {
let result = nanos_to_micros(value);
assert_eq!(result, expected);
}
#[rstest]
#[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) {
let result = unix_nanos_to_iso8601(UnixNanos::from(nanos));
assert_eq!(result, expected);
}
#[rstest]
#[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) {
let result = unix_nanos_to_iso8601_millis(UnixNanos::from(nanos));
assert_eq!(result, expected);
}
#[rstest]
#[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(
#[case] year: i32,
#[case] month: u32,
#[case] day: u32,
#[case] expected: u64,
) {
let result = last_weekday_nanos(year, month, day).unwrap().as_u64();
assert_eq!(result, expected);
}
#[rstest]
fn test_last_closest_weekday_nanos_with_invalid_date() {
let result = last_weekday_nanos(2023, 4, 31);
assert!(result.is_err());
}
#[rstest]
fn test_last_closest_weekday_nanos_with_nonexistent_date() {
let result = last_weekday_nanos(2023, 2, 30);
assert!(result.is_err());
}
#[rstest]
fn test_last_closest_weekday_nanos_with_invalid_conversion() {
let result = last_weekday_nanos(9999, 12, 31);
assert!(result.is_err());
}
#[rstest]
fn test_is_within_last_24_hours_when_now() {
let now_ns = Utc::now().timestamp_nanos_opt().unwrap();
assert!(is_within_last_24_hours(UnixNanos::from(now_ns as u64)).unwrap());
}
#[rstest]
fn test_is_within_last_24_hours_when_two_days_ago() {
let past_ns = (Utc::now() - TimeDelta::try_days(2).unwrap())
.timestamp_nanos_opt()
.unwrap();
assert!(!is_within_last_24_hours(UnixNanos::from(past_ns as u64)).unwrap());
}
#[rstest]
#[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(
#[case] input: DateTime<Utc>,
#[case] months: isize,
#[case] expected: DateTime<Utc>,
) {
let result = subtract_n_months(input, months);
assert_eq!(result, Some(expected));
}
#[rstest]
#[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(
#[case] input: DateTime<Utc>,
#[case] months: isize,
#[case] expected: DateTime<Utc>,
) {
let result = add_n_months(input, months);
assert_eq!(result, Some(expected));
}
#[rstest]
#[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) {
let result = last_day_of_month(year, month);
assert_eq!(result, expected);
}
#[rstest]
#[case(2024, true)] #[case(1900, false)] #[case(2000, true)] #[case(2023, false)] fn test_is_leap_year(#[case] year: i32, #[case] expected: bool) {
let result = is_leap_year(year);
assert_eq!(result, expected);
}
}