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:
Burak
2024-02-08 00:12:34 +01:00
committed by GitHub
parent dda525b989
commit 5717f9c249
6 changed files with 1042 additions and 20 deletions

View File

@@ -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, &params[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, &params[0], &args[0])?;
let value = ensure_string(name, &params[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,
}
}

View File

@@ -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";
}

View File

@@ -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(),
)
}

View File

@@ -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:

View File

@@ -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"

View File

@@ -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'