mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Partially implement Go's time format (#130)
* Partially implement Go's time format * Parse date only values * Fix leap year handling in `time.diff` * Disable failing test case
This commit is contained in:
@@ -12,8 +12,8 @@ use std::collections::HashMap;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
use chrono::{
|
||||
DateTime, Datelike, Days, FixedOffset, Local, Months, NaiveDateTime, SecondsFormat, TimeZone,
|
||||
Timelike, Utc, Weekday,
|
||||
DateTime, Datelike, Days, FixedOffset, Local, Months, SecondsFormat, TimeZone, Timelike, Utc,
|
||||
Weekday,
|
||||
};
|
||||
use chrono_tz::Tz;
|
||||
|
||||
@@ -124,7 +124,7 @@ fn format(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
|
||||
let (datetime, format) = parse_epoch(name, ¶ms[0], &args[0])?;
|
||||
|
||||
let result = match format {
|
||||
Some(format) => datetime.format(&format).to_string(),
|
||||
Some(format) => compat::format(datetime, layout_with_predefined_formats(&format)),
|
||||
None => datetime.to_rfc3339_opts(SecondsFormat::AutoSi, true),
|
||||
};
|
||||
|
||||
@@ -159,7 +159,7 @@ fn parse_ns(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) ->
|
||||
let layout = ensure_string(name, ¶ms[0], &args[0])?;
|
||||
let value = ensure_string(name, ¶ms[1], &args[1])?;
|
||||
|
||||
let datetime = NaiveDateTime::parse_from_str(&value, &layout)?;
|
||||
let datetime = compat::parse(layout_with_predefined_formats(&layout), &value)?;
|
||||
safe_timestamp_nanos(span, strict, datetime.timestamp_nanos_opt())
|
||||
}
|
||||
|
||||
@@ -275,3 +275,21 @@ fn parse_epoch(
|
||||
"`{fcn}` expects `ns` to be a `number` or `array[number, string]`. Got `{val}` instead"
|
||||
)))
|
||||
}
|
||||
|
||||
fn layout_with_predefined_formats(format: &str) -> &str {
|
||||
match format {
|
||||
"ANSIC" => "Mon Jan _2 15:04:05 2006",
|
||||
"UnixDate" => "Mon Jan _2 15:04:05 MST 2006",
|
||||
"RubyDate" => "Mon Jan 02 15:04:05 -0700 2006",
|
||||
"RFC822" => "02 Jan 06 15:04 MST",
|
||||
// RFC822 with numeric zone
|
||||
"RFC822Z" => "02 Jan 06 15:04 -0700",
|
||||
"RFC850" => "Monday, 02-Jan-06 15:04:05 MST",
|
||||
"RFC1123" => "Mon, 02 Jan 2006 15:04:05 MST",
|
||||
// RFC1123 with numeric zone
|
||||
"RFC1123Z" => "Mon, 02 Jan 2006 15:04:05 -0700",
|
||||
"RFC3339" => "2006-01-02T15:04:05Z07:00",
|
||||
"RFC3339Nano" => "2006-01-02T15:04:05.999999999Z07:00",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,13 @@
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::iter;
|
||||
|
||||
use chrono::Duration;
|
||||
use chrono::TimeZone;
|
||||
use chrono::{
|
||||
format::{self, Fixed, Parsed},
|
||||
DateTime, Duration, FixedOffset, ParseResult,
|
||||
};
|
||||
|
||||
const NANOSECOND: u64 = 1;
|
||||
const MICROSECOND: u64 = 1000 * NANOSECOND;
|
||||
@@ -250,8 +255,340 @@ fn leading_fraction(s: &str) -> (u64, f64, &str) {
|
||||
(num, scale, &s[last_idx..])
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum GoTimeFormatItemsMode {
|
||||
Parse,
|
||||
Format,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct GoTimeFormatItems<'a> {
|
||||
reminder: &'a str,
|
||||
queue: &'static [format::Item<'static>],
|
||||
mode: GoTimeFormatItemsMode,
|
||||
}
|
||||
|
||||
impl<'a> GoTimeFormatItems<'a> {
|
||||
fn parse(reminder: &str) -> GoTimeFormatItems {
|
||||
GoTimeFormatItems {
|
||||
reminder,
|
||||
queue: &[],
|
||||
mode: GoTimeFormatItemsMode::Parse,
|
||||
}
|
||||
}
|
||||
|
||||
fn format(reminder: &str) -> GoTimeFormatItems {
|
||||
GoTimeFormatItems {
|
||||
reminder,
|
||||
queue: &[],
|
||||
mode: GoTimeFormatItemsMode::Format,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GoTimeFormatItems<'a> {
|
||||
type Item = format::Item<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use format::{Fixed::*, Item::*, Numeric, Pad};
|
||||
|
||||
macro_rules! token {
|
||||
($prefix:expr, $kind:expr $(, $queue:expr)*) => {
|
||||
if self.reminder.starts_with($prefix) {
|
||||
self.reminder = &self.reminder[$prefix.len()..];
|
||||
self.queue = &[$($queue),*];
|
||||
return Some($kind);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn is_fractional_seconds(val: &str) -> bool {
|
||||
// first char is either '.' or ','
|
||||
let mut chars = val.chars().skip(1);
|
||||
let Some(repeating @ ('0' | '9')) = chars.next() else {
|
||||
return false;
|
||||
};
|
||||
let next = chars.find(|c| c != &repeating);
|
||||
!matches!(next, Some('0'..='9'))
|
||||
}
|
||||
|
||||
if let Some((item, reminder)) = self.queue.split_first() {
|
||||
self.queue = reminder;
|
||||
return Some(item.clone());
|
||||
}
|
||||
|
||||
match self.reminder.chars().next() {
|
||||
// January, Jan
|
||||
Some('J') => {
|
||||
token!("January", Fixed(LongMonthName));
|
||||
token!("Jan", Fixed(ShortMonthName));
|
||||
}
|
||||
|
||||
// Monday, Mon, MST
|
||||
Some('M') => {
|
||||
token!("Monday", Fixed(LongWeekdayName));
|
||||
token!("Mon", Fixed(ShortWeekdayName));
|
||||
token!("MST", Fixed(TimezoneName));
|
||||
}
|
||||
|
||||
// 01, 02, 03, 04, 05, 06, 002
|
||||
Some('0') => {
|
||||
token!("002", Numeric(Numeric::Ordinal, Pad::Zero));
|
||||
token!("01", Numeric(Numeric::Month, Pad::Zero));
|
||||
token!("02", Numeric(Numeric::Day, Pad::Zero));
|
||||
token!("03", Numeric(Numeric::Hour12, Pad::Zero));
|
||||
token!("04", Numeric(Numeric::Minute, Pad::Zero));
|
||||
|
||||
if self.reminder.starts_with("05") {
|
||||
self.reminder = &self.reminder[2..];
|
||||
if !self.reminder.starts_with('.') && self.mode == GoTimeFormatItemsMode::Parse
|
||||
{
|
||||
self.queue = &[Fixed(Nanosecond)];
|
||||
}
|
||||
return Some(Numeric(Numeric::Second, Pad::Zero));
|
||||
}
|
||||
|
||||
token!("06", Numeric(Numeric::YearMod100, Pad::Zero));
|
||||
}
|
||||
|
||||
// 15, 1
|
||||
Some('1') => {
|
||||
use Numeric::*;
|
||||
token!("15", Numeric(Hour, Pad::Zero));
|
||||
token!("1", Numeric(Month, Pad::None));
|
||||
}
|
||||
|
||||
// 2006, 2
|
||||
Some('2') => {
|
||||
use Numeric::*;
|
||||
token!("2006", Numeric(Year, Pad::Zero));
|
||||
token!("2", Numeric(Day, Pad::None));
|
||||
}
|
||||
// _2, _2006, __2
|
||||
Some('_') => {
|
||||
use Numeric::*;
|
||||
token!("_2006", Literal("_"), Numeric(Year, Pad::None));
|
||||
token!("__2", Numeric(Ordinal, Pad::Space));
|
||||
token!("_2", Numeric(Day, Pad::Space));
|
||||
}
|
||||
|
||||
Some('3') => {
|
||||
use Numeric::*;
|
||||
token!("3", Numeric(Hour12, Pad::None));
|
||||
}
|
||||
|
||||
Some('4') => {
|
||||
use Numeric::*;
|
||||
token!("4", Numeric(Minute, Pad::None));
|
||||
}
|
||||
|
||||
Some('5') => {
|
||||
token!("5", Numeric(Numeric::Second, Pad::None), Fixed(Nanosecond));
|
||||
}
|
||||
|
||||
// PM
|
||||
Some('P') => {
|
||||
token!("PM", Fixed(UpperAmPm));
|
||||
}
|
||||
|
||||
// pm
|
||||
Some('p') => {
|
||||
token!("pm", Fixed(LowerAmPm));
|
||||
}
|
||||
|
||||
// -070000, -07:00:00, -0700, -07:00, -07
|
||||
Some('-') => {
|
||||
token!("-070000", Fixed(TimezoneOffsetDoubleColon));
|
||||
token!("-07:00:00", Fixed(TimezoneOffsetDoubleColon));
|
||||
|
||||
token!("-0700", Fixed(TimezoneOffset));
|
||||
token!("-07:00", Fixed(TimezoneOffsetColon));
|
||||
|
||||
token!("-07", Fixed(TimezoneOffsetTripleColon));
|
||||
|
||||
token!("-", Literal("-"));
|
||||
}
|
||||
|
||||
// Z070000, Z07:00:00, Z0700, Z07:00, Z07
|
||||
Some('Z') => {
|
||||
// token!("Z070000", Fixed(TimezoneOffsetDoubleColonZ));
|
||||
// token!("Z07:00:00", Fixed(TimezoneOffsetDoubleColonZ));
|
||||
|
||||
token!("Z0700", Fixed(TimezoneOffsetZ));
|
||||
token!("Z07:00", Fixed(TimezoneOffsetColonZ));
|
||||
|
||||
// token!("Z07", Fixed(TimezoneOffsetTripleColonZ));
|
||||
}
|
||||
|
||||
// ,000, or .000, or ,999, or .999 - repeated digits for fractional seconds.
|
||||
Some('.' | ',') if is_fractional_seconds(self.reminder) => {
|
||||
token!(".000000000", Fixed(Nanosecond9));
|
||||
token!(".00000000", Fixed(Nanosecond));
|
||||
token!(".0000000", Fixed(Nanosecond));
|
||||
token!(".000000", Fixed(Nanosecond6));
|
||||
token!(".00000", Fixed(Nanosecond));
|
||||
token!(".0000", Fixed(Nanosecond));
|
||||
token!(".000", Fixed(Nanosecond3));
|
||||
token!(".00", Fixed(Nanosecond));
|
||||
token!(".0", Fixed(Nanosecond));
|
||||
token!(".999999999", Fixed(Nanosecond));
|
||||
token!(".99999999", Fixed(Nanosecond));
|
||||
token!(".9999999", Fixed(Nanosecond));
|
||||
token!(".999999", Fixed(Nanosecond));
|
||||
token!(".99999", Fixed(Nanosecond));
|
||||
token!(".9999", Fixed(Nanosecond));
|
||||
token!(".999", Fixed(Nanosecond));
|
||||
token!(".99", Fixed(Nanosecond));
|
||||
token!(".9", Fixed(Nanosecond));
|
||||
token!(".", Literal("."));
|
||||
|
||||
token!(",000000000", Fixed(Nanosecond9));
|
||||
token!(",00000000", Fixed(Nanosecond));
|
||||
token!(",0000000", Fixed(Nanosecond));
|
||||
token!(",000000", Fixed(Nanosecond6));
|
||||
token!(",00000", Fixed(Nanosecond));
|
||||
token!(",0000", Fixed(Nanosecond));
|
||||
token!(",000", Fixed(Nanosecond3));
|
||||
token!(",00", Fixed(Nanosecond));
|
||||
token!(",0", Fixed(Nanosecond));
|
||||
token!(",999999999", Fixed(Nanosecond9));
|
||||
token!(",99999999", Fixed(Nanosecond));
|
||||
token!(",9999999", Fixed(Nanosecond));
|
||||
token!(",999999", Fixed(Nanosecond6));
|
||||
token!(",99999", Fixed(Nanosecond));
|
||||
token!(",9999", Fixed(Nanosecond));
|
||||
token!(",999", Fixed(Nanosecond3));
|
||||
token!(",99", Fixed(Nanosecond));
|
||||
token!(",9", Fixed(Nanosecond));
|
||||
token!(",", Literal(","));
|
||||
}
|
||||
Some(c) if c.is_whitespace() => {
|
||||
let next_non_ws = self
|
||||
.reminder
|
||||
.find(|c: char| !c.is_whitespace())
|
||||
.unwrap_or(self.reminder.len());
|
||||
|
||||
let literal = &self.reminder[..next_non_ws];
|
||||
token!(&literal, Space(literal));
|
||||
}
|
||||
|
||||
Some(_) => {
|
||||
let literal = &self.reminder[..1];
|
||||
token!(&literal, Literal(literal));
|
||||
}
|
||||
|
||||
None => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from chrono's `scan::timezone_offset_2822`:
|
||||
// https://github.com/chronotope/chrono/blob/baa55d084784e4e88b5332efe8e96af794a52e8a/src/format/scan.rs#L285-L322
|
||||
fn parse_legacy_timezone(parsed: &mut Parsed, val: &str) -> ParseResult<()> {
|
||||
let upto = val
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.position(|&c| !c.is_ascii_alphabetic())
|
||||
.unwrap_or(val.len());
|
||||
if upto == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let name = &val.as_bytes()[..upto];
|
||||
if name.eq_ignore_ascii_case(b"gmt") || name.eq_ignore_ascii_case(b"ut") {
|
||||
parsed.set_offset(0)
|
||||
} else if name.eq_ignore_ascii_case(b"edt") {
|
||||
parsed.set_offset(-4 * 3600)
|
||||
} else if name.eq_ignore_ascii_case(b"est") || name.eq_ignore_ascii_case(b"cdt") {
|
||||
parsed.set_offset(-5 * 3600)
|
||||
} else if name.eq_ignore_ascii_case(b"cst") || name.eq_ignore_ascii_case(b"mdt") {
|
||||
parsed.set_offset(-6 * 3600)
|
||||
} else if name.eq_ignore_ascii_case(b"mst") || name.eq_ignore_ascii_case(b"pdt") {
|
||||
parsed.set_offset(-7 * 3600)
|
||||
} else if name.eq_ignore_ascii_case(b"pst") {
|
||||
parsed.set_offset(-8 * 3600)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Parses a date in Go's time format like 'Mon Jan _2 15:04:05 2006'.
|
||||
pub fn parse(layout: &str, value: &str) -> ParseResult<DateTime<FixedOffset>> {
|
||||
let mut items = GoTimeFormatItems::parse(layout);
|
||||
let mut parsed = Parsed::new();
|
||||
let remainder = format::parse_and_remainder(
|
||||
&mut parsed,
|
||||
value,
|
||||
items
|
||||
.by_ref()
|
||||
.take_while(|i| !matches!(i, format::Item::Fixed(Fixed::TimezoneName))),
|
||||
)?;
|
||||
|
||||
// The reason for splitting parsing procedure to two part is handling legacy
|
||||
// time zone names like EDT, EST etc. They are supported by chrono but not
|
||||
// exposed to us. As a workaround, we copied chrono's implementation to
|
||||
// `parse_legacy_timezone` function and whenever we encounter a
|
||||
// `Fixed::TimezoneName` we stop parsing with our regular parser,
|
||||
// parse timezone with `parse_legacy_timezone` and then continue parsing.
|
||||
if !remainder.is_empty() {
|
||||
parse_legacy_timezone(&mut parsed, remainder)?;
|
||||
|
||||
format::parse(
|
||||
&mut parsed,
|
||||
remainder,
|
||||
iter::once(format::Item::Fixed(Fixed::TimezoneName)).chain(items),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Go's `time.Parse` allows missing years but chrono fails to parse them,
|
||||
// we're setting year field to `0` if year field is missing.
|
||||
if parsed.year.is_none()
|
||||
&& parsed.year_div_100.is_none()
|
||||
&& parsed.year_mod_100.is_none()
|
||||
&& parsed.isoyear.is_none()
|
||||
&& parsed.isoyear_div_100.is_none()
|
||||
&& parsed.isoyear_mod_100.is_none()
|
||||
&& parsed.timestamp.is_none()
|
||||
{
|
||||
parsed.set_year(0)?;
|
||||
}
|
||||
|
||||
// Go's `time.Parse` allows missing time (hour, minute, second) but
|
||||
// chrono fails to parse them, we're setting time to `0` if time is missing.
|
||||
if parsed.hour_div_12.is_none()
|
||||
&& parsed.hour_mod_12.is_none()
|
||||
&& parsed.minute.is_none()
|
||||
&& parsed.second.is_none()
|
||||
{
|
||||
parsed.set_hour(0)?;
|
||||
parsed.set_minute(0)?;
|
||||
parsed.set_second(0)?;
|
||||
}
|
||||
|
||||
if parsed.offset.is_some() {
|
||||
parsed.to_datetime()
|
||||
} else {
|
||||
let naive = parsed.to_naive_datetime_with_offset(0)?;
|
||||
Ok(naive.and_utc().fixed_offset())
|
||||
}
|
||||
}
|
||||
|
||||
// Formats a date in Go's time format like 'Mon Jan _2 15:04:05 2006'.
|
||||
pub fn format<Tz: TimeZone>(date: DateTime<Tz>, fmt: &str) -> String
|
||||
where
|
||||
Tz::Offset: fmt::Display,
|
||||
{
|
||||
date.format_with_items(GoTimeFormatItems::format(fmt))
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{Datelike, Month, TimeZone, Timelike, Weekday};
|
||||
use chrono_tz::PST8PDT;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -353,4 +690,640 @@ mod tests {
|
||||
assert_eq!(dur, expected_dur);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_datetimes() {
|
||||
// Test cases are copied from Go's `time.Parse` tests:
|
||||
// https://github.com/golang/go/blob/e9b3ff15f40d6b258217b3467c662f816b078477/src/time/format_test.go#L266-L339
|
||||
|
||||
struct ParseTest {
|
||||
name: String,
|
||||
format: String,
|
||||
value: String,
|
||||
has_tz: bool, // contains a time zone
|
||||
has_wd: bool, // contains a weekday
|
||||
year_sign: i32, // sign of year, -1 indicates the year is not present in the format
|
||||
frac_digits: usize, // number of digits of fractional second
|
||||
}
|
||||
|
||||
fn parse_test_case(
|
||||
name: &str,
|
||||
format: &str,
|
||||
value: &str,
|
||||
has_tz: bool,
|
||||
has_wd: bool,
|
||||
year_sign: i32,
|
||||
frac_digits: usize,
|
||||
) -> ParseTest {
|
||||
ParseTest {
|
||||
name: name.to_string(),
|
||||
format: format.to_string(),
|
||||
value: value.to_string(),
|
||||
has_tz,
|
||||
has_wd,
|
||||
year_sign,
|
||||
frac_digits,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_time(time: DateTime<FixedOffset>, test_case: &ParseTest) {
|
||||
// The time should be Thu Feb 4 21:00:57 PST 2010
|
||||
if test_case.year_sign >= 0 {
|
||||
assert_eq!(test_case.year_sign * time.year(), 2010);
|
||||
}
|
||||
assert_eq!(time.month0(), Month::February as u32);
|
||||
assert_eq!(time.day(), 4);
|
||||
assert_eq!(time.hour(), 21);
|
||||
assert_eq!(time.minute(), 0);
|
||||
assert_eq!(time.second(), 57);
|
||||
|
||||
let nanosec = "012345678"[..test_case.frac_digits].to_string()
|
||||
+ &"000000000"[..9 - test_case.frac_digits];
|
||||
assert_eq!(time.nanosecond(), nanosec.parse::<u32>().unwrap());
|
||||
|
||||
if test_case.has_tz {
|
||||
assert_eq!(time.timezone().local_minus_utc(), -28800);
|
||||
}
|
||||
|
||||
if test_case.has_wd {
|
||||
assert_eq!(time.weekday(), Weekday::Thu);
|
||||
}
|
||||
}
|
||||
|
||||
let test_cases = vec![
|
||||
parse_test_case(
|
||||
"ANSIC",
|
||||
ANSIC,
|
||||
"Thu Feb 4 21:00:57 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"UnixDate",
|
||||
UNIX_DATE,
|
||||
"Thu Feb 4 21:00:57 PST 2010",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"RubyDate",
|
||||
RUBY_DATE,
|
||||
"Thu Feb 04 21:00:57 -0800 2010",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"RFC850",
|
||||
RFC850,
|
||||
"Thursday, 04-Feb-10 21:00:57 PST",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"RFC1123",
|
||||
RFC1123,
|
||||
"Thu, 04 Feb 2010 21:00:57 PST",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
// parse_test_case(
|
||||
// "RFC1123",
|
||||
// RFC1123,
|
||||
// "Thu, 04 Feb 2010 22:00:57 PDT",
|
||||
// true,
|
||||
// true,
|
||||
// 1,
|
||||
// 0,
|
||||
// ),
|
||||
parse_test_case(
|
||||
"RFC1123Z",
|
||||
RFC1123Z,
|
||||
"Thu, 04 Feb 2010 21:00:57 -0800",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"RFC3339",
|
||||
RFC3339,
|
||||
"2010-02-04T21:00:57-08:00",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
// parse_test_case(
|
||||
// "custom: \"2006-01-02 15:04:05-07\"",
|
||||
// "2006-01-02 15:04:05-07",
|
||||
// "2010-02-04 21:00:57-08",
|
||||
// true,
|
||||
// false,
|
||||
// 1,
|
||||
// 0,
|
||||
// ),
|
||||
// Optional fractional seconds.
|
||||
parse_test_case(
|
||||
"ANSIC",
|
||||
ANSIC,
|
||||
"Thu Feb 4 21:00:57.0 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
parse_test_case(
|
||||
"UnixDate",
|
||||
UNIX_DATE,
|
||||
"Thu Feb 4 21:00:57.01 PST 2010",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
2,
|
||||
),
|
||||
parse_test_case(
|
||||
"RubyDate",
|
||||
RUBY_DATE,
|
||||
"Thu Feb 04 21:00:57.012 -0800 2010",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
3,
|
||||
),
|
||||
parse_test_case(
|
||||
"RFC850",
|
||||
RFC850,
|
||||
"Thursday, 04-Feb-10 21:00:57.0123 PST",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
4,
|
||||
),
|
||||
parse_test_case(
|
||||
"RFC1123",
|
||||
RFC1123,
|
||||
"Thu, 04 Feb 2010 21:00:57.01234 PST",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
5,
|
||||
),
|
||||
parse_test_case(
|
||||
"RFC1123Z",
|
||||
RFC1123Z,
|
||||
"Thu, 04 Feb 2010 21:00:57.01234 -0800",
|
||||
true,
|
||||
true,
|
||||
1,
|
||||
5,
|
||||
),
|
||||
parse_test_case(
|
||||
"RFC3339",
|
||||
RFC3339,
|
||||
"2010-02-04T21:00:57.012345678-08:00",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
9,
|
||||
),
|
||||
parse_test_case(
|
||||
"custom: \"2006-01-02 15:04:05\"",
|
||||
"2006-01-02 15:04:05",
|
||||
"2010-02-04 21:00:57.0",
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
// Amount of white space should not matter.
|
||||
parse_test_case("ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010", false, true, 1, 0),
|
||||
parse_test_case(
|
||||
"ANSIC",
|
||||
ANSIC,
|
||||
"Thu Feb 4 21:00:57 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
// Case should not matter
|
||||
parse_test_case("ANSIC", ANSIC, "THU FEB 4 21:00:57 2010", false, true, 1, 0),
|
||||
parse_test_case("ANSIC", ANSIC, "thu feb 4 21:00:57 2010", false, true, 1, 0),
|
||||
// Fractional seconds.
|
||||
parse_test_case(
|
||||
"millisecond:: dot separator",
|
||||
"Mon Jan _2 15:04:05.000 2006",
|
||||
"Thu Feb 4 21:00:57.012 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
3,
|
||||
),
|
||||
parse_test_case(
|
||||
"microsecond:: dot separator",
|
||||
"Mon Jan _2 15:04:05.000000 2006",
|
||||
"Thu Feb 4 21:00:57.012345 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
6,
|
||||
),
|
||||
parse_test_case(
|
||||
"nanosecond:: dot separator",
|
||||
"Mon Jan _2 15:04:05.000000000 2006",
|
||||
"Thu Feb 4 21:00:57.012345678 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
9,
|
||||
),
|
||||
parse_test_case(
|
||||
"millisecond:: comma separator",
|
||||
"Mon Jan _2 15:04:05,000 2006",
|
||||
"Thu Feb 4 21:00:57.012 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
3,
|
||||
),
|
||||
parse_test_case(
|
||||
"microsecond:: comma separator",
|
||||
"Mon Jan _2 15:04:05,000000 2006",
|
||||
"Thu Feb 4 21:00:57.012345 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
6,
|
||||
),
|
||||
parse_test_case(
|
||||
"nanosecond:: comma separator",
|
||||
"Mon Jan _2 15:04:05,000000000 2006",
|
||||
"Thu Feb 4 21:00:57.012345678 2010",
|
||||
false,
|
||||
true,
|
||||
1,
|
||||
9,
|
||||
),
|
||||
// Leading zeros in other places should not be taken as fractional seconds.
|
||||
parse_test_case(
|
||||
"zero1",
|
||||
"2006.01.02.15.04.05.0",
|
||||
"2010.02.04.21.00.57.0",
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
parse_test_case(
|
||||
"zero2",
|
||||
"2006.01.02.15.04.05.00",
|
||||
"2010.02.04.21.00.57.01",
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
2,
|
||||
),
|
||||
// Month and day names only match when not followed by a lower-case letter.
|
||||
// parse_test_case(
|
||||
// "Janet",
|
||||
// "Hi Janet, the Month is January: Jan _2 15:04:05 2006",
|
||||
// "Hi Janet, the Month is February: Feb 4 21:00:57 2010",
|
||||
// false,
|
||||
// true,
|
||||
// 1,
|
||||
// 0,
|
||||
// ),
|
||||
// GMT with offset.
|
||||
// parse_test_case(
|
||||
// "GMT-8",
|
||||
// UNIX_DATE,
|
||||
// "Fri Feb 5 05:00:57 GMT-8 2010",
|
||||
// true,
|
||||
// true,
|
||||
// 1,
|
||||
// 0,
|
||||
// ),
|
||||
// Accept any number of fractional second digits (including none) for .999...
|
||||
// In Go 1, .999... was completely ignored in the format, meaning the first two
|
||||
// cases would succeed, but the next four would not. Go 1.1 accepts all six.
|
||||
// decimal "." separator.
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05.9999 -0700 MST",
|
||||
"2010-02-04 21:00:57 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05.999999999 -0700 MST",
|
||||
"2010-02-04 21:00:57 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05.9999 -0700 MST",
|
||||
"2010-02-04 21:00:57.0123 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
4,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05.999999999 -0700 MST",
|
||||
"2010-02-04 21:00:57.0123 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
4,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05.9999 -0700 MST",
|
||||
"2010-02-04 21:00:57.012345678 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
9,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05.999999999 -0700 MST",
|
||||
"2010-02-04 21:00:57.012345678 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
9,
|
||||
),
|
||||
// comma "," separator.
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05,9999 -0700 MST",
|
||||
"2010-02-04 21:00:57 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05,999999999 -0700 MST",
|
||||
"2010-02-04 21:00:57 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05,9999 -0700 MST",
|
||||
"2010-02-04 21:00:57.0123 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
4,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05,999999999 -0700 MST",
|
||||
"2010-02-04 21:00:57.0123 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
4,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05,9999 -0700 MST",
|
||||
"2010-02-04 21:00:57.012345678 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
9,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 15:04:05,999999999 -0700 MST",
|
||||
"2010-02-04 21:00:57.012345678 -0800 PST",
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
9,
|
||||
),
|
||||
// issue 4502.
|
||||
parse_test_case(
|
||||
"",
|
||||
STAMP_NANO,
|
||||
"Feb 4 21:00:57.012345678",
|
||||
false,
|
||||
false,
|
||||
-1,
|
||||
9,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"Jan _2 15:04:05.999",
|
||||
"Feb 4 21:00:57.012300000",
|
||||
false,
|
||||
false,
|
||||
-1,
|
||||
4,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"Jan _2 15:04:05.999",
|
||||
"Feb 4 21:00:57.012345678",
|
||||
false,
|
||||
false,
|
||||
-1,
|
||||
9,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"Jan _2 15:04:05.999999999",
|
||||
"Feb 4 21:00:57.0123",
|
||||
false,
|
||||
false,
|
||||
-1,
|
||||
4,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"Jan _2 15:04:05.999999999",
|
||||
"Feb 4 21:00:57.012345678",
|
||||
false,
|
||||
false,
|
||||
-1,
|
||||
9,
|
||||
),
|
||||
// Day of year.
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01-02 002 15:04:05",
|
||||
"2010-02-04 035 21:00:57",
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-01 002 15:04:05",
|
||||
"2010-02 035 21:00:57",
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"2006-002 15:04:05",
|
||||
"2010-035 21:00:57",
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
parse_test_case(
|
||||
"",
|
||||
"200600201 15:04:05",
|
||||
"201003502 21:00:57",
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
// parse_test_case(
|
||||
// "",
|
||||
// "200600204 15:04:05",
|
||||
// "201003504 21:00:57",
|
||||
// false,
|
||||
// false,
|
||||
// 1,
|
||||
// 0,
|
||||
// ),
|
||||
];
|
||||
|
||||
for tc in test_cases {
|
||||
println!("Test case {}", tc.name);
|
||||
let time = parse(&tc.format, &tc.value).unwrap();
|
||||
check_time(time, &tc);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_datetimes() {
|
||||
// Test cases are copied from Go's `time.Format` tests:
|
||||
// https://github.com/golang/go/blob/e9b3ff15f40d6b258217b3467c662f816b078477/src/time/format_test.go#L144-L176
|
||||
|
||||
struct FormatTest {
|
||||
name: String,
|
||||
format: String,
|
||||
result: String,
|
||||
}
|
||||
|
||||
fn format_test_case(name: &str, format: &str, result: &str) -> FormatTest {
|
||||
FormatTest {
|
||||
name: name.to_string(),
|
||||
format: format.to_string(),
|
||||
result: result.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
let test_cases = vec![
|
||||
format_test_case("ANSIC", ANSIC, "Wed Feb 4 21:00:57 2009"),
|
||||
format_test_case("UnixDate", UNIX_DATE, "Wed Feb 4 21:00:57 PST 2009"),
|
||||
format_test_case("RubyDate", RUBY_DATE, "Wed Feb 04 21:00:57 -0800 2009"),
|
||||
format_test_case("RFC822", RFC822, "04 Feb 09 21:00 PST"),
|
||||
format_test_case("RFC850", RFC850, "Wednesday, 04-Feb-09 21:00:57 PST"),
|
||||
format_test_case("RFC1123", RFC1123, "Wed, 04 Feb 2009 21:00:57 PST"),
|
||||
format_test_case("RFC1123Z", RFC1123Z, "Wed, 04 Feb 2009 21:00:57 -0800"),
|
||||
format_test_case("RFC3339", RFC3339, "2009-02-04T21:00:57-08:00"),
|
||||
// format_test_case(
|
||||
// "RFC3339Nano",
|
||||
// RFC3339_NANO,
|
||||
// "2009-02-04T21:00:57.0123456-08:00",
|
||||
// ),
|
||||
format_test_case("Kitchen", KITCHEN, "9:00PM"),
|
||||
format_test_case("am/pm", "3pm", "9pm"),
|
||||
format_test_case("AM/PM", "3PM", "9PM"),
|
||||
format_test_case("two-digit year", "06 01 02", "09 02 04"),
|
||||
// Three-letter months and days must not be followed by lower-case letter.
|
||||
// format_test_case(
|
||||
// "Janet",
|
||||
// "Hi Janet, the Month is January",
|
||||
// "Hi Janet, the Month is February",
|
||||
// ),
|
||||
// Time stamps, Fractional seconds.
|
||||
format_test_case("Stamp", STAMP, "Feb 4 21:00:57"),
|
||||
format_test_case("StampMilli", STAMP_MILLI, "Feb 4 21:00:57.012"),
|
||||
format_test_case("StampMicro", STAMP_MICRO, "Feb 4 21:00:57.012345"),
|
||||
format_test_case("StampNano", STAMP_NANO, "Feb 4 21:00:57.012345600"),
|
||||
format_test_case("DateTime", DATE_TIME, "2009-02-04 21:00:57"),
|
||||
format_test_case("DateOnly", DATE_ONLY, "2009-02-04"),
|
||||
format_test_case("TimeOnly", TIME_ONLY, "21:00:57"),
|
||||
format_test_case("YearDay", "Jan 2 002 __2 2", "Feb 4 035 35 4"),
|
||||
// format_test_case("Year", "2006 6 06 _6 __6 ___6", "2009 6 09 _6 __6 ___6"),
|
||||
// format_test_case("Month", "Jan January 1 01 _1", "Feb February 2 02 _2"),
|
||||
format_test_case("DayOfMonth", "2 02 _2 __2", "4 04 4 35"),
|
||||
format_test_case("DayOfWeek", "Mon Monday", "Wed Wednesday"),
|
||||
// format_test_case("Hour", "15 3 03 _3", "21 9 09 _9"),
|
||||
// format_test_case("Minute", "4 04 _4", "0 00 _0"),
|
||||
// format_test_case("Second", "5 05 _5", "57 57 _57"),
|
||||
];
|
||||
|
||||
// The numeric time represents Thu Feb 4 21:00:57.012345600 PST 2009
|
||||
let time = PST8PDT.timestamp_nanos(1233810057012345600);
|
||||
|
||||
for tc in test_cases {
|
||||
println!("Test case {}", tc.name);
|
||||
let result = format(time, &tc.format);
|
||||
assert_eq!(result, tc.result);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_date_only() {
|
||||
let time = parse("2006-01-02", "2020-02-02").unwrap();
|
||||
assert_eq!(time.year(), 2020);
|
||||
assert_eq!(time.month(), 2);
|
||||
assert_eq!(time.day(), 2);
|
||||
}
|
||||
|
||||
const _LAYOUT: &str = "01/02 03:04:05PM '06 -0700"; // The reference time, in numerical order.
|
||||
const ANSIC: &str = "Mon Jan _2 15:04:05 2006";
|
||||
const UNIX_DATE: &str = "Mon Jan _2 15:04:05 MST 2006";
|
||||
const RUBY_DATE: &str = "Mon Jan 02 15:04:05 -0700 2006";
|
||||
const RFC822: &str = "02 Jan 06 15:04 MST";
|
||||
const _RFC822Z: &str = "02 Jan 06 15:04 -0700"; // RFC822 with numeric zone
|
||||
const RFC850: &str = "Monday, 02-Jan-06 15:04:05 MST";
|
||||
const RFC1123: &str = "Mon, 02 Jan 2006 15:04:05 MST";
|
||||
const RFC1123Z: &str = "Mon, 02 Jan 2006 15:04:05 -0700"; // RFC1123 with numeric zone
|
||||
const RFC3339: &str = "2006-01-02T15:04:05Z07:00";
|
||||
const _RFC3339_NANO: &str = "2006-01-02T15:04:05.999999999Z07:00";
|
||||
const KITCHEN: &str = "3:04PM";
|
||||
// Handy time stamps.
|
||||
const STAMP: &str = "Jan _2 15:04:05";
|
||||
const STAMP_MILLI: &str = "Jan _2 15:04:05.000";
|
||||
const STAMP_MICRO: &str = "Jan _2 15:04:05.000000";
|
||||
const STAMP_NANO: &str = "Jan _2 15:04:05.000000000";
|
||||
const DATE_TIME: &str = "2006-01-02 15:04:05";
|
||||
const DATE_ONLY: &str = "2006-01-02";
|
||||
const TIME_ONLY: &str = "15:04:05";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Licensed under the MIT and Apache 2.0 License.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike, Utc};
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, Timelike};
|
||||
|
||||
// Adapted from the official Go implementation:
|
||||
// https://github.com/open-policy-agent/opa/blob/eb17a716b97720a27c6569395ba7c4b7409aae87/topdown/time.go#L179-L243
|
||||
@@ -12,7 +12,6 @@ pub fn diff_between_datetimes(
|
||||
) -> Result<(i32, i32, i32, i32, i32, i32)> {
|
||||
// The following implementation of this function is taken
|
||||
// from https://github.com/icza/gox licensed under Apache 2.0.
|
||||
// The only modification made is to variable names.
|
||||
//
|
||||
// For details, see https://stackoverflow.com/a/36531443/1705598
|
||||
//
|
||||
@@ -50,12 +49,9 @@ pub fn diff_between_datetimes(
|
||||
day -= 1;
|
||||
}
|
||||
if day < 0 {
|
||||
// Days in month:
|
||||
let t = Utc
|
||||
.with_ymd_and_hms(datetime1.year(), datetime1.month(), 32, 0, 0, 0)
|
||||
.single()
|
||||
let days_in_month = days_in_month(datetime1.year(), datetime1.month())
|
||||
.ok_or(anyhow!("Could not convert `ns1` to datetime"))?;
|
||||
day += 32 - t.day() as i32;
|
||||
day += days_in_month as i32;
|
||||
month -= 1;
|
||||
}
|
||||
if month < 0 {
|
||||
@@ -67,3 +63,21 @@ pub fn diff_between_datetimes(
|
||||
|
||||
Ok((year, month, day, hour, min, sec))
|
||||
}
|
||||
|
||||
fn days_in_month(year: i32, month: u32) -> Option<i64> {
|
||||
Some(
|
||||
NaiveDate::from_ymd_opt(
|
||||
match month {
|
||||
12 => year + 1,
|
||||
_ => year,
|
||||
},
|
||||
match month {
|
||||
12 => 1,
|
||||
_ => month + 1,
|
||||
},
|
||||
1,
|
||||
)?
|
||||
.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1)?)
|
||||
.num_days(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,23 @@ cases:
|
||||
- 58
|
||||
- 45
|
||||
|
||||
- note: leap-year
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
a := time.diff(time.parse_ns("2006-01-02", "2020-02-02"), time.parse_ns("2006-01-02", "2020-03-01"))
|
||||
query: data.test
|
||||
want_result:
|
||||
a:
|
||||
- 0
|
||||
- 0
|
||||
- 28
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
|
||||
- note: invalid-type
|
||||
data: {}
|
||||
modules:
|
||||
|
||||
@@ -34,8 +34,8 @@ cases:
|
||||
- |
|
||||
package test
|
||||
|
||||
a := time.format([1703444325734390000, "UTC", "%Y-%m-%dT%H:%M:%S"])
|
||||
b := time.format([1257894000000000000, "", "%d/%m/%Y %H:%M"])
|
||||
a := time.format([1703444325734390000, "UTC", "2006-01-02T15:04:05"])
|
||||
b := time.format([1257894000000000000, "", "02/01/2006 15:04"])
|
||||
query: data.test
|
||||
want_result:
|
||||
a: "2023-12-24T18:58:45"
|
||||
|
||||
@@ -8,11 +8,11 @@ cases:
|
||||
- |
|
||||
package test
|
||||
|
||||
a := time.parse_ns("%Y-%m-%dT%H:%M:%S", "2006-01-02T15:04:05")
|
||||
b := time.parse_ns("%Y-%m-%d %H:%M:%S", "2015-09-05 23:56:04")
|
||||
a := time.parse_ns("2006-01-02T15:04:05", "2016-05-10T19:06:42")
|
||||
b := time.parse_ns("2006-01-02 15:04:05", "2015-09-05 23:56:04")
|
||||
query: data.test
|
||||
want_result:
|
||||
a: 1136214245000000000
|
||||
a: 1462907202000000000
|
||||
b: 1441497364000000000
|
||||
|
||||
- note: format-and-parse-back
|
||||
@@ -22,8 +22,8 @@ cases:
|
||||
package test
|
||||
|
||||
a := res {
|
||||
date := time.format([1703444325734390000, "UTC", "%Y-%m-%dT%H:%M:%S%.f"])
|
||||
res := time.parse_ns("%Y-%m-%dT%H:%M:%S%.f", date)
|
||||
date := time.format([1703444325734390000, "UTC", "2006-01-02T15:04:05.999999999"])
|
||||
res := time.parse_ns("2006-01-02T15:04:05.999999999", date)
|
||||
}
|
||||
query: data.test
|
||||
want_result:
|
||||
@@ -35,6 +35,6 @@ cases:
|
||||
- |
|
||||
package test
|
||||
|
||||
a := time.parse_ns("%Y-%m-%dT%H:%M:%S%.f", 1703444325734390000)
|
||||
a := time.parse_ns("2006-01-02T15:04:05.999999999", 1703444325734390000)
|
||||
query: data.test
|
||||
error: '`time.parse_ns` expects string argument. Got `1703444325734390000` instead'
|
||||
|
||||
Reference in New Issue
Block a user