feat!: parsing local timestamp (#1352)

* fix: parse and display timestamp/datetime in local time zone

* fix display

* fix: unit tests

* change time zone env

* fix: remove useless code
This commit is contained in:
Lei, HUANG
2023-04-11 12:54:15 +08:00
committed by GitHub
parent 1a21a6ea41
commit f5cf5685cc
8 changed files with 100 additions and 33 deletions

View File

@@ -15,13 +15,13 @@
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use chrono::NaiveDateTime;
use chrono::{Local, LocalResult, NaiveDateTime, TimeZone};
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use crate::error::{Error, ParseDateStrSnafu, Result};
use crate::error::{Error, InvalidDateStrSnafu, Result};
const DATETIME_FORMAT: &str = "%F %T";
const DATETIME_FORMAT_WITH_TZ: &str = "%F %T%z";
/// [DateTime] represents the **seconds elapsed since "1970-01-01 00:00:00 UTC" (UNIX Epoch)**.
#[derive(
@@ -32,7 +32,13 @@ pub struct DateTime(i64);
impl Display for DateTime {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(abs_time) = NaiveDateTime::from_timestamp_opt(self.0, 0) {
write!(f, "{}", abs_time.format(DATETIME_FORMAT))
write!(
f,
"{}",
Local {}
.from_utc_datetime(&abs_time)
.format(DATETIME_FORMAT_WITH_TZ)
)
} else {
write!(f, "DateTime({})", self.0)
}
@@ -49,9 +55,21 @@ impl FromStr for DateTime {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let datetime = NaiveDateTime::parse_from_str(s, DATETIME_FORMAT)
.context(ParseDateStrSnafu { raw: s })?;
Ok(Self(datetime.timestamp()))
let local = Local {};
let timestamp = if let Ok(d) = NaiveDateTime::parse_from_str(s, DATETIME_FORMAT) {
match local.from_local_datetime(&d) {
LocalResult::None => {
return InvalidDateStrSnafu { raw: s }.fail();
}
LocalResult::Single(d) | LocalResult::Ambiguous(d, _) => d.naive_utc().timestamp(),
}
} else if let Ok(v) = chrono::DateTime::parse_from_str(s, DATETIME_FORMAT_WITH_TZ) {
v.timestamp()
} else {
return InvalidDateStrSnafu { raw: s }.fail();
};
Ok(Self(timestamp))
}
}
@@ -81,14 +99,16 @@ mod tests {
#[test]
pub fn test_new_date_time() {
assert_eq!("1970-01-01 00:00:00", DateTime::new(0).to_string());
assert_eq!("1970-01-01 00:00:01", DateTime::new(1).to_string());
assert_eq!("1969-12-31 23:59:59", DateTime::new(-1).to_string());
std::env::set_var("TZ", "Asia/Shanghai");
assert_eq!("1970-01-01 08:00:00+0800", DateTime::new(0).to_string());
assert_eq!("1970-01-01 08:00:01+0800", DateTime::new(1).to_string());
assert_eq!("1970-01-01 07:59:59+0800", DateTime::new(-1).to_string());
}
#[test]
pub fn test_parse_from_string() {
let time = "1970-01-01 00:00:00";
std::env::set_var("TZ", "Asia/Shanghai");
let time = "1970-01-01 00:00:00+0800";
let dt = DateTime::from_str(time).unwrap();
assert_eq!(time, &dt.to_string());
}
@@ -98,4 +118,22 @@ mod tests {
let d: DateTime = 42.into();
assert_eq!(42, d.val());
}
#[test]
fn test_parse_local_date_time() {
std::env::set_var("TZ", "Asia/Shanghai");
assert_eq!(
-28800,
DateTime::from_str("1970-01-01 00:00:00").unwrap().val()
);
assert_eq!(0, DateTime::from_str("1970-01-01 08:00:00").unwrap().val());
}
#[test]
fn test_parse_local_date_time_with_tz() {
let ts = DateTime::from_str("1970-01-01 08:00:00+0000")
.unwrap()
.val();
assert_eq!(28800, ts);
}
}

View File

@@ -26,6 +26,9 @@ pub enum Error {
#[snafu(display("Failed to parse string to date, raw: {}, source: {}", raw, source))]
ParseDateStr { raw: String, source: ParseError },
#[snafu(display("Invalid date string, raw: {}", raw))]
InvalidDateStr { raw: String, location: Location },
#[snafu(display("Failed to parse a string into Timestamp, raw string: {}", raw))]
ParseTimestamp { raw: String, location: Location },
@@ -46,7 +49,9 @@ impl ErrorExt for Error {
StatusCode::InvalidArguments
}
Error::TimestampOverflow { .. } => StatusCode::Internal,
Error::ArithmeticOverflow { .. } => StatusCode::InvalidArguments,
Error::InvalidDateStr { .. } | Error::ArithmeticOverflow { .. } => {
StatusCode::InvalidArguments
}
}
}
@@ -60,6 +65,7 @@ impl ErrorExt for Error {
| Error::TimestampOverflow { location, .. }
| Error::ArithmeticOverflow { location, .. } => Some(*location),
Error::ParseDateStr { .. } => None,
Error::InvalidDateStr { location, .. } => Some(*location),
}
}
}

View File

@@ -164,16 +164,20 @@ impl Timestamp {
/// Format timestamp to ISO8601 string. If the timestamp exceeds what chrono timestamp can
/// represent, this function simply print the timestamp unit and value in plain string.
pub fn to_iso8601_string(&self) -> String {
if let LocalResult::Single(datetime) = self.to_chrono_datetime() {
format!("{}", datetime.format("%Y-%m-%d %H:%M:%S%.f%z"))
if let Some(v) = self.to_chrono_datetime() {
let local = Local {};
format!(
"{}",
local.from_utc_datetime(&v).format("%Y-%m-%d %H:%M:%S%.f%z")
)
} else {
format!("[Timestamp{}: {}]", self.unit, self.value)
}
}
pub fn to_chrono_datetime(&self) -> LocalResult<DateTime<Utc>> {
pub fn to_chrono_datetime(&self) -> Option<NaiveDateTime> {
let (sec, nsec) = self.split();
Utc.timestamp_opt(sec, nsec)
NaiveDateTime::from_timestamp_opt(sec, nsec)
}
}
@@ -636,31 +640,33 @@ mod tests {
#[test]
fn test_to_iso8601_string() {
std::env::set_var("TZ", "Asia/Shanghai");
let datetime_str = "2020-09-08 13:42:29.042+0000";
let ts = Timestamp::from_str(datetime_str).unwrap();
assert_eq!(datetime_str, ts.to_iso8601_string());
assert_eq!("2020-09-08 21:42:29.042+0800", ts.to_iso8601_string());
let ts_millis = 1668070237000;
let ts = Timestamp::new_millisecond(ts_millis);
assert_eq!("2022-11-10 08:50:37+0000", ts.to_iso8601_string());
assert_eq!("2022-11-10 16:50:37+0800", ts.to_iso8601_string());
let ts_millis = -1000;
let ts = Timestamp::new_millisecond(ts_millis);
assert_eq!("1969-12-31 23:59:59+0000", ts.to_iso8601_string());
assert_eq!("1970-01-01 07:59:59+0800", ts.to_iso8601_string());
let ts_millis = -1;
let ts = Timestamp::new_millisecond(ts_millis);
assert_eq!("1969-12-31 23:59:59.999+0000", ts.to_iso8601_string());
assert_eq!("1970-01-01 07:59:59.999+0800", ts.to_iso8601_string());
let ts_millis = -1001;
let ts = Timestamp::new_millisecond(ts_millis);
assert_eq!("1969-12-31 23:59:58.999+0000", ts.to_iso8601_string());
assert_eq!("1970-01-01 07:59:58.999+0800", ts.to_iso8601_string());
}
#[test]
fn test_serialize_to_json_value() {
std::env::set_var("TZ", "Asia/Shanghai");
assert_eq!(
"1970-01-01 00:00:01+0000",
"1970-01-01 08:00:01+0800",
match serde_json::Value::from(Timestamp::new(1, TimeUnit::Second)) {
Value::String(s) => s,
_ => unreachable!(),
@@ -668,7 +674,7 @@ mod tests {
);
assert_eq!(
"1970-01-01 00:00:00.001+0000",
"1970-01-01 08:00:00.001+0800",
match serde_json::Value::from(Timestamp::new(1, TimeUnit::Millisecond)) {
Value::String(s) => s,
_ => unreachable!(),
@@ -676,7 +682,7 @@ mod tests {
);
assert_eq!(
"1970-01-01 00:00:00.000001+0000",
"1970-01-01 08:00:00.000001+0800",
match serde_json::Value::from(Timestamp::new(1, TimeUnit::Microsecond)) {
Value::String(s) => s,
_ => unreachable!(),
@@ -684,7 +690,7 @@ mod tests {
);
assert_eq!(
"1970-01-01 00:00:00.000000001+0000",
"1970-01-01 08:00:00.000000001+0800",
match serde_json::Value::from(Timestamp::new(1, TimeUnit::Nanosecond)) {
Value::String(s) => s,
_ => unreachable!(),
@@ -869,4 +875,18 @@ mod tests {
assert_eq!(1, res.value);
assert_eq!(TimeUnit::Second, res.unit);
}
#[test]
fn test_parse_in_time_zone() {
std::env::set_var("TZ", "Asia/Shanghai");
assert_eq!(
Timestamp::new(0, TimeUnit::Nanosecond),
Timestamp::from_str("1970-01-01 08:00:00.000").unwrap()
);
assert_eq!(
Timestamp::new(0, TimeUnit::Second),
Timestamp::from_str("1970-01-01 08:00:00").unwrap()
);
}
}