feat: adds date_format function (#3167)

* feat: adds date_format function

* fix: compile error

* chore: use system timezone for FunctionContext and EvalContext

* test: as_formatted_string

* test: sqlness test

* chore: rename function
This commit is contained in:
dennis zhuang
2024-01-17 11:24:40 +08:00
committed by GitHub
parent d020a3db23
commit 204b9433b8
18 changed files with 651 additions and 61 deletions

View File

@@ -12,16 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt::{Display, Formatter};
use std::fmt::{Display, Formatter, Write};
use std::str::FromStr;
use chrono::{Datelike, Days, Months, NaiveDate};
use chrono::{Datelike, Days, Months, NaiveDate, NaiveTime, TimeZone};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use snafu::ResultExt;
use crate::error::{Error, ParseDateStrSnafu, Result};
use crate::interval::Interval;
use crate::timezone::get_timezone;
use crate::Timezone;
const UNIX_EPOCH_FROM_CE: i32 = 719_163;
@@ -84,6 +86,40 @@ impl Date {
NaiveDate::from_num_days_from_ce_opt(UNIX_EPOCH_FROM_CE + self.0)
}
/// Format Date for given format and timezone.
/// If `tz==None`, the server default timezone will used.
pub fn as_formatted_string(
self,
pattern: &str,
timezone: Option<&Timezone>,
) -> Result<Option<String>> {
if let Some(v) = self.to_chrono_date() {
// Safety: always success
let time = NaiveTime::from_hms_nano_opt(0, 0, 0, 0).unwrap();
let v = v.and_time(time);
let mut formatted = String::new();
match get_timezone(timezone) {
Timezone::Offset(offset) => {
write!(
formatted,
"{}",
offset.from_utc_datetime(&v).format(pattern)
)
.context(crate::error::FormatSnafu { pattern })?;
}
Timezone::Named(tz) => {
write!(formatted, "{}", tz.from_utc_datetime(&v).format(pattern))
.context(crate::error::FormatSnafu { pattern })?;
}
}
return Ok(Some(formatted));
}
Ok(None)
}
pub fn to_secs(&self) -> i64 {
(self.0 as i64) * 24 * 3600
}
@@ -170,6 +206,37 @@ mod tests {
assert_eq!(date, Date::from_str(&date.to_string()).unwrap());
}
#[test]
fn test_as_formatted_string() {
let d: Date = 42.into();
assert_eq!(
"1970-02-12",
d.as_formatted_string("%Y-%m-%d", None).unwrap().unwrap()
);
assert_eq!(
"1970-02-12 00:00:00",
d.as_formatted_string("%Y-%m-%d %H:%M:%S", None)
.unwrap()
.unwrap()
);
assert_eq!(
"1970-02-12T00:00:00:000",
d.as_formatted_string("%Y-%m-%dT%H:%M:%S:%3f", None)
.unwrap()
.unwrap()
);
assert_eq!(
"1970-02-12T08:00:00:000",
d.as_formatted_string(
"%Y-%m-%dT%H:%M:%S:%3f",
Some(&Timezone::from_tz_string("Asia/Shanghai").unwrap())
)
.unwrap()
.unwrap()
);
}
#[test]
pub fn test_from() {
let d: Date = 42.into();

View File

@@ -12,15 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt::{Display, Formatter};
use std::fmt::{Display, Formatter, Write};
use std::str::FromStr;
use std::time::Duration;
use chrono::{Days, LocalResult, Months, NaiveDateTime, TimeZone as ChronoTimeZone, Utc};
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use crate::error::{Error, InvalidDateStrSnafu, Result};
use crate::timezone::Timezone;
use crate::timezone::{get_timezone, Timezone};
use crate::util::{format_utc_datetime, local_datetime_to_utc};
use crate::{Date, Interval};
@@ -110,7 +111,38 @@ impl DateTime {
NaiveDateTime::from_timestamp_millis(self.0)
}
pub fn to_chrono_datetime_with_timezone(&self, tz: Option<Timezone>) -> Option<NaiveDateTime> {
/// Format DateTime for given format and timezone.
/// If `tz==None`, the server default timezone will used.
pub fn as_formatted_string(
self,
pattern: &str,
timezone: Option<&Timezone>,
) -> Result<Option<String>> {
if let Some(v) = self.to_chrono_datetime() {
let mut formatted = String::new();
match get_timezone(timezone) {
Timezone::Offset(offset) => {
write!(
formatted,
"{}",
offset.from_utc_datetime(&v).format(pattern)
)
.context(crate::error::FormatSnafu { pattern })?;
}
Timezone::Named(tz) => {
write!(formatted, "{}", tz.from_utc_datetime(&v).format(pattern))
.context(crate::error::FormatSnafu { pattern })?;
}
}
return Ok(Some(formatted));
}
Ok(None)
}
pub fn to_chrono_datetime_with_timezone(&self, tz: Option<&Timezone>) -> Option<NaiveDateTime> {
let datetime = self.to_chrono_datetime();
datetime.map(|v| match tz {
Some(Timezone::Offset(offset)) => offset.from_utc_datetime(&v).naive_local(),
@@ -211,6 +243,38 @@ mod tests {
assert_eq!(28800000, ts);
}
#[test]
fn test_as_formatted_string() {
let d: DateTime = DateTime::new(1000);
assert_eq!(
"1970-01-01",
d.as_formatted_string("%Y-%m-%d", None).unwrap().unwrap()
);
assert_eq!(
"1970-01-01 00:00:01",
d.as_formatted_string("%Y-%m-%d %H:%M:%S", None)
.unwrap()
.unwrap()
);
assert_eq!(
"1970-01-01T00:00:01:000",
d.as_formatted_string("%Y-%m-%dT%H:%M:%S:%3f", None)
.unwrap()
.unwrap()
);
assert_eq!(
"1970-01-01T08:00:01:000",
d.as_formatted_string(
"%Y-%m-%dT%H:%M:%S:%3f",
Some(&Timezone::from_tz_string("Asia/Shanghai").unwrap())
)
.unwrap()
.unwrap()
);
}
#[test]
fn test_from_max_date() {
let date = Date::new(i32::MAX);

View File

@@ -68,6 +68,14 @@ pub enum Error {
#[snafu(display("Invalid timezone string {raw}"))]
ParseTimezoneName { raw: String, location: Location },
#[snafu(display("Failed to format, pattern: {}", pattern))]
Format {
pattern: String,
#[snafu(source)]
error: std::fmt::Error,
location: Location,
},
}
impl ErrorExt for Error {
@@ -76,6 +84,7 @@ impl ErrorExt for Error {
Error::ParseDateStr { .. }
| Error::ParseTimestamp { .. }
| Error::InvalidTimezoneOffset { .. }
| Error::Format { .. }
| Error::ParseOffsetStr { .. }
| Error::ParseTimezoneName { .. } => StatusCode::InvalidArguments,
Error::TimestampOverflow { .. } => StatusCode::Internal,
@@ -93,6 +102,7 @@ impl ErrorExt for Error {
fn location_opt(&self) -> Option<common_error::snafu::Location> {
match self {
Error::ParseTimestamp { location, .. }
| Error::Format { location, .. }
| Error::TimestampOverflow { location, .. }
| Error::ArithmeticOverflow { location, .. } => Some(*location),
Error::ParseDateStr { .. }

View File

@@ -115,11 +115,11 @@ impl Time {
/// Format Time for given timezone.
/// When timezone is None, using system timezone by default.
pub fn to_timezone_aware_string(&self, tz: Option<Timezone>) -> String {
pub fn to_timezone_aware_string(&self, tz: Option<&Timezone>) -> String {
self.as_formatted_string("%H:%M:%S%.f", tz)
}
fn as_formatted_string(self, pattern: &str, timezone: Option<Timezone>) -> String {
fn as_formatted_string(self, pattern: &str, timezone: Option<&Timezone>) -> String {
if let Some(time) = self.to_chrono_time() {
let date = Utc::now().date_naive();
let datetime = NaiveDateTime::new(date, time);
@@ -380,37 +380,39 @@ mod tests {
assert_eq!(
"08:00:00.001",
Time::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("SYSTEM").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("SYSTEM").unwrap()))
);
assert_eq!(
"08:00:00.001",
Time::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("+08:00").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("+08:00").unwrap()))
);
assert_eq!(
"07:00:00.001",
Time::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("+07:00").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("+07:00").unwrap()))
);
assert_eq!(
"23:00:00.001",
Time::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("-01:00").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("-01:00").unwrap()))
);
assert_eq!(
"08:00:00.001",
Time::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("Asia/Shanghai").unwrap()))
Time::new(1, TimeUnit::Millisecond).to_timezone_aware_string(Some(
&Timezone::from_tz_string("Asia/Shanghai").unwrap()
))
);
assert_eq!(
"00:00:00.001",
Time::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("UTC").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("UTC").unwrap()))
);
assert_eq!(
"03:00:00.001",
Time::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("Europe/Moscow").unwrap()))
Time::new(1, TimeUnit::Millisecond).to_timezone_aware_string(Some(
&Timezone::from_tz_string("Europe/Moscow").unwrap()
))
);
}
}

View File

@@ -14,7 +14,7 @@
use core::default::Default;
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::fmt::{Display, Formatter, Write};
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::time::Duration;
@@ -26,7 +26,9 @@ use chrono::{
use serde::{Deserialize, Serialize};
use snafu::{OptionExt, ResultExt};
use crate::error::{ArithmeticOverflowSnafu, Error, ParseTimestampSnafu, TimestampOverflowSnafu};
use crate::error::{
ArithmeticOverflowSnafu, Error, ParseTimestampSnafu, Result, TimestampOverflowSnafu,
};
use crate::timezone::{get_timezone, Timezone};
use crate::util::div_ceil;
use crate::{error, Interval};
@@ -290,32 +292,50 @@ 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 {
// Safety: the format is valid
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f%z", None)
.unwrap()
}
/// Format timestamp use **system timezone**.
pub fn to_local_string(&self) -> String {
// Safety: the format is valid
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f", None)
.unwrap()
}
/// Format timestamp for given timezone.
/// If `tz==None`, the server default timezone will used.
pub fn to_timezone_aware_string(&self, tz: Option<Timezone>) -> String {
pub fn to_timezone_aware_string(&self, tz: Option<&Timezone>) -> String {
// Safety: the format is valid
self.as_formatted_string("%Y-%m-%d %H:%M:%S%.f", tz)
.unwrap()
}
fn as_formatted_string(self, pattern: &str, timezone: Option<Timezone>) -> String {
/// Format timestamp for given format and timezone.
/// If `tz==None`, the server default timezone will used.
pub fn as_formatted_string(self, pattern: &str, timezone: Option<&Timezone>) -> Result<String> {
if let Some(v) = self.to_chrono_datetime() {
let mut formatted = String::new();
match get_timezone(timezone) {
Timezone::Offset(offset) => {
format!("{}", offset.from_utc_datetime(&v).format(pattern))
write!(
formatted,
"{}",
offset.from_utc_datetime(&v).format(pattern)
)
.context(crate::error::FormatSnafu { pattern })?;
}
Timezone::Named(tz) => {
format!("{}", tz.from_utc_datetime(&v).format(pattern))
write!(formatted, "{}", tz.from_utc_datetime(&v).format(pattern))
.context(crate::error::FormatSnafu { pattern })?;
}
}
Ok(formatted)
} else {
format!("[Timestamp{}: {}]", self.unit, self.value)
Ok(format!("[Timestamp{}: {}]", self.unit, self.value))
}
}
@@ -324,7 +344,7 @@ impl Timestamp {
NaiveDateTime::from_timestamp_opt(sec, nsec)
}
pub fn to_chrono_datetime_with_timezone(&self, tz: Option<Timezone>) -> Option<NaiveDateTime> {
pub fn to_chrono_datetime_with_timezone(&self, tz: Option<&Timezone>) -> Option<NaiveDateTime> {
let datetime = self.to_chrono_datetime();
datetime.map(|v| match tz {
Some(Timezone::Offset(offset)) => offset.from_utc_datetime(&v).naive_local(),
@@ -369,7 +389,7 @@ impl FromStr for Timestamp {
/// - `2022-09-20 14:16:43` (Zulu timezone, without T)
/// - `2022-09-20 14:16:43.012345` (Zulu timezone, without T)
#[allow(deprecated)]
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
// RFC3339 timestamp (with a T)
let s = s.trim();
if let Ok(ts) = DateTime::parse_from_rfc3339(s) {
@@ -1113,47 +1133,77 @@ mod tests {
assert_eq!(
"1970-01-01 08:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("SYSTEM").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("SYSTEM").unwrap()))
);
assert_eq!(
"1970-01-01 08:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("SYSTEM").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("SYSTEM").unwrap()))
);
assert_eq!(
"1970-01-01 08:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("+08:00").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("+08:00").unwrap()))
);
assert_eq!(
"1970-01-01 07:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("+07:00").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("+07:00").unwrap()))
);
assert_eq!(
"1969-12-31 23:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("-01:00").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("-01:00").unwrap()))
);
assert_eq!(
"1970-01-01 08:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("Asia/Shanghai").unwrap()))
Timestamp::new(1, TimeUnit::Millisecond).to_timezone_aware_string(Some(
&Timezone::from_tz_string("Asia/Shanghai").unwrap()
))
);
assert_eq!(
"1970-01-01 00:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("UTC").unwrap()))
.to_timezone_aware_string(Some(&Timezone::from_tz_string("UTC").unwrap()))
);
assert_eq!(
"1970-01-01 01:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("Europe/Berlin").unwrap()))
Timestamp::new(1, TimeUnit::Millisecond).to_timezone_aware_string(Some(
&Timezone::from_tz_string("Europe/Berlin").unwrap()
))
);
assert_eq!(
"1970-01-01 03:00:00.001",
Timestamp::new(1, TimeUnit::Millisecond)
.to_timezone_aware_string(Some(Timezone::from_tz_string("Europe/Moscow").unwrap()))
Timestamp::new(1, TimeUnit::Millisecond).to_timezone_aware_string(Some(
&Timezone::from_tz_string("Europe/Moscow").unwrap()
))
);
}
#[test]
fn test_as_formatted_string() {
let ts = Timestamp::new(1, TimeUnit::Millisecond);
assert_eq!(
"1970-01-01",
ts.as_formatted_string("%Y-%m-%d", None).unwrap()
);
assert_eq!(
"1970-01-01 00:00:00",
ts.as_formatted_string("%Y-%m-%d %H:%M:%S", None).unwrap()
);
assert_eq!(
"1970-01-01T00:00:00:001",
ts.as_formatted_string("%Y-%m-%dT%H:%M:%S:%3f", None)
.unwrap()
);
assert_eq!(
"1970-01-01T08:00:00:001",
ts.as_formatted_string(
"%Y-%m-%dT%H:%M:%S:%3f",
Some(&Timezone::from_tz_string("Asia/Shanghai").unwrap())
)
.unwrap()
);
}

View File

@@ -43,13 +43,8 @@ pub fn set_default_timezone(tz_str: Option<&str>) -> Result<()> {
#[inline(always)]
/// If the `tz=Some(timezone)`, return `timezone` directly,
/// or return current system timezone.
pub fn get_timezone(tz: Option<Timezone>) -> Timezone {
tz.unwrap_or_else(|| {
DEFAULT_TIMEZONE
.get()
.cloned()
.unwrap_or(Timezone::Named(Tz::UTC))
})
pub fn get_timezone(tz: Option<&Timezone>) -> &Timezone {
tz.unwrap_or_else(|| DEFAULT_TIMEZONE.get().unwrap_or(&Timezone::Named(Tz::UTC)))
}
#[inline(always)]