diff --git a/expression/builtin_time_test.go b/expression/builtin_time_test.go index 4015794377486..e87f1f24875e0 100644 --- a/expression/builtin_time_test.go +++ b/expression/builtin_time_test.go @@ -1392,41 +1392,52 @@ func (s *testEvaluatorSuite) TestUTCDate(c *C) { } func (s *testEvaluatorSuite) TestStrToDate(c *C) { + // If you want to add test cases for `strToDate` but not the builtin function, + // adding cases in `types.format_test.go` `TestStrToDate` maybe more clear and easier tests := []struct { Date string Format string Success bool + Kind byte Expect time.Time }{ - {"10/28/2011 9:46:29 pm", "%m/%d/%Y %l:%i:%s %p", true, time.Date(2011, 10, 28, 21, 46, 29, 0, time.Local)}, - {"10/28/2011 9:46:29 Pm", "%m/%d/%Y %l:%i:%s %p", true, time.Date(2011, 10, 28, 21, 46, 29, 0, time.Local)}, - {"2011/10/28 9:46:29 am", "%Y/%m/%d %l:%i:%s %p", true, time.Date(2011, 10, 28, 9, 46, 29, 0, time.Local)}, - {"20161122165022", `%Y%m%d%H%i%s`, true, time.Date(2016, 11, 22, 16, 50, 22, 0, time.Local)}, - {"2016 11 22 16 50 22", `%Y%m%d%H%i%s`, true, time.Date(2016, 11, 22, 16, 50, 22, 0, time.Local)}, - {"16-50-22 2016 11 22", `%H-%i-%s%Y%m%d`, true, time.Date(2016, 11, 22, 16, 50, 22, 0, time.Local)}, - {"16-50 2016 11 22", `%H-%i-%s%Y%m%d`, false, time.Time{}}, - {"15-01-2001 1:59:58.999", "%d-%m-%Y %I:%i:%s.%f", true, time.Date(2001, 1, 15, 1, 59, 58, 999000000, time.Local)}, - {"15-01-2001 1:59:58.1", "%d-%m-%Y %H:%i:%s.%f", true, time.Date(2001, 1, 15, 1, 59, 58, 100000000, time.Local)}, - {"15-01-2001 1:59:58.", "%d-%m-%Y %H:%i:%s.%f", true, time.Date(2001, 1, 15, 1, 59, 58, 000000000, time.Local)}, - {"15-01-2001 1:9:8.999", "%d-%m-%Y %H:%i:%s.%f", true, time.Date(2001, 1, 15, 1, 9, 8, 999000000, time.Local)}, - {"15-01-2001 1:9:8.999", "%d-%m-%Y %H:%i:%S.%f", true, time.Date(2001, 1, 15, 1, 9, 8, 999000000, time.Local)}, - {"2003-01-02 10:11:12 PM", "%Y-%m-%d %H:%i:%S %p", false, time.Time{}}, - {"10:20:10AM", "%H:%i:%S%p", false, time.Time{}}, + {"10/28/2011 9:46:29 pm", "%m/%d/%Y %l:%i:%s %p", true, types.KindMysqlTime, time.Date(2011, 10, 28, 21, 46, 29, 0, time.Local)}, + {"10/28/2011 9:46:29 Pm", "%m/%d/%Y %l:%i:%s %p", true, types.KindMysqlTime, time.Date(2011, 10, 28, 21, 46, 29, 0, time.Local)}, + {"2011/10/28 9:46:29 am", "%Y/%m/%d %l:%i:%s %p", true, types.KindMysqlTime, time.Date(2011, 10, 28, 9, 46, 29, 0, time.Local)}, + {"20161122165022", `%Y%m%d%H%i%s`, true, types.KindMysqlTime, time.Date(2016, 11, 22, 16, 50, 22, 0, time.Local)}, + {"2016 11 22 16 50 22", `%Y%m%d%H%i%s`, true, types.KindMysqlTime, time.Date(2016, 11, 22, 16, 50, 22, 0, time.Local)}, + {"16-50-22 2016 11 22", `%H-%i-%s%Y%m%d`, true, types.KindMysqlTime, time.Date(2016, 11, 22, 16, 50, 22, 0, time.Local)}, + {"16-50 2016 11 22", `%H-%i-%s%Y%m%d`, false, types.KindMysqlTime, time.Time{}}, + {"15-01-2001 1:59:58.999", "%d-%m-%Y %I:%i:%s.%f", true, types.KindMysqlTime, time.Date(2001, 1, 15, 1, 59, 58, 999000000, time.Local)}, + {"15-01-2001 1:59:58.1", "%d-%m-%Y %H:%i:%s.%f", true, types.KindMysqlTime, time.Date(2001, 1, 15, 1, 59, 58, 100000000, time.Local)}, + {"15-01-2001 1:59:58.", "%d-%m-%Y %H:%i:%s.%f", true, types.KindMysqlTime, time.Date(2001, 1, 15, 1, 59, 58, 000000000, time.Local)}, + {"15-01-2001 1:9:8.999", "%d-%m-%Y %H:%i:%s.%f", true, types.KindMysqlTime, time.Date(2001, 1, 15, 1, 9, 8, 999000000, time.Local)}, + {"15-01-2001 1:9:8.999", "%d-%m-%Y %H:%i:%S.%f", true, types.KindMysqlTime, time.Date(2001, 1, 15, 1, 9, 8, 999000000, time.Local)}, + {"2003-01-02 10:11:12 PM", "%Y-%m-%d %H:%i:%S %p", false, types.KindMysqlTime, time.Time{}}, + {"10:20:10AM", "%H:%i:%S%p", false, types.KindMysqlTime, time.Time{}}, // test %@(skip alpha), %#(skip number), %.(skip punct) - {"2020-10-10ABCD", "%Y-%m-%d%@", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, - {"2020-10-101234", "%Y-%m-%d%#", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, - {"2020-10-10....", "%Y-%m-%d%.", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, - {"2020-10-10.1", "%Y-%m-%d%.%#%@", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, - {"abcd2020-10-10.1", "%@%Y-%m-%d%.%#%@", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, - {"abcd-2020-10-10.1", "%@-%Y-%m-%d%.%#%@", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, - {"2020-10-10", "%Y-%m-%d%@", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, - {"2020-10-10abcde123abcdef", "%Y-%m-%d%@%#", true, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"2020-10-10ABCD", "%Y-%m-%d%@", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"2020-10-101234", "%Y-%m-%d%#", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"2020-10-10....", "%Y-%m-%d%.", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"2020-10-10.1", "%Y-%m-%d%.%#%@", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"abcd2020-10-10.1", "%@%Y-%m-%d%.%#%@", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"abcd-2020-10-10.1", "%@-%Y-%m-%d%.%#%@", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"2020-10-10", "%Y-%m-%d%@", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + {"2020-10-10abcde123abcdef", "%Y-%m-%d%@%#", true, types.KindMysqlTime, time.Date(2020, 10, 10, 0, 0, 0, 0, time.Local)}, + // some input for '%r' + {"12:3:56pm 13/05/2019", "%r %d/%c/%Y", true, types.KindMysqlTime, time.Date(2019, 5, 13, 12, 3, 56, 0, time.Local)}, + {"11:13:56 am", "%r", true, types.KindMysqlDuration, time.Date(0, 0, 0, 11, 13, 56, 0, time.Local)}, + // some input for '%T' + {"12:13:56 13/05/2019", "%T %d/%c/%Y", true, types.KindMysqlTime, time.Date(2019, 5, 13, 12, 13, 56, 0, time.Local)}, + {"19:3:56 13/05/2019", "%T %d/%c/%Y", true, types.KindMysqlTime, time.Date(2019, 5, 13, 19, 3, 56, 0, time.Local)}, + {"21:13:24", "%T", true, types.KindMysqlDuration, time.Date(0, 0, 0, 21, 13, 24, 0, time.Local)}, } fc := funcs[ast.StrToDate] for _, test := range tests { date := types.NewStringDatum(test.Date) format := types.NewStringDatum(test.Format) + c.Logf("input: %s, format: %s", test.Date, test.Format) f, err := fc.getFunction(s.ctx, s.datumsToConstants([]types.Datum{date, format})) c.Assert(err, IsNil) result, err := evalBuiltinFunc(f, chunk.Row{}) @@ -1436,10 +1447,17 @@ func (s *testEvaluatorSuite) TestStrToDate(c *C) { c.Assert(result.IsNull(), IsTrue) continue } - c.Assert(result.Kind(), Equals, types.KindMysqlTime) - value := result.GetMysqlTime() - t1, _ := value.GoTime(time.Local) - c.Assert(t1, Equals, test.Expect) + c.Assert(result.Kind(), Equals, test.Kind) + switch test.Kind { + case types.KindMysqlTime: + value := result.GetMysqlTime() + t1, _ := value.GoTime(time.Local) + c.Assert(t1, Equals, test.Expect) + case types.KindMysqlDuration: + value := result.GetMysqlDuration() + timeExpect := test.Expect.Sub(time.Date(0, 0, 0, 0, 0, 0, 0, time.Local)) + c.Assert(value.Duration, Equals, timeExpect) + } } } diff --git a/types/format_test.go b/types/format_test.go index 7dac7bd45105f..c7d328a07805a 100644 --- a/types/format_test.go +++ b/types/format_test.go @@ -118,20 +118,55 @@ func (s *testTimeSuite) TestStrToDate(c *C) { {`70/10/22`, `%Y/%m/%d`, types.FromDate(1970, 10, 22, 0, 0, 0, 0)}, {`18/10/22`, `%Y/%m/%d`, types.FromDate(2018, 10, 22, 0, 0, 0, 0)}, {`100/10/22`, `%Y/%m/%d`, types.FromDate(100, 10, 22, 0, 0, 0, 0)}, + //'%b'/'%M' should be case insensitive + {"31/may/2016 12:34:56.1234", "%d/%b/%Y %H:%i:%S.%f", types.FromDate(2016, 5, 31, 12, 34, 56, 123400)}, + {"30/april/2016 12:34:56.", "%d/%M/%Y %H:%i:%s.%f", types.FromDate(2016, 4, 30, 12, 34, 56, 0)}, + {"31/mAy/2016 12:34:56.1234", "%d/%b/%Y %H:%i:%S.%f", types.FromDate(2016, 5, 31, 12, 34, 56, 123400)}, + {"30/apRil/2016 12:34:56.", "%d/%M/%Y %H:%i:%s.%f", types.FromDate(2016, 4, 30, 12, 34, 56, 0)}, + // '%r' + {" 04 :13:56 AM13/05/2019", "%r %d/%c/%Y", types.FromDate(2019, 5, 13, 4, 13, 56, 0)}, // + {"12: 13:56 AM 13/05/2019", "%r%d/%c/%Y", types.FromDate(2019, 5, 13, 0, 13, 56, 0)}, // + {"12:13 :56 pm 13/05/2019", "%r %d/%c/%Y", types.FromDate(2019, 5, 13, 12, 13, 56, 0)}, // + {"12:3: 56pm 13/05/2019", "%r %d/%c/%Y", types.FromDate(2019, 5, 13, 12, 3, 56, 0)}, // + {"11:13:56", "%r", types.FromDate(0, 0, 0, 11, 13, 56, 0)}, // EOF before parsing "AM"/"PM" + {"11:13", "%r", types.FromDate(0, 0, 0, 11, 13, 0, 0)}, // EOF after hh:mm + {"11:", "%r", types.FromDate(0, 0, 0, 11, 0, 0, 0)}, // EOF after hh: + {"11", "%r", types.FromDate(0, 0, 0, 11, 0, 0, 0)}, // EOF after hh: + {"12", "%r", types.FromDate(0, 0, 0, 0, 0, 0, 0)}, // EOF after hh:, and hh=12 -> 0 + // '%T' + {" 4 :13:56 13/05/2019", "%T %d/%c/%Y", types.FromDate(2019, 5, 13, 4, 13, 56, 0)}, + {"23: 13:56 13/05/2019", "%T%d/%c/%Y", types.FromDate(2019, 5, 13, 23, 13, 56, 0)}, + {"12:13 :56 13/05/2019", "%T %d/%c/%Y", types.FromDate(2019, 5, 13, 12, 13, 56, 0)}, + {"19:3: 56 13/05/2019", "%T %d/%c/%Y", types.FromDate(2019, 5, 13, 19, 3, 56, 0)}, + {"21:13", "%T", types.FromDate(0, 0, 0, 21, 13, 0, 0)}, // EOF after hh:mm + {"21:", "%T", types.FromDate(0, 0, 0, 21, 0, 0, 0)}, // EOF after hh: + // More patterns than input string + {" 2/Jun", "%d/%b/%Y", types.FromDate(0, 6, 2, 0, 0, 0, 0)}, + {" liter", "lit era l", types.ZeroCoreTime}, + // Feb 29 in leap-year + {"29/Feb/2020 12:34:56.", "%d/%b/%Y %H:%i:%s.%f", types.FromDate(2020, 2, 29, 12, 34, 56, 0)}, + // When `AllowInvalidDate` is true, check only that the month is in the range from 1 to 12 and the day is in the range from 1 to 31 + {"31/April/2016 12:34:56.", "%d/%M/%Y %H:%i:%s.%f", types.FromDate(2016, 4, 31, 12, 34, 56, 0)}, // April 31th + {"29/Feb/2021 12:34:56.", "%d/%b/%Y %H:%i:%s.%f", types.FromDate(2021, 2, 29, 12, 34, 56, 0)}, // Feb 29 in non-leap-year + {"30/Feb/2016 12:34:56.1234", "%d/%b/%Y %H:%i:%S.%f", types.FromDate(2016, 2, 30, 12, 34, 56, 123400)}, // Feb 30th } for i, tt := range tests { + sc.AllowInvalidDate = true var t types.Time - c.Assert(t.StrToDate(sc, tt.input, tt.format), IsTrue, Commentf("no.%d failed", i)) - c.Assert(t.CoreTime(), Equals, tt.expect, Commentf("no.%d failed", i)) + c.Assert(t.StrToDate(sc, tt.input, tt.format), IsTrue, Commentf("no.%d failed input=%s format=%s", i, tt.input, tt.format)) + c.Assert(t.CoreTime(), Equals, tt.expect, Commentf("no.%d failed input=%s format=%s", i, tt.input, tt.format)) } errTests := []struct { input string format string }{ - {`04/31/2004`, `%m/%d/%Y`}, + // invalid days when `AllowInvalidDate` is false + {`04/31/2004`, `%m/%d/%Y`}, // not exists in the real world + {"29/Feb/2021 12:34:56.", "%d/%b/%Y %H:%i:%s.%f"}, // Feb 29 in non-leap-year + {`a09:30:17`, `%h:%i:%s`}, // format mismatch - {`12:43:24`, `%r`}, // no PM or AM followed + {`12:43:24 a`, `%r`}, // followed by incomplete 'AM'/'PM' {`23:60:12`, `%T`}, // invalid minute {`18`, `%l`}, {`00:21:22 AM`, `%h:%i:%s %p`}, @@ -139,9 +174,18 @@ func (s *testTimeSuite) TestStrToDate(c *C) { {"2010-11-12 11 am", `%Y-%m-%d %H %p`}, {"2010-11-12 13 am", `%Y-%m-%d %h %p`}, {"2010-11-12 0 am", `%Y-%m-%d %h %p`}, + // MySQL accept `SEPTEMB` as `SEPTEMBER`, but we don't want this "feature" in TiDB + // unless we have to. + {"15 SEPTEMB 2001", "%d %M %Y"}, + // '%r' + {"13:13:56 AM13/5/2019", "%r"}, // hh = 13 with am is invalid + {"00:13:56 AM13/05/2019", "%r"}, // hh = 0 with am is invalid + {"00:13:56 pM13/05/2019", "%r"}, // hh = 0 with pm is invalid + {"11:13:56a", "%r"}, // EOF while parsing "AM"/"PM" } for i, tt := range errTests { + sc.AllowInvalidDate = false var t types.Time - c.Assert(t.StrToDate(sc, tt.input, tt.format), IsFalse, Commentf("no.%d failed", i)) + c.Assert(t.StrToDate(sc, tt.input, tt.format), IsFalse, Commentf("no.%d failed input=%s format=%s", i, tt.input, tt.format)) } } diff --git a/types/time.go b/types/time.go index 28a34b0284833..5d92fc0cd9b5b 100644 --- a/types/time.go +++ b/types/time.go @@ -2849,18 +2849,18 @@ func skipWhiteSpace(input string) string { } var monthAbbrev = map[string]gotime.Month{ - "Jan": gotime.January, - "Feb": gotime.February, - "Mar": gotime.March, - "Apr": gotime.April, - "May": gotime.May, - "Jun": gotime.June, - "Jul": gotime.July, - "Aug": gotime.August, - "Sep": gotime.September, - "Oct": gotime.October, - "Nov": gotime.November, - "Dec": gotime.December, + "jan": gotime.January, + "feb": gotime.February, + "mar": gotime.March, + "apr": gotime.April, + "may": gotime.May, + "jun": gotime.June, + "jul": gotime.July, + "aug": gotime.August, + "sep": gotime.September, + "oct": gotime.October, + "nov": gotime.November, + "dec": gotime.December, } type dateFormatParser func(t *CoreTime, date string, ctx map[string]int) (remain string, succ bool) @@ -2980,76 +2980,150 @@ func minutesNumeric(t *CoreTime, input string, ctx map[string]int) (string, bool return input[length:], true } -const time12HourLen = len("hh:mm:ssAM") +type parseState int32 -func time12Hour(t *CoreTime, input string, ctx map[string]int) (string, bool) { - // hh:mm:ss AM - if len(input) < time12HourLen { - return input, false +const ( + parseStateNormal parseState = 1 + parseStateFail parseState = 2 + parseStateEndOfLine parseState = 3 +) + +func parseSep(input string) (string, parseState) { + input = skipWhiteSpace(input) + if len(input) == 0 { + return input, parseStateEndOfLine } - hour, succ := parseDigits(input, 2) - if !succ || hour > 12 || hour == 0 || input[2] != ':' { - return input, false + if input[0] != ':' { + return input, parseStateFail } - // 12:34:56 AM -> 00:34:56 - if hour == 12 { - hour = 0 + if input = skipWhiteSpace(input[1:]); len(input) == 0 { + return input, parseStateEndOfLine } + return input, parseStateNormal +} - minute, succ := parseDigits(input[3:], 2) - if !succ || minute > 59 || input[5] != ':' { - return input, false - } +func time12Hour(t *CoreTime, input string, ctx map[string]int) (string, bool) { + tryParse := func(input string) (string, parseState) { + state := parseStateNormal + // hh:mm:ss AM + /// Note that we should update `t` as soon as possible, or we + /// can not get correct result for incomplete input like "12:13" + /// that is shorter than "hh:mm:ss" + result := oneOrTwoDigitRegex.FindString(input) // 1..12 + length := len(result) + hour, succ := parseDigits(input, length) + if !succ || hour > 12 || hour == 0 { + return input, parseStateFail + } + // Handle special case: 12:34:56 AM -> 00:34:56 + // For PM, we will add 12 it later + if hour == 12 { + hour = 0 + } + t.setHour(uint8(hour)) - second, succ := parseDigits(input[6:], 2) - if !succ || second > 59 { - return input, false + // ':' + if input, state = parseSep(input[length:]); state != parseStateNormal { + return input, state + } + + result = oneOrTwoDigitRegex.FindString(input) // 0..59 + length = len(result) + minute, succ := parseDigits(input, length) + if !succ || minute > 59 { + return input, parseStateFail + } + t.setMinute(uint8(minute)) + + // ':' + if input, state = parseSep(input[length:]); state != parseStateNormal { + return input, state + } + + result = oneOrTwoDigitRegex.FindString(input) // 0..59 + length = len(result) + second, succ := parseDigits(input, length) + if !succ || second > 59 { + return input, parseStateFail + } + t.setSecond(uint8(second)) + + input = skipWhiteSpace(input[length:]) + if len(input) == 0 { + // No "AM"/"PM" suffix, it is ok + return input, parseStateEndOfLine + } else if len(input) < 2 { + // some broken char, fail + return input, parseStateFail + } + + switch { + case hasCaseInsensitivePrefix(input, "AM"): + t.setHour(uint8(hour)) + case hasCaseInsensitivePrefix(input, "PM"): + t.setHour(uint8(hour + 12)) + default: + return input, parseStateFail + } + + return input[2:], parseStateNormal } - remain := skipWhiteSpace(input[8:]) - switch { - case strings.HasPrefix(remain, "AM"): - t.setHour(uint8(hour)) - remain = strings.TrimPrefix(remain, "AM") - case strings.HasPrefix(remain, "PM"): - t.setHour(uint8(hour + 12)) - remain = strings.TrimPrefix(remain, "PM") - default: + remain, state := tryParse(input) + if state == parseStateFail { return input, false } - - t.setMinute(uint8(minute)) - t.setSecond(uint8(second)) return remain, true } -const time24HourLen = len("hh:mm:ss") - func time24Hour(t *CoreTime, input string, ctx map[string]int) (string, bool) { - // hh:mm:ss - if len(input) < time24HourLen { - return input, false - } + tryParse := func(input string) (string, parseState) { + // hh:mm:ss + /// Note that we should update `t` as soon as possible, or we + /// can not get correct result for incomplete input like "12:13" + /// that is shorter than "hh:mm:ss" + state := parseStateNormal + result := oneOrTwoDigitRegex.FindString(input) // 0..23 + length := len(result) + hour, succ := parseDigits(input, length) + if !succ || hour > 23 { + return input, parseStateFail + } + t.setHour(uint8(hour)) - hour, succ := parseDigits(input, 2) - if !succ || hour > 23 || input[2] != ':' { - return input, false - } + // ':' + if input, state = parseSep(input[length:]); state != parseStateNormal { + return input, state + } - minute, succ := parseDigits(input[3:], 2) - if !succ || minute > 59 || input[5] != ':' { - return input, false + result = oneOrTwoDigitRegex.FindString(input) // 0..59 + length = len(result) + minute, succ := parseDigits(input, length) + if !succ || minute > 59 { + return input, parseStateFail + } + t.setMinute(uint8(minute)) + + // ':' + if input, state = parseSep(input[length:]); state != parseStateNormal { + return input, state + } + + result = oneOrTwoDigitRegex.FindString(input) // 0..59 + length = len(result) + second, succ := parseDigits(input, length) + if !succ || second > 59 { + return input, parseStateFail + } + t.setSecond(uint8(second)) + return input[length:], parseStateNormal } - second, succ := parseDigits(input[6:], 2) - if !succ || second > 59 { + remain, state := tryParse(input) + if state == parseStateFail { return input, false } - - t.setHour(uint8(hour)) - t.setMinute(uint8(minute)) - t.setSecond(uint8(second)) - return input[8:], true + return remain, true } const ( @@ -3183,7 +3257,7 @@ func dayOfYearThreeDigits(t *CoreTime, input string, ctx map[string]int) (string func abbreviatedMonth(t *CoreTime, input string, ctx map[string]int) (string, bool) { if len(input) >= 3 { - monthName := input[:3] + monthName := strings.ToLower(input[:3]) if month, ok := monthAbbrev[monthName]; ok { t.setMonth(uint8(month)) return input[len(monthName):], true @@ -3192,9 +3266,16 @@ func abbreviatedMonth(t *CoreTime, input string, ctx map[string]int) (string, bo return input, false } +func hasCaseInsensitivePrefix(input, prefix string) bool { + if len(input) < len(prefix) { + return false + } + return strings.EqualFold(input[:len(prefix)], prefix) +} + func fullNameMonth(t *CoreTime, input string, ctx map[string]int) (string, bool) { for i, month := range MonthNames { - if strings.HasPrefix(input, month) { + if hasCaseInsensitivePrefix(input, month) { t.setMonth(uint8(i + 1)) return input[len(month):], true }