diff --git a/enginetest/queries/queries.go b/enginetest/queries/queries.go index f3b425d7c2..9c83ba29fa 100644 --- a/enginetest/queries/queries.go +++ b/enginetest/queries/queries.go @@ -10506,59 +10506,67 @@ var VersionedScripts = []ScriptTest{ var DateParseQueries = []QueryTest{ { Query: "SELECT STR_TO_DATE('Jan 3, 2000', '%b %e, %Y')", - Expected: []sql.Row{{"2000-01-03"}}, + Expected: []sql.Row{{time.Date(2000, time.January, 3, 0, 0, 0, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('01,5,2013', '%d,%m,%Y')", - Expected: []sql.Row{{"2013-05-01"}}, + Expected: []sql.Row{{time.Date(2013, time.May, 1, 0, 0, 0, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('May 1, 2013','%M %d,%Y')", - Expected: []sql.Row{{"2013-05-01"}}, + Expected: []sql.Row{{time.Date(2013, time.May, 1, 0, 0, 0, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('a09:30:17','a%h:%i:%s')", - Expected: []sql.Row{{"09:30:17"}}, + Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}}, + }, + { + Query: "SELECT STR_TO_DATE('A09:30:17','A%h:%i:%s')", + Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('a09:30:17','%h:%i:%s')", Expected: []sql.Row{{nil}}, }, + { + Query: "SELECT STR_TO_DATE('A09:30:17','a%h:%i:%s')", + Expected: []sql.Row{{nil}}, + }, { Query: "SELECT STR_TO_DATE('09:30:17a','%h:%i:%s')", - Expected: []sql.Row{{"09:30:17"}}, + Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('09:30:17 pm','%h:%i:%s %p')", - Expected: []sql.Row{{"21:30:17"}}, + Expected: []sql.Row{{time.Date(-1, time.November, 30, 9, 30, 17, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('9','%m')", - Expected: []sql.Row{{"0000-09-00"}}, + Expected: []sql.Row{{time.Date(0, time.August, 31, 0, 0, 0, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('9','%s')", - Expected: []sql.Row{{"00:00:09"}}, + Expected: []sql.Row{{time.Date(-1, time.November, 30, 0, 0, 9, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('01/02/99 314', '%m/%e/%y %f')", - Expected: []sql.Row{{"1999-01-02 00:00:00.314000"}}, + Expected: []sql.Row{{time.Date(1999, time.January, 2, 0, 0, 0, 314000, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('01/02/99 0', '%m/%e/%y %f')", - Expected: []sql.Row{{"1999-01-02 00:00:00.000000"}}, + Expected: []sql.Row{{time.Date(1999, time.January, 2, 0, 0, 0, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('01/02/99 05:14:12 PM', '%m/%e/%y %r')", - Expected: []sql.Row{{"1999-01-02 17:14:12"}}, + Expected: []sql.Row{{time.Date(1999, time.January, 2, 5, 14, 12, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('May 3, 10:23:00 2000', '%b %e, %H:%i:%s %Y')", - Expected: []sql.Row{{"2000-05-03 10:23:00"}}, + Expected: []sql.Row{{time.Date(2000, time.May, 3, 10, 23, 0, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('May 3, 10:23:00 PM 2000', '%b %e, %h:%i:%s %p %Y')", - Expected: []sql.Row{{"2000-05-03 22:23:00"}}, + Expected: []sql.Row{{time.Date(2000, time.May, 3, 10, 23, 0, 0, time.UTC)}}, }, { Query: "SELECT STR_TO_DATE('May 3, 10:23:00 PM 2000', '%b %e, %H:%i:%s %p %Y')", // cannot use 24 hour time (%H) with AM/PM (%p) diff --git a/sql/expression/function/registry.go b/sql/expression/function/registry.go index 08583c0ace..cdbd365a46 100644 --- a/sql/expression/function/registry.go +++ b/sql/expression/function/registry.go @@ -218,7 +218,7 @@ var BuiltIns = []sql.Function{ sql.Function1{Name: "sleep", Fn: NewSleep}, sql.Function1{Name: "soundex", Fn: NewSoundex}, sql.Function1{Name: "sqrt", Fn: NewSqrt}, - sql.FunctionN{Name: "str_to_date", Fn: NewStrToDate}, + sql.Function2{Name: "str_to_date", Fn: NewStrToDate}, sql.FunctionN{Name: "subdate", Fn: NewSubDate}, sql.Function2{Name: "point", Fn: spatial.NewPoint}, sql.FunctionN{Name: "linestring", Fn: spatial.NewLineString}, diff --git a/sql/expression/function/str_to_date.go b/sql/expression/function/str_to_date.go index 6fc0bd68d0..71c8d39746 100644 --- a/sql/expression/function/str_to_date.go +++ b/sql/expression/function/str_to_date.go @@ -3,23 +3,12 @@ package function import ( "fmt" - "github.com/dolthub/go-mysql-server/sql/planbuilder/dateparse" - "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/planbuilder/dateparse" "github.com/dolthub/go-mysql-server/sql/types" ) -// NewStrToDate constructs a new function expression from the given child expressions. -func NewStrToDate(args ...sql.Expression) (sql.Expression, error) { - if len(args) != 2 { - return nil, sql.ErrInvalidArgumentNumber.New("STR_TO_DATE", 2, len(args)) - } - return &StrToDate{ - Date: args[0], - Format: args[1], - }, nil -} - // StrToDate defines the built-in function STR_TO_DATE(str, format) type StrToDate struct { Date sql.Expression @@ -29,40 +18,91 @@ type StrToDate struct { var _ sql.FunctionExpression = (*StrToDate)(nil) var _ sql.CollationCoercible = (*StrToDate)(nil) +// NewStrToDate constructs a new function expression from the given child expressions. +func NewStrToDate(arg1, arg2 sql.Expression) sql.Expression { + return &StrToDate{ + Date: arg1, + Format: arg2, + } +} + +func (s *StrToDate) FunctionName() string { + return "str_to_date" +} + // Description implements sql.FunctionExpression -func (s StrToDate) Description() string { +func (s *StrToDate) Description() string { return "parses the date/datetime/timestamp expression according to the format specifier." } // Resolved returns whether the node is resolved. -func (s StrToDate) Resolved() bool { +func (s *StrToDate) Resolved() bool { dateResolved := s.Date == nil || s.Date.Resolved() formatResolved := s.Format == nil || s.Format.Resolved() return dateResolved && formatResolved } -func (s StrToDate) String() string { +func (s *StrToDate) String() string { return fmt.Sprintf("%s(%s,%s)", s.FunctionName(), s.Date, s.Format) } // Type returns the expression type. -func (s StrToDate) Type() sql.Type { - // TODO: precision +func (s *StrToDate) Type() sql.Type { + // TODO: depending on the format, the return type can be a date, datetime or timestamp + // just make best guess for now + formatLit, isLit := s.Format.(*expression.Literal) + if !isLit { + return types.Datetime + } + format, err := formatLit.Eval(nil, nil) + if err != nil { + return types.Datetime + } + formatStr, isStr := format.(string) + if !isStr { + return types.Datetime + } + + hasDate, hasTime, err := dateparse.HasDateOrTime(formatStr) + if err != nil { + return types.Datetime + } + if hasDate && hasTime { + return types.Datetime + } + if hasDate { + return types.Date + } + if hasTime { + return types.Time + } return types.Datetime } // CollationCoercibility implements the interface sql.CollationCoercible. -func (StrToDate) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { +func (*StrToDate) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 } // IsNullable returns whether the expression can be null. -func (s StrToDate) IsNullable() bool { +func (s *StrToDate) IsNullable() bool { return true } +// Children returns the children expressions of this expression. +func (s *StrToDate) Children() []sql.Expression { + return []sql.Expression{s.Date, s.Format} +} + +func (s *StrToDate) WithChildren(children ...sql.Expression) (sql.Expression, error) { + if len(children) != 2 { + return nil, sql.ErrInvalidArgumentNumber.New("STR_TO_DATE", 2, len(children)) + } + return NewStrToDate(children[0], children[1]), nil +} + // Eval evaluates the given row and returns a result. -func (s StrToDate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { +func (s *StrToDate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { date, err := s.Date.Eval(ctx, row) if err != nil { return nil, err @@ -93,23 +133,3 @@ func (s StrToDate) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { // but depends on strict sql_mode with NO_ZERO_DATE or NO_ZERO_IN_DATE modes enabled. return goTime, nil } - -// Children returns the children expressions of this expression. -func (s StrToDate) Children() []sql.Expression { - children := make([]sql.Expression, 0, 2) - if s.Date != nil { - children = append(children, s.Date) - } - if s.Format != nil { - children = append(children, s.Format) - } - return children -} - -func (s StrToDate) WithChildren(children ...sql.Expression) (sql.Expression, error) { - return NewStrToDate(children...) -} - -func (s StrToDate) FunctionName() string { - return "str_to_date" -} diff --git a/sql/expression/function/str_to_date_test.go b/sql/expression/function/str_to_date_test.go index fffea82a30..9fe7048467 100644 --- a/sql/expression/function/str_to_date_test.go +++ b/sql/expression/function/str_to_date_test.go @@ -20,25 +20,22 @@ func TestStrToDate(t *testing.T) { fmtStr string expected interface{} }{ - {"standard", "Dec 26, 2000 2:13:15", "%b %e, %Y %T", "2000-12-26 02:13:15"}, - {"ymd", "20240101", "%Y%m%d", "2024-01-01"}, - {"ymd", "2024121", "%Y%m%d", "2024-12-01"}, + {"standard", "Dec 26, 2000 2:13:15", "%b %e, %Y %T", time.Date(2000, time.December, 26, 2, 13, 15, 0, time.UTC)}, + {"ymd", "20240101", "%Y%m%d", time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)}, + {"ymd", "2024121", "%Y%m%d", time.Date(2024, time.December, 1, 0, 0, 0, 0, time.UTC)}, {"ymd", "20241301", "%Y%m%d", nil}, {"ymd", "20240001", "%Y%m%d", nil}, - {"ymd-with-time", "2024010203:04:05", "%Y%m%d%T", "2024-01-02 03:04:05"}, - {"ymd-with-time", "202408122:03:04", "%Y%m%d%T", "2024-08-12 02:03:04"}, + {"ymd-with-time", "2024010203:04:05", "%Y%m%d%T", time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC)}, + {"ymd-with-time", "202408122:03:04", "%Y%m%d%T", time.Date(2024, time.August, 12, 2, 3, 4, 0, time.UTC)}, // TODO: It shoud be nil, but returns "2024-02-31" // {"ymd", "20240231", "%Y%m%d", nil}, } for _, tt := range testCases { - f, err := NewStrToDate( + f := NewStrToDate( expression.NewGetField(0, types.Text, "", true), expression.NewGetField(1, types.Text, "", true), ) - if err != nil { - t.Fatal(err) - } t.Run(tt.name, func(t *testing.T) { dtime := eval(t, f, sql.NewRow(tt.dateStr, tt.fmtStr)) require.Equal(t, tt.expected, dtime) @@ -60,13 +57,10 @@ func TestStrToDateFailure(t *testing.T) { } for _, tt := range testCases { - f, err := NewStrToDate( + f := NewStrToDate( expression.NewGetField(0, types.Text, "", true), expression.NewGetField(1, types.Text, "", true), ) - if err != nil { - t.Fatal(err) - } t.Run(tt.name, func(t *testing.T) { dtime := eval(t, f, sql.NewRow(tt.dateStr, tt.fmtStr)) require.Equal(t, nil, dtime) diff --git a/sql/planbuilder/dateparse/date.go b/sql/planbuilder/dateparse/date.go index 12e05cbf33..fbd6c8f4f3 100644 --- a/sql/planbuilder/dateparse/date.go +++ b/sql/planbuilder/dateparse/date.go @@ -11,6 +11,30 @@ var ( timeSpecifiers = []uint8{'f', 'H', 'h', 'I', 'i', 'k', 'l', 'p', 'r', 'S', 's', 'T'} ) +// HasDateOrTime returns whether the format string contains date or time specifiers. +func HasDateOrTime(format string) (hasDate bool, hasTime bool, err error) { + _, specifiers, err := parsersFromFormatString(format) + if err != nil { + return false, false, err + } + + for _, s := range dateSpecifiers { + if _, ok := specifiers[s]; ok { + hasDate = true + break + } + } + + for _, s := range timeSpecifiers { + if _, ok := specifiers[s]; ok { + hasTime = true + break + } + } + + return hasDate, hasTime, nil +} + // ParseDateWithFormat parses the date string according to the given // format string, as defined in the MySQL specification. // @@ -26,15 +50,11 @@ func ParseDateWithFormat(date, format string) (interface{}, error) { return nil, err } - hasDate := false for _, s := range dateSpecifiers { if _, ok := specifiers[s]; ok { - hasDate = true break } } - - hasTime := false _, hasAmPm := specifiers['p'] for _, s := range timeSpecifiers { if _, ok := specifiers[s]; ok { @@ -42,7 +62,6 @@ func ParseDateWithFormat(date, format string) (interface{}, error) { if (s == 'H' || s == 'k' || s == 'T') && hasAmPm { return nil, fmt.Errorf("cannot use 24 hour time (H) with AM/PM (p)") } - hasTime = true break } } @@ -50,9 +69,6 @@ func ParseDateWithFormat(date, format string) (interface{}, error) { // trim all leading and trailing whitespace date = strings.TrimSpace(date) - // convert to all lowercase - date = strings.ToLower(date) - var dt datetime target := date for _, parser := range parsers { @@ -64,18 +80,52 @@ func ParseDateWithFormat(date, format string) (interface{}, error) { target = rest } - var result string - if hasDate && hasTime { - result = fmt.Sprintf("%s %s", evaluateDate(dt), evaluateTime(dt)) - } else if hasTime { - result = fmt.Sprintf("%s", evaluateTime(dt)) - } else if hasDate { - result = fmt.Sprintf("%s", evaluateDate(dt)) - } else { - return nil, fmt.Errorf("no value to evaluate") + if dt.isEmpty() { + return nil, nil + } + + // TODO: depending on if it's a date or time we should return a different type + var year, month, day, hours, minutes, seconds, milliseconds, microseconds, nanoseconds int + if dt.year != nil { + year = int(*dt.year) + } + if dt.month != nil { + month = int(*dt.month) + } + if dt.day != nil { + day = int(*dt.day) + } + if dt.dayOfYear != nil { + // offset from Jan 1st by the specified number of days + dayOffsetted := time.Date(year, time.January, 0, 0, 0, 0, 0, time.Local).AddDate(0, 0, int(*dt.dayOfYear)) + month = int(dayOffsetted.Month()) + day = dayOffsetted.Day() + } + + if dt.hours != nil { + hours = int(*dt.hours) + } + if dt.minutes != nil { + minutes = int(*dt.minutes) + } + if dt.seconds != nil { + seconds = int(*dt.seconds) + } + if dt.milliseconds != nil { + milliseconds = int(*dt.milliseconds) + } + if dt.microseconds != nil { + microseconds = int(*dt.microseconds) + } + if dt.nanoseconds != nil { + nanoseconds = int(*dt.nanoseconds) } + // convert partial seconds to nanoseconds + nanosecondDuration := time.Microsecond*time.Duration(microseconds) + + time.Millisecond*time.Duration(milliseconds) + + time.Nanosecond*time.Duration(nanoseconds) - return result, nil + return time.Date(year, time.Month(month), day, hours, minutes, seconds, int(nanosecondDuration), time.UTC), nil } // Convert the user-defined format string into a slice of parser functions @@ -272,7 +322,7 @@ func boolPtr(a bool) *bool { return &a } // Convert a week abbreviation to a defined weekday. func weekdayAbbrev(abbrev string) (time.Weekday, bool) { - switch abbrev { + switch strings.ToLower(abbrev) { case "sun": return time.Sunday, true case "mon": @@ -293,7 +343,7 @@ func weekdayAbbrev(abbrev string) (time.Weekday, bool) { // Convert a month abbreviation to a defined month. func monthAbbrev(abbrev string) (time.Month, bool) { - switch abbrev { + switch strings.ToLower(abbrev) { case "jan": return time.January, true case "feb": @@ -323,11 +373,11 @@ func monthAbbrev(abbrev string) (time.Month, bool) { } // TODO: allow this to match partial months -// janu should match janurary +// janu should match january func monthName(name string) (month time.Month, charCount int, ok bool) { for i := 1; i < 13; i++ { m := time.Month(i) - if strings.HasPrefix(name, strings.ToLower(m.String())) { + if strings.HasPrefix(strings.ToLower(name), strings.ToLower(m.String())) { return m, len(m.String()), true } } diff --git a/sql/planbuilder/dateparse/date_test.go b/sql/planbuilder/dateparse/date_test.go index bb9e12af10..ca25067cd8 100644 --- a/sql/planbuilder/dateparse/date_test.go +++ b/sql/planbuilder/dateparse/date_test.go @@ -15,48 +15,48 @@ func TestParseDate(t *testing.T) { name string date string format string - expected string + expected interface{} }{ - {"simple", "Jan 3, 2000", "%b %e, %Y", "2000-01-03"}, - {"simple_with_spaces", "Nov 03 , 2000", "%b %e, %Y", "2000-11-03"}, - {"simple_with_spaces_2", "Dec 15 , 2000", "%b %e, %Y", "2000-12-15"}, - {"reverse", "2023/Feb/ 1", "%Y/%b/%e", "2023-02-01"}, - {"reverse_with_spaces", " 2023 /Apr/ 01 ", "%Y/%b/%e", "2023-04-01"}, - {"weekday", "Thu, Aug 5, 2021", "%a, %b %e, %Y", "2021-08-05"}, - {"weekday", "Fri, Aug 6, 2021", "%a, %b %e, %Y", "2021-08-06"}, - {"weekday", "Sat, Aug 7, 2021", "%a, %b %e, %Y", "2021-08-07"}, - {"weekday", "Sun, Aug 8, 2021", "%a, %b %e, %Y", "2021-08-08"}, - {"weekday", "Mon, Aug 9, 2021", "%a, %b %e, %Y", "2021-08-09"}, - {"weekday", "Tue, Aug 10, 2021", "%a, %b %e, %Y", "2021-08-10"}, - {"weekday", "Wed, Aug 11, 2021", "%a, %b %e, %Y", "2021-08-11"}, - - {"time_only", "22:23:00", "%H:%i:%s", "22:23:00"}, - {"with_time", "Sep 3, 22:23:00 2000", "%b %e, %H:%i:%s %Y", "2000-09-03 22:23:00"}, - {"with_pm", "May 3, 10:23:00 PM 2000", "%b %e, %h:%i:%s %p %Y", "2000-05-03 22:23:00"}, - {"lowercase_pm", "Jul 3, 10:23:00 pm 2000", "%b %e, %h:%i:%s %p %Y", "2000-07-03 22:23:00"}, - {"with_am", "Mar 3, 10:23:00 am 2000", "%b %e, %h:%i:%s %p %Y", "2000-03-03 10:23:00"}, - - {"month_number", "1 3, 10:23:00 pm 2000", "%c %e, %h:%i:%s %p %Y", "2000-01-03 22:23:00"}, - - {"day_with_suffix", "Jun 3rd, 10:23:00 pm 2000", "%b %D, %h:%i:%s %p %Y", "2000-06-03 22:23:00"}, - {"day_with_suffix_2", "Oct 21st, 10:23:00 pm 2000", "%b %D, %h:%i:%s %p %Y", "2000-10-21 22:23:00"}, - {"with_timestamp", "01/02/2003, 12:13:14", "%c/%d/%Y, %T", "2003-01-02 12:13:14"}, - - {"month_number", "03: 3, 20", "%m: %e, %y", "2020-03-03"}, - {"month_name", "march: 3, 20", "%M: %e, %y", "2020-03-03"}, - {"two_digit_date", "january: 3, 20", "%M: %e, %y", "2020-01-03"}, - {"two_digit_date_2000", "september: 3, 70", "%M: %e, %y", "1970-09-03"}, - {"two_digit_date_1900", "may: 3, 69", "%M: %e, %y", "2069-05-03"}, - - {"microseconds", "01/02/99 314", "%m/%e/%y %f", "1999-01-02 00:00:00.314000"}, - {"hour_number", "01/02/99 5:14", "%m/%e/%y %h:%i", "1999-01-02 05:14:00"}, - {"hour_number_2", "01/02/99 5:14", "%m/%e/%y %I:%i", "1999-01-02 05:14:00"}, - - {"timestamp", "01/02/99 05:14:12 PM", "%m/%e/%y %r", "1999-01-02 17:14:12"}, - {"date_with_seconds", "01/02/99 57", "%m/%e/%y %S", "1999-01-02 00:00:57"}, - - {"date_by_year_offset", "100 20", "%j %y", "2020-04-09"}, - {"date_by_year_offset_singledigit_year", "100 5", "%j %y", "2005-04-10"}, + {"simple", "Jan 3, 2000", "%b %e, %Y", time.Date(2000, time.January, 3, 0, 0, 0, 0, time.UTC)}, + {"simple_with_spaces", "Nov 03 , 2000", "%b %e, %Y", time.Date(2000, time.November, 3, 0, 0, 0, 0, time.UTC)}, + {"simple_with_spaces_2", "Dec 15 , 2000", "%b %e, %Y", time.Date(2000, time.December, 15, 0, 0, 0, 0, time.UTC)}, + {"reverse", "2023/Feb/ 1", "%Y/%b/%e", time.Date(2023, time.February, 1, 0, 0, 0, 0, time.UTC)}, + {"reverse_with_spaces", " 2023 /Apr/ 01 ", "%Y/%b/%e", time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC)}, + {"weekday", "Thu, Aug 5, 2021", "%a, %b %e, %Y", time.Date(2021, time.August, 5, 0, 0, 0, 0, time.UTC)}, + {"weekday", "Fri, Aug 6, 2021", "%a, %b %e, %Y", time.Date(2021, time.August, 6, 0, 0, 0, 0, time.UTC)}, + {"weekday", "Sat, Aug 7, 2021", "%a, %b %e, %Y", time.Date(2021, time.August, 7, 0, 0, 0, 0, time.UTC)}, + {"weekday", "Sun, Aug 8, 2021", "%a, %b %e, %Y", time.Date(2021, time.August, 8, 0, 0, 0, 0, time.UTC)}, + {"weekday", "Mon, Aug 9, 2021", "%a, %b %e, %Y", time.Date(2021, time.August, 9, 0, 0, 0, 0, time.UTC)}, + {"weekday", "Tue, Aug 10, 2021", "%a, %b %e, %Y", time.Date(2021, time.August, 10, 0, 0, 0, 0, time.UTC)}, + {"weekday", "Wed, Aug 11, 2021", "%a, %b %e, %Y", time.Date(2021, time.August, 11, 0, 0, 0, 0, time.UTC)}, + + {"time_only", "22:23:00", "%H:%i:%s", time.Date(-1, time.November, 30, 22, 23, 0, 0, time.UTC)}, + {"with_time", "Sep 3, 22:23:00 2000", "%b %e, %H:%i:%s %Y", time.Date(2000, time.September, 3, 22, 23, 0, 0, time.UTC)}, + {"with_pm", "May 3, 10:23:00 PM 2000", "%b %e, %h:%i:%s %p %Y", time.Date(2000, time.May, 3, 10, 23, 0, 0, time.UTC)}, + {"lowercase_pm", "Jul 3, 10:23:00 pm 2000", "%b %e, %h:%i:%s %p %Y", time.Date(2000, time.July, 3, 10, 23, 0, 0, time.UTC)}, + {"with_am", "Mar 3, 10:23:00 am 2000", "%b %e, %h:%i:%s %p %Y", time.Date(2000, time.March, 3, 10, 23, 0, 0, time.UTC)}, + + {"month_number", "1 3, 10:23:00 pm 2000", "%c %e, %h:%i:%s %p %Y", time.Date(2000, time.January, 3, 10, 23, 0, 0, time.UTC)}, + + {"day_with_suffix", "Jun 3rd, 10:23:00 pm 2000", "%b %D, %h:%i:%s %p %Y", time.Date(2000, time.June, 3, 10, 23, 0, 0, time.UTC)}, + {"day_with_suffix_2", "Oct 21st, 10:23:00 pm 2000", "%b %D, %h:%i:%s %p %Y", time.Date(2000, time.October, 21, 10, 23, 0, 0, time.UTC)}, + {"with_timestamp", "01/02/2003, 12:13:14", "%c/%d/%Y, %T", time.Date(2003, time.January, 2, 12, 13, 14, 0, time.UTC)}, + + {"month_number", "03: 3, 20", "%m: %e, %y", time.Date(2020, time.March, 3, 0, 0, 0, 0, time.UTC)}, + {"month_name", "march: 3, 20", "%M: %e, %y", time.Date(2020, time.March, 3, 0, 0, 0, 0, time.UTC)}, + {"two_digit_date", "january: 3, 20", "%M: %e, %y", time.Date(2020, time.January, 3, 0, 0, 0, 0, time.UTC)}, + {"two_digit_date_2000", "september: 3, 70", "%M: %e, %y", time.Date(1970, time.September, 3, 0, 0, 0, 0, time.UTC)}, + {"two_digit_date_1900", "may: 3, 69", "%M: %e, %y", time.Date(2069, time.May, 3, 0, 0, 0, 0, time.UTC)}, + + {"microseconds", "01/02/99 314", "%m/%e/%y %f", time.Date(1999, time.January, 2, 0, 0, 0, 314000, time.UTC)}, + {"hour_number", "01/02/99 5:14", "%m/%e/%y %h:%i", time.Date(1999, time.January, 2, 5, 14, 0, 0, time.UTC)}, + {"hour_number_2", "01/02/99 5:14", "%m/%e/%y %I:%i", time.Date(1999, time.January, 2, 5, 14, 0, 0, time.UTC)}, + + {"timestamp", "01/02/99 05:14:12 PM", "%m/%e/%y %r", time.Date(1999, time.January, 2, 5, 14, 12, 0, time.UTC)}, + {"date_with_seconds", "01/02/99 57", "%m/%e/%y %S", time.Date(1999, time.January, 2, 0, 0, 57, 0, time.UTC)}, + + {"date_by_year_offset", "100 20", "%j %y", time.Date(2020, time.April, 9, 0, 0, 0, 0, time.UTC)}, + {"date_by_year_offset_singledigit_year", "100 5", "%j %y", time.Date(2005, time.April, 10, 0, 0, 0, 0, time.UTC)}, } for _, tt := range tests { @@ -87,9 +87,9 @@ func TestConversionFailure(t *testing.T) { expectedError string }{ // with strict mode with NO_ZERO_IN_DATE,NO_ZERO_DATE enabled, these tests result NULL - {"no_year", "Jan 3", "%b %e", "0000-01-03", ""}, - {"no_day", "Jan 2000", "%b %y", "2020-01-00", ""}, - {"day_of_month_and_day_of_year", "Jan 3, 100 2000", "%b %e, %j %y", "2020-04-09", ""}, + {"no_year", "Jan 3", "%b %e", time.Date(0, time.January, 3, 0, 0, 0, 0, time.UTC), ""}, + {"no_day", "Jan 2000", "%b %Y", time.Date(2000, time.January, 0, 0, 0, 0, 0, time.UTC), ""}, + {"day_of_month_and_day_of_year", "Jan 3, 100 2000", "%b %e, %j %Y", time.Date(2000, time.April, 9, 0, 0, 0, 0, time.UTC), ""}, {"24hour_time_with_pm", "May 3, 10:23:00 PM 2000", "%b %e, %H:%i:%s %p %Y", nil, "cannot use 24 hour time (H) with AM/PM (p)"}, {"specifier_end_of_line", "Jan 3", "%b %e %", nil, `"%" found at end of format string`}, @@ -125,10 +125,10 @@ func TestParseErr(t *testing.T) { Specifier: 'e', Tokens: "abc", err: fmt.Errorf("strconv.ParseUint: parsing \"\": invalid syntax")}, }, {"bad_month", "1 Jen, 2000", "%e %b, %Y", ParseSpecifierErr{ - Specifier: 'b', Tokens: "jen, 2000", err: fmt.Errorf(`invalid month abbreviation "jen"`)}, + Specifier: 'b', Tokens: "Jen, 2000", err: fmt.Errorf(`invalid month abbreviation "Jen"`)}, }, {"bad_weekday", "Ten 1 Jan, 2000", "%a %e %b, %Y", ParseSpecifierErr{ - Specifier: 'a', Tokens: "ten 1 jan, 2000", err: fmt.Errorf(`invalid week abbreviation "ten"`)}, + Specifier: 'a', Tokens: "Ten 1 Jan, 2000", err: fmt.Errorf(`invalid week abbreviation "Ten"`)}, }, } diff --git a/sql/planbuilder/dateparse/eval.go b/sql/planbuilder/dateparse/eval.go deleted file mode 100644 index f316438c1b..0000000000 --- a/sql/planbuilder/dateparse/eval.go +++ /dev/null @@ -1,84 +0,0 @@ -package dateparse - -import ( - "fmt" - "time" -) - -func evaluateDate(dt datetime) string { - var year, month, day int - - if dt.year != nil { - year = int(*dt.year) - } - - if dt.month != nil { - month = int(*dt.month) - } - - if dt.day != nil { - day = int(*dt.day) - } - - if dt.dayOfYear != nil { - // offset from Jan 1st by the specified number of days - dayOffsetted := time.Date(year, time.January, 0, 0, 0, 0, 0, time.Local).AddDate(0, 0, int(*dt.dayOfYear)) - month = int(dayOffsetted.Month()) - day = dayOffsetted.Day() - } - - return fillWithZero(year, 4) + "-" + fillWithZero(month, 2) + "-" + fillWithZero(day, 2) -} - -func evaluateTime(dt datetime) string { - var hours, minutes, seconds, milliseconds, microseconds, nanoseconds int - - if dt.hours != nil { - if *dt.hours < 13 && dt.am != nil && !*dt.am { - *dt.hours += 12 - } - hours = int(*dt.hours) - } - if dt.minutes != nil { - minutes = int(*dt.minutes) - } - if dt.seconds != nil { - seconds = int(*dt.seconds) - } - - t := fillWithZero(hours, 2) + ":" + fillWithZero(minutes, 2) + ":" + fillWithZero(seconds, 2) - - includeMicrosecond := false - if dt.milliseconds != nil { - milliseconds = int(*dt.milliseconds) - includeMicrosecond = true - } - if dt.microseconds != nil { - microseconds = int(*dt.microseconds) - includeMicrosecond = true - } - if dt.nanoseconds != nil { - nanoseconds = int(*dt.nanoseconds) - includeMicrosecond = true - } - - // convert partial seconds to nanoseconds - nanosecondDuration := time.Microsecond*time.Duration(microseconds) + time.Millisecond*time.Duration(milliseconds) + time.Nanosecond*time.Duration(nanoseconds) - if includeMicrosecond { - t = t + "." + fillWithZero(int(nanosecondDuration), 6) - } - - return t -} - -func fillWithZero(n int, length int) string { - r := fmt.Sprintf("%d", n) - if len(r) > length { - r = "" - } - for len(r) < length { - r = fmt.Sprintf("0%s", r) - } - - return r -} diff --git a/sql/planbuilder/dateparse/parsers.go b/sql/planbuilder/dateparse/parsers.go index 756c686ac7..0d0abc049c 100644 --- a/sql/planbuilder/dateparse/parsers.go +++ b/sql/planbuilder/dateparse/parsers.go @@ -2,6 +2,7 @@ package dateparse import ( "fmt" + "strings" "time" ) @@ -39,7 +40,7 @@ func parseAmPm(result *datetime, chars string) (rest string, _ error) { if len(chars) < 2 { return "", fmt.Errorf("expected > 2 chars, found %d", len(chars)) } - switch chars[:2] { + switch strings.ToLower(chars[:2]) { case "am": result.am = boolPtr(true) case "pm":