use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, Timelike};
use crate::tz::Tz;
pub fn parse_ns(s: &str) -> Option<i64> {
parse_ns_in_tz(s, Tz::Utc)
}
pub fn parse_ns_in_tz(s: &str, tz: Tz) -> Option<i64> {
let s = s.trim();
if let Some(ns) = parse_offset_aware(s) {
return Some(ns);
}
if let Some((y, mo, d, h, mi, se, subsec)) = naive_parts(s) {
return tz.wall_to_utc_ns(y, mo, d, h, mi, se)?.checked_add(subsec);
}
for fmt in ["%H:%M:%S", "%H:%M"] {
if let Ok(t) = chrono::NaiveTime::parse_from_str(s, fmt) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos() as i64;
let (y, mo, d, _, _, _) = tz.civil_parts(now);
return tz.wall_to_utc_ns(
y as i32,
mo as u32,
d as u32,
t.hour(),
t.minute(),
t.second(),
);
}
}
None
}
pub fn offset_suffix_secs(s: &str) -> Option<i32> {
let s = s.trim();
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(chrono::Offset::fix(dt.offset()).local_minus_utc());
}
for fmt in [
"%Y-%m-%d %H:%M:%S%.f%:z",
"%Y-%m-%d %H:%M:%S%.f%z",
"%Y-%m-%dT%H:%M:%S%.f%z",
] {
if let Ok(dt) = DateTime::parse_from_str(s, fmt) {
return Some(chrono::Offset::fix(dt.offset()).local_minus_utc());
}
}
None
}
fn parse_offset_aware(s: &str) -> Option<i64> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return dt.timestamp_nanos_opt();
}
for fmt in [
"%Y-%m-%d %H:%M:%S%.f%:z",
"%Y-%m-%d %H:%M:%S%.f%z",
"%Y-%m-%dT%H:%M:%S%.f%z",
] {
if let Ok(dt) = DateTime::parse_from_str(s, fmt) {
return dt.timestamp_nanos_opt();
}
}
None
}
fn naive_parts(s: &str) -> Option<(i32, u32, u32, u32, u32, u32, i64)> {
for fmt in [
"%Y-%m-%d %H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S%.f",
"%Y/%m/%d %H:%M:%S%.f",
] {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return Some((
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
dt.nanosecond() as i64,
));
}
}
for fmt in ["%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M", "%Y/%m/%d %H:%M"] {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return Some((
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
0,
0,
));
}
}
for fmt in ["%Y-%m-%d", "%Y/%m/%d"] {
if let Ok(d) = NaiveDate::parse_from_str(s, fmt) {
return Some((d.year(), d.month(), d.day(), 0, 0, 0, 0));
}
}
None
}
pub fn epoch_to_ns(value: i64, unit: &str) -> Option<i64> {
let scale: i64 = match unit {
"s" => 1_000_000_000,
"ms" => 1_000_000,
"us" => 1_000,
"ns" => 1,
_ => return None,
};
value.checked_mul(scale)
}
pub fn epoch_to_ns_f64(value: f64, unit: &str) -> Option<i64> {
let scale: f64 = match unit {
"s" => 1_000_000_000.0,
"ms" => 1_000_000.0,
"us" => 1_000.0,
"ns" => 1.0,
_ => return None,
};
let ns = (value * scale).round();
if ns.is_finite() && ns >= i64::MIN as f64 && ns <= i64::MAX as f64 {
Some(ns as i64)
} else {
None
}
}
pub fn civil_parts(ns: i64) -> (i64, i64, i64, i64, i64, i64) {
let secs = ns.div_euclid(1_000_000_000);
let nsub = ns.rem_euclid(1_000_000_000) as u32;
let dt = DateTime::from_timestamp(secs, nsub)
.expect("an i64-ns timestamp is within chrono's representable range")
.naive_utc();
(
dt.year() as i64,
dt.month() as i64,
dt.day() as i64,
dt.hour() as i64,
dt.minute() as i64,
dt.second() as i64,
)
}
pub fn days_from_civil(y: i64, mo: i64, d: i64) -> i64 {
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("epoch is valid");
let date = NaiveDate::from_ymd_opt(y as i32, mo as u32, d as u32).unwrap_or(epoch);
date.signed_duration_since(epoch).num_days()
}
pub fn civil_from_days(days: i64) -> (i64, i64, i64) {
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("epoch is valid");
let date = epoch + chrono::Duration::days(days);
(date.year() as i64, date.month() as i64, date.day() as i64)
}
fn subsec_suffix(nsub: u32) -> String {
if nsub == 0 {
String::new()
} else if nsub.is_multiple_of(1_000_000) {
format!(".{:03}", nsub / 1_000_000)
} else if nsub.is_multiple_of(1_000) {
format!(".{:06}", nsub / 1_000)
} else {
format!(".{nsub:09}")
}
}
pub fn format_ns(ns: i64) -> String {
if ns == i64::MIN {
return "NaT".to_string(); }
let secs = ns.div_euclid(1_000_000_000);
let nsub = ns.rem_euclid(1_000_000_000) as u32;
DateTime::from_timestamp(secs, 0)
.map(|dt| {
format!(
"{}{}",
dt.naive_utc().format("%Y-%m-%d %H:%M:%S"),
subsec_suffix(nsub)
)
})
.unwrap_or_default()
}
pub fn format_ns_tz(ns: i64, tz: Tz) -> String {
if ns == i64::MIN {
return "NaT".to_string();
}
if tz.is_utc() {
return format_ns(ns);
}
let (y, mo, d, h, mi, s) = tz.civil_parts(ns);
let nsub = ns.rem_euclid(1_000_000_000) as u32;
format!(
"{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}{}",
subsec_suffix(nsub)
)
}
pub fn civil_parts_tz(ns: i64, tz: Tz) -> (i64, i64, i64, i64, i64, i64) {
if tz.is_utc() {
civil_parts(ns)
} else {
tz.civil_parts(ns)
}
}
pub fn strftime(ns: i64, tz: Tz, fmt: &str) -> Option<String> {
use chrono::format::{Item, StrftimeItems};
if ns == i64::MIN {
return Some("NaT".to_string()); }
let (y, mo, d, h, mi, s) = civil_parts_tz(ns, tz);
let dt = NaiveDate::from_ymd_opt(y as i32, mo as u32, d as u32)?
.and_hms_opt(h as u32, mi as u32, s as u32)?;
let items: Vec<Item> = StrftimeItems::new(fmt).collect();
if items.iter().any(|it| matches!(it, Item::Error)) {
return None;
}
Some(dt.format_with_items(items.iter()).to_string())
}
pub fn parse_ns_format(s: &str, fmt: &str) -> Option<i64> {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return dt.and_utc().timestamp_nanos_opt();
}
NaiveDate::parse_from_str(s, fmt)
.ok()?
.and_hms_opt(0, 0, 0)?
.and_utc()
.timestamp_nanos_opt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let ns = parse_ns("2020-02-07 00:00:00").unwrap();
assert_eq!(format_ns(ns), "2020-02-07 00:00:00");
assert_eq!(parse_ns("2020-02-07").unwrap(), ns);
assert!(parse_ns("not a date").is_none());
}
#[test]
fn offset_aware_strings_are_absolute() {
assert_eq!(
format_ns(parse_ns("2020-01-01T08:00:00+08:00").unwrap()),
"2020-01-01 00:00:00"
);
assert_eq!(
format_ns(parse_ns("2020-01-01T00:00:00Z").unwrap()),
"2020-01-01 00:00:00"
);
assert_eq!(
format_ns(parse_ns("2020-01-01 09:30:00 -05:00").unwrap()),
"2020-01-01 14:30:00"
);
}
#[test]
fn naive_string_interpreted_in_tz() {
let tz = Tz::parse("+08:00").unwrap();
assert_eq!(
format_ns(parse_ns_in_tz("2020-01-01 08:00:00", tz).unwrap()),
"2020-01-01 00:00:00"
);
assert_eq!(
format_ns(parse_ns_in_tz("2020-01-01T00:00:00Z", tz).unwrap()),
"2020-01-01 00:00:00"
);
}
#[test]
fn epoch_units() {
assert_eq!(
format_ns(epoch_to_ns(1_577_836_800_000, "ms").unwrap()),
"2020-01-01 00:00:00"
);
assert_eq!(
epoch_to_ns(1_577_836_800, "s").unwrap(),
1_577_836_800_000_000_000
);
assert!(epoch_to_ns(1, "weeks").is_none());
}
#[test]
fn epoch_units_f64_preserves_fraction() {
assert_eq!(
epoch_to_ns_f64(1_577_836_800.0, "s").unwrap(),
1_577_836_800_000_000_000
);
assert_eq!(
epoch_to_ns_f64(1_577_836_800.5, "s").unwrap(),
1_577_836_800_500_000_000
);
assert_eq!(epoch_to_ns_f64(1.0, "ms").unwrap(), 1_000_000);
assert_eq!(epoch_to_ns_f64(1.0, "us").unwrap(), 1_000);
assert_eq!(epoch_to_ns_f64(1.0, "ns").unwrap(), 1);
assert!(epoch_to_ns_f64(1.0, "weeks").is_none());
assert!(epoch_to_ns_f64(f64::NAN, "s").is_none());
assert!(epoch_to_ns_f64(f64::INFINITY, "s").is_none());
assert!(epoch_to_ns_f64(1e30, "s").is_none());
}
#[test]
fn civil_parts_and_format_ns_tz() {
assert_eq!(civil_parts(0), (1970, 1, 1, 0, 0, 0));
assert_eq!(format_ns_tz(0, crate::tz::Tz::Utc), format_ns(0));
let ny = crate::tz::Tz::parse("America/New_York").unwrap();
assert!(format_ns_tz(0, ny).starts_with("1969")); }
}
pub fn iso_calendar(y: i64, mo: i64, d: i64) -> (i64, i64, i64) {
let date = NaiveDate::from_ymd_opt(y as i32, mo as u32, d as u32).unwrap_or_default();
let iw = date.iso_week();
(
iw.year() as i64,
iw.week() as i64,
date.weekday().number_from_monday() as i64,
)
}
#[cfg(test)]
mod offset_suffix_tests {
use super::*;
#[test]
fn offset_suffix_forms() {
assert_eq!(offset_suffix_secs("2021-01-01T09:00:00+08:00"), Some(28800));
assert_eq!(offset_suffix_secs("2021-01-01 09:00:00+08:00"), Some(28800));
assert_eq!(offset_suffix_secs("2021-01-01T09:00:00+0800"), Some(28800));
assert_eq!(offset_suffix_secs("2021-01-01 09:00:00"), None);
}
}