use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, TimeZone, Timelike, Utc};
const DATE: &str = "%Y-%m-%d";
const LOCAL_DATETIME: &str = "%Y-%m-%dT%H:%M:%S%.f";
const MS_PER_DAY: i64 = 86_400_000;
#[must_use]
pub fn parse_datetime(value: &str) -> Option<i64> {
let value = value.trim();
if let Some(ms) = parse_signed_year(value) {
return Some(ms);
}
if let Ok(datetime) = DateTime::parse_from_rfc3339(value) {
return Some(datetime.timestamp_millis());
}
if let Some(prefix) = value.strip_suffix('z') {
let normalized = format!("{prefix}Z");
if let Ok(datetime) = DateTime::parse_from_rfc3339(&normalized) {
return Some(datetime.timestamp_millis());
}
}
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATETIME) {
return Some(datetime.and_utc().timestamp_millis());
}
NaiveDate::parse_from_str(value, DATE)
.ok()?
.and_hms_opt(0, 0, 0)
.map(|datetime| datetime.and_utc().timestamp_millis())
}
fn parse_signed_year(value: &str) -> Option<i64> {
let (sign, rest) = value.split_at_checked(1)?;
let digits = rest.split('-').next()?;
if !matches!(sign, "-" | "+") || digits.len() < 4 || !digits.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
if let Ok(datetime) = DateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f%:z") {
return Some(datetime.timestamp_millis());
}
if let Some(local) = value.strip_suffix(['Z', 'z']) {
return NaiveDateTime::parse_from_str(local, LOCAL_DATETIME)
.ok()
.map(|datetime| datetime.and_utc().timestamp_millis());
}
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATETIME) {
return Some(datetime.and_utc().timestamp_millis());
}
NaiveDate::parse_from_str(value, DATE)
.ok()?
.and_hms_opt(0, 0, 0)
.map(|datetime| datetime.and_utc().timestamp_millis())
}
#[must_use]
pub fn components_to_epoch_ms_checked(
year: i64,
month: i64,
day: i64,
hour: i64,
minute: i64,
second: i64,
millis: i64,
) -> Option<i64> {
let year = i32::try_from(year).ok()?;
let month = u32::try_from(month).ok()?;
let day = u32::try_from(day).ok()?;
let hour = u32::try_from(hour).ok()?;
let minute = u32::try_from(minute).ok()?;
let second = u32::try_from(second).ok()?;
let millis = u32::try_from(millis).ok()?;
Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()?
.with_nanosecond(millis.checked_mul(1_000_000)?)
.map(|datetime| datetime.timestamp_millis())
}
#[must_use]
pub fn components_to_epoch_ms(
year: i64,
month: i64,
day: i64,
hour: i64,
minute: i64,
second: i64,
millis: i64,
) -> i64 {
components_to_epoch_ms_checked(year, month, day, hour, minute, second, millis)
.expect("valid datetime components")
}
#[must_use]
pub fn is_datetime_string(value: &str) -> bool {
parse_datetime(value).is_some()
}
#[must_use]
pub fn json_datetime_ms(value: &serde_json::Value) -> Option<i64> {
match value {
serde_json::Value::Number(number) => number.as_i64(),
serde_json::Value::String(value) => parse_datetime(value),
_ => None,
}
}
fn utc(ms: i64) -> DateTime<Utc> {
DateTime::from_timestamp_millis(ms).expect("epoch milliseconds in Chrono range")
}
#[must_use]
pub fn start_of_month(ms: i64) -> i64 {
let datetime = utc(ms);
Utc.with_ymd_and_hms(datetime.year(), datetime.month(), 1, 0, 0, 0)
.single()
.expect("valid month start")
.timestamp_millis()
}
#[must_use]
pub fn start_of_year(ms: i64) -> i64 {
let datetime = utc(ms);
Utc.with_ymd_and_hms(datetime.year(), 1, 1, 0, 0, 0)
.single()
.expect("valid year start")
.timestamp_millis()
}
#[must_use]
pub fn start_of_quarter(ms: i64) -> i64 {
let datetime = utc(ms);
let quarter_start_month = ((datetime.month() - 1) / 3) * 3 + 1;
Utc.with_ymd_and_hms(datetime.year(), quarter_start_month, 1, 0, 0, 0)
.single()
.expect("valid quarter start")
.timestamp_millis()
}
#[must_use]
pub fn ms_to_datetime_string(ms: i64) -> String {
let datetime = utc(ms);
if ms.rem_euclid(MS_PER_DAY) == 0 {
datetime.format(DATE).to_string()
} else {
datetime.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
}
}
#[must_use]
pub fn component_of(ms: i64, which: u8) -> i64 {
let datetime = utc(ms);
match which {
0 => i64::from(datetime.year()),
1 => i64::from(datetime.month()),
2 => i64::from(datetime.day()),
3 => i64::from(datetime.hour()),
4 => i64::from(datetime.minute()),
5 => i64::from(datetime.second()),
6 => i64::from(datetime.timestamp_subsec_millis()),
7 => i64::from(datetime.timestamp_subsec_micros()),
8 => i64::from(datetime.timestamp_subsec_nanos()),
_ => 0,
}
}