use anyhow::{Result, anyhow, bail};
use chrono::{DateTime, Local, NaiveDateTime, TimeZone as _};
use dateparser::DateTimeUtc;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum XTimeStrFormat {
YMDHMZ1, YMDHMZ2, YMDHMZ3, Custom(String),
}
impl XTimeStrFormat {
pub fn format_str(&self) -> &str {
match self {
XTimeStrFormat::YMDHMZ2 => "%Y-%m-%d %H:%M:%S",
XTimeStrFormat::YMDHMZ3 => "%Y-%m-%dT%H:%M:%S",
XTimeStrFormat::YMDHMZ1 => "%Y%m%d%H%M%S",
XTimeStrFormat::Custom(s) => s.as_str(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct XTime {
inner: DateTime<Local>,
}
impl XTime {
pub fn from_now() -> Self {
let inner = Local::now();
Self { inner }
}
pub fn from_timestamp_auto(timestamp: impl TryInto<i64>) -> Result<Self> {
let timestamp = timestamp.try_into().map_err(|_e| anyhow!("Covert to i64 fail"))?;
if timestamp > 9999999999 {
Self::from_timestamp_ms(timestamp)
} else {
Self::from_timestamp_s(timestamp)
}
}
pub fn from_timestamp_s(timestamp_secs: impl TryInto<i64>) -> Result<Self> {
let timestamp_secs = timestamp_secs.try_into().map_err(|_e| anyhow!("Covert to i64 fail"))?;
let inner = match Local.timestamp_opt(timestamp_secs, 0) {
chrono::offset::LocalResult::Single(dt) => dt,
_ => bail!("timestamp_opt fail"),
};
Ok(Self { inner })
}
pub fn from_timestamp_s_nocheck(timestamp_secs: impl TryInto<i64>) -> Self {
Self::from_timestamp_s(timestamp_secs).unwrap()
}
pub fn from_timestamp_ms(timestamp_millis: impl TryInto<i64>) -> Result<Self> {
let timestamp_millis = timestamp_millis
.try_into()
.map_err(|_e| anyhow!("Covert to i64 fail"))?;
let inner = match Local.timestamp_millis_opt(timestamp_millis) {
chrono::offset::LocalResult::Single(dt) => dt,
_ => bail!("timestamp_millis_opt fail"),
};
Ok(Self { inner })
}
pub fn from_timestr(time_str: &str, time_format: XTimeStrFormat) -> Result<Self> {
let format_str = time_format.format_str();
let naive = NaiveDateTime::parse_from_str(time_str, format_str)
.map_err(|e| anyhow::anyhow!("Parse time_str error: {}, please check format match: {}", e, format_str))?;
let inner = match Local.from_local_datetime(&naive) {
chrono::offset::LocalResult::Single(dt) => dt,
chrono::offset::LocalResult::Ambiguous(_, _) => anyhow::bail!("Ambiguous"),
chrono::offset::LocalResult::None => anyhow::bail!("Invalid LocalTime setting in system"),
};
Ok(Self { inner })
}
pub fn from_timestr_guess(time_str: &str) -> Result<Self> {
let inner = time_str
.parse::<DateTimeUtc>()
.map_err(|e| {
anyhow::anyhow!(
"Check time_str format match, see: {}, error: {}",
"https://docs.rs/dateparser/latest/dateparser",
e
)
})?
.0
.with_timezone(&Local);
Ok(Self { inner })
}
pub fn from_timestamp_ms_nocheck(timestamp_millis: impl TryInto<i64>) -> Self {
Self::from_timestamp_ms(timestamp_millis).unwrap()
}
pub fn format_timestr(&self, time_format: XTimeStrFormat) -> String {
self.inner.format(time_format.format_str()).to_string()
}
pub fn as_timestamp_ms(&self) -> u64 {
self.inner.timestamp_millis() as _
}
pub fn as_timestamp_s(&self) -> u64 {
self.inner.timestamp() as _
}
pub fn drag_hours(&self, hours: i64) -> Self {
let inner = self.inner + chrono::Duration::hours(hours);
Self { inner }
}
pub fn drag_minutes(&self, minutes: i64) -> Self {
let inner = self.inner + chrono::Duration::minutes(minutes);
Self { inner }
}
pub fn drag_seconds(&self, seconds: i64) -> Self {
let inner = self.inner + chrono::Duration::seconds(seconds);
Self { inner }
}
pub fn drag_millis(&self, millis: i64) -> Self {
let inner = self.inner + chrono::Duration::milliseconds(millis);
Self { inner }
}
}
impl std::str::FromStr for XTime {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
XTime::from_timestr_guess(s)
}
}
impl TryFrom<&str> for XTime {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
XTime::from_timestr_guess(value)
}
}
impl TryFrom<String> for XTime {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
XTime::from_timestr_guess(&value)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr as _;
use super::*;
#[test]
fn test_timestamp_ms_to_local_time() {
let timestamp_ms = 1699598631608i64;
let xtime = XTime::from_timestamp_ms_nocheck(timestamp_ms);
assert_eq!(
xtime.format_timestr(XTimeStrFormat::YMDHMZ1).to_string(),
"20231110144351"
);
assert_eq!(
xtime.format_timestr(XTimeStrFormat::YMDHMZ2).to_string(),
"2023-11-10 14:43:51"
);
assert_eq!(
xtime.format_timestr(XTimeStrFormat::YMDHMZ3).to_string(),
"2023-11-10T14:43:51"
);
assert_eq!(xtime.as_timestamp_ms(), timestamp_ms as u64);
assert_eq!(XTime::from_timestamp_auto(timestamp_ms).unwrap(), xtime);
assert_eq!(
xtime.drag_hours(-1).format_timestr(XTimeStrFormat::YMDHMZ2).to_string(),
"2023-11-10 13:43:51"
);
assert_eq!(
xtime.drag_hours(1).format_timestr(XTimeStrFormat::YMDHMZ2).to_string(),
"2023-11-10 15:43:51"
);
assert_eq!(
xtime
.drag_minutes(30)
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 15:13:51"
);
assert_eq!(
xtime
.drag_seconds(90)
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:45:21"
);
assert_eq!(
xtime.drag_hours(-1).format_timestr(XTimeStrFormat::YMDHMZ2).to_string(),
"2023-11-10 13:43:51"
);
assert_eq!(
xtime
.drag_minutes(-30)
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:13:51"
);
assert_eq!(
xtime
.drag_seconds(-90)
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:42:21"
);
}
#[test]
fn test_timestamp_s_to_local_time() {
let timestamp_s = 1699598631i64;
let xtime = XTime::from_timestamp_s(timestamp_s).unwrap();
assert_eq!(
xtime.format_timestr(XTimeStrFormat::YMDHMZ1).to_string(),
"20231110144351"
);
assert_eq!(
xtime.format_timestr(XTimeStrFormat::YMDHMZ2).to_string(),
"2023-11-10 14:43:51"
);
assert_eq!(
xtime.format_timestr(XTimeStrFormat::YMDHMZ3).to_string(),
"2023-11-10T14:43:51"
);
assert_eq!(xtime.as_timestamp_s(), timestamp_s as u64);
assert_eq!(
xtime.format_timestr(XTimeStrFormat::Custom("%Y-%m-%d/%H%M%S/996_%H%M%S%.3f.ts".to_string())),
"2023-11-10/144351/996_144351.000.ts"
);
assert_eq!(XTime::from_timestamp_auto(timestamp_s).unwrap(), xtime);
}
#[test]
fn test_case_xtime_from_now() {
let xtime = XTime::from_now();
println!("xtime now: {:?}", xtime.format_timestr(XTimeStrFormat::YMDHMZ2));
}
#[test]
fn test_case_from_str() {
let xtime = XTime::from_timestr("2023-11-10 14:43:51", XTimeStrFormat::YMDHMZ2).unwrap();
assert_eq!(
xtime.format_timestr(XTimeStrFormat::YMDHMZ2).to_string(),
"2023-11-10 14:43:51"
);
assert_eq!(
xtime,
XTime::from_timestr("20231110144351", XTimeStrFormat::YMDHMZ1).unwrap()
);
}
#[test]
fn test_case_from_str_guess_traits() {
assert_eq!(
"2023-11-10 14:43:51"
.parse::<XTime>()
.unwrap()
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:43:51"
);
assert_eq!(
XTime::from_str("2023-11-10 14:43:51")
.unwrap()
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:43:51"
);
assert_eq!(
XTime::try_from("2023-11-10 14:43:51")
.unwrap()
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:43:51"
);
assert_eq!(
XTime::try_from("2023-11-10 14:43:51".to_string())
.unwrap()
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:43:51"
);
}
#[test]
fn test_case_from_str_guess() {
assert_eq!(
XTime::from_timestr_guess("2023-11-10 14:43:51")
.unwrap()
.format_timestr(XTimeStrFormat::YMDHMZ2)
.to_string(),
"2023-11-10 14:43:51"
);
}
#[test]
fn test_case_xtime_compare() {
assert!(
XTime::from_timestr("2023-11-10 14:43:52", XTimeStrFormat::YMDHMZ2).unwrap()
== XTime::from_timestr("2023-11-10 14:43:52", XTimeStrFormat::YMDHMZ2).unwrap()
);
assert!(
XTime::from_timestr("2023-11-10 14:43:52", XTimeStrFormat::YMDHMZ2).unwrap()
> XTime::from_timestr("2023-11-10 14:43:51", XTimeStrFormat::YMDHMZ2).unwrap()
);
assert!(
XTime::from_timestr("2023-11-10 14:43:51", XTimeStrFormat::YMDHMZ2).unwrap()
< XTime::from_timestr("2023-11-10 14:43:52", XTimeStrFormat::YMDHMZ2).unwrap()
);
}
}