use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta, Utc};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CfCalendar {
Standard,
ProlepticGregorian,
NoLeap,
AllLeap,
Day360,
Julian,
}
impl CfCalendar {
pub fn parse(s: &str) -> Self {
match s.trim().to_lowercase().as_str() {
"standard" | "gregorian" => CfCalendar::Standard,
"proleptic_gregorian" => CfCalendar::ProlepticGregorian,
"noleap" | "365_day" => CfCalendar::NoLeap,
"all_leap" | "366_day" => CfCalendar::AllLeap,
"360_day" => CfCalendar::Day360,
"julian" => CfCalendar::Julian,
_ => CfCalendar::Standard, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CfTimeUnit {
Seconds,
Minutes,
Hours,
Days,
Months,
}
#[derive(Debug, Clone)]
pub struct CfTimeRef {
pub unit: CfTimeUnit,
pub epoch: NaiveDateTime,
pub calendar: CfCalendar,
}
pub fn parse_time_units(units: &str, calendar: CfCalendar) -> Result<CfTimeRef> {
let lower = units.trim().to_lowercase();
let parts: Vec<&str> = lower.splitn(2, " since ").collect();
if parts.len() != 2 {
return Err(Error::InvalidData(format!(
"invalid CF time units '{}': expected '<unit> since <date>'",
units
)));
}
let unit = match parts[0].trim() {
"second" | "seconds" | "s" => CfTimeUnit::Seconds,
"minute" | "minutes" | "min" => CfTimeUnit::Minutes,
"hour" | "hours" | "hr" | "h" => CfTimeUnit::Hours,
"day" | "days" | "d" => CfTimeUnit::Days,
"month" | "months" => CfTimeUnit::Months,
u => {
return Err(Error::InvalidData(format!(
"unsupported CF time unit '{}'",
u
)));
}
};
let epoch = parse_epoch(parts[1].trim())?;
Ok(CfTimeRef {
unit,
epoch,
calendar,
})
}
fn parse_epoch(s: &str) -> Result<NaiveDateTime> {
for fmt in &[
"%Y-%m-%d %H:%M:%S%.f",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%d",
] {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return Ok(dt);
}
}
if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
return Ok(d.and_hms_opt(0, 0, 0).unwrap());
}
Err(Error::InvalidData(format!("cannot parse CF epoch '{}'", s)))
}
pub fn decode_time(value: f64, time_ref: &CfTimeRef) -> Result<DateTime<Utc>> {
let delta = match time_ref.unit {
CfTimeUnit::Seconds => TimeDelta::milliseconds((value * 1000.0) as i64),
CfTimeUnit::Minutes => TimeDelta::seconds((value * 60.0) as i64),
CfTimeUnit::Hours => TimeDelta::seconds((value * 3600.0) as i64),
CfTimeUnit::Days => TimeDelta::milliseconds((value * 86_400_000.0) as i64),
CfTimeUnit::Months => {
TimeDelta::milliseconds((value * 30.44 * 86_400_000.0) as i64)
}
};
let naive = time_ref
.epoch
.checked_add_signed(delta)
.ok_or_else(|| Error::InvalidData(format!("time value {} out of range", value)))?;
Ok(DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc))
}
pub fn decode_times(values: &[f64], time_ref: &CfTimeRef) -> Result<Vec<DateTime<Utc>>> {
values.iter().map(|&v| decode_time(v, time_ref)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_days_since() {
let tr = parse_time_units("days since 1970-01-01", CfCalendar::Standard).unwrap();
assert_eq!(tr.unit, CfTimeUnit::Days);
assert_eq!(
tr.epoch,
NaiveDate::from_ymd_opt(1970, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
);
}
#[test]
fn test_parse_hours_since_with_time() {
let tr = parse_time_units("hours since 2000-01-01 00:00:00", CfCalendar::Standard).unwrap();
assert_eq!(tr.unit, CfTimeUnit::Hours);
assert_eq!(
tr.epoch,
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
);
}
#[test]
fn test_decode_days() {
let tr = parse_time_units("days since 1970-01-01", CfCalendar::Standard).unwrap();
let dt = decode_time(365.0, &tr).unwrap();
assert_eq!(dt.format("%Y-%m-%d").to_string(), "1971-01-01");
}
#[test]
fn test_decode_hours() {
let tr = parse_time_units("hours since 2000-01-01 00:00:00", CfCalendar::Standard).unwrap();
let dt = decode_time(24.0, &tr).unwrap();
assert_eq!(dt.format("%Y-%m-%d").to_string(), "2000-01-02");
}
#[test]
fn test_calendar_from_str() {
assert_eq!(CfCalendar::parse("standard"), CfCalendar::Standard);
assert_eq!(CfCalendar::parse("noleap"), CfCalendar::NoLeap);
assert_eq!(CfCalendar::parse("365_day"), CfCalendar::NoLeap);
assert_eq!(CfCalendar::parse("360_day"), CfCalendar::Day360);
assert_eq!(
CfCalendar::parse("proleptic_gregorian"),
CfCalendar::ProlepticGregorian
);
}
#[test]
fn test_invalid_units() {
assert!(parse_time_units("invalid", CfCalendar::Standard).is_err());
assert!(parse_time_units("furlongs since yesterday", CfCalendar::Standard).is_err());
}
}