Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

types: support %X %V %W formats for STR_TO_DATE() #21887

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions types/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func TestStrToDate(t *testing.T) {
format string
expect types.CoreTime
}{
{`2004420`, `%x%v%w`, types.FromDate(2004, 10, 17, 0, 0, 0, 0)},
{`01,05,2013`, `%d,%m,%Y`, types.FromDate(2013, 5, 1, 0, 0, 0, 0)},
{`5 12 2021`, `%m%d%Y`, types.FromDate(2021, 5, 12, 0, 0, 0, 0)},
{`May 01, 2013`, `%M %d,%Y`, types.FromDate(2013, 5, 1, 0, 0, 0, 0)},
Expand Down Expand Up @@ -121,6 +122,16 @@ func TestStrToDate(t *testing.T) {
{`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)},
// %X %V %W
{`200442 Monday`, `%X%V %W`, types.FromDate(2004, 10, 18, 0, 0, 0, 0)},
{`2004420`, `%X%V%w`, types.FromDate(2004, 10, 17, 0, 0, 0, 0)},
{`2004423`, `%X%V%w`, types.FromDate(2004, 10, 20, 0, 0, 0, 0)},
{`200442 Sunday`, `%x%v%W`, types.FromDate(2004, 10, 17, 0, 0, 0, 0)},
tiancaiamao marked this conversation as resolved.
Show resolved Hide resolved
{`200442 Sun`, `%x%v%W`, types.FromDate(2004, 10, 17, 0, 0, 0, 0)},
{`200442 sun`, `%x%v%W`, types.FromDate(2004, 10, 17, 0, 0, 0, 0)},
{`200442 suNd`, `%x%v%W`, types.FromDate(2004, 10, 17, 0, 0, 0, 0)}, // Weird MySQL behavior, matched as sunday
{"2004421", "%Y%U%w", types.FromDate(2004, 10, 18, 0, 0, 0, 0)}, // %U,%u should be used with %Y and not %X or %x
{"69421", "%y%U%w", types.FromDate(2069, 10, 21, 0, 0, 0, 0)}, // %U,%u should be used with %Y and not %X or %x
{`09/10/1021`, `%d/%m/%y`, types.FromDate(2010, 10, 9, 0, 0, 0, 0)}, // '%y' only accept up to 2 digits for year
{`09/10/1021`, `%d/%m/%Y`, types.FromDate(1021, 10, 9, 0, 0, 0, 0)}, // '%Y' accept up to 4 digits for year
{`09/10/10`, `%d/%m/%Y`, types.FromDate(2010, 10, 9, 0, 0, 0, 0)}, // '%Y' will fix the year for only 2 digits
Expand Down Expand Up @@ -182,6 +193,13 @@ func TestStrToDate(t *testing.T) {
{"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`},
// %X %V %W
{`2004427`, `%X%V%w`}, // %w range 0-6, here is 7
{"2004421", "%x%V%w"}, // %x should be used with %v
{"2004421", "%X%v%w"}, // %X should be used with %V
{"2004421", "%X%U%w"}, // %U,%u should be used with %Y and not %X or %x
{`2004663`, `%X%V%w`}, // %V out of range [0, 53]
{`200442 S`, `%x%v%W`},
// MySQL accept `SEPTEMB` as `SEPTEMBER`, but we don't want this "feature" in TiDB
// unless we have to.
{"15 SEPTEMB 2001", "%d %M %Y"},
Expand Down
184 changes: 173 additions & 11 deletions types/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2806,9 +2806,54 @@ func mysqlTimeFix(t *CoreTime, ctx map[string]int) error {
if valueAMorPm == constForPM {
t.setHour(t.getHour() + 12)
}
} else {
if _, ok := ctx["%h"]; ok && t.Hour() == 12 {
t.setHour(0)
} else if _, ok := ctx["%h"]; ok && t.Hour() == 12 {
t.setHour(0)
} else if weekNumber, ok := ctx["WeekNumber"]; ok {
weekDay := ctx["WeekDay"]
sundayFirstDayOfWeek := ctx["SundayFirst"] > 0
if weekDay > 0 {
// %V,%v require %X,%x resprectively,
// %U,%u should be used with %Y and not %X or %x
if ctx["V"] != ctx["X"] || ctx["v"] != ctx["x"] {
return ErrWrongValue.GenWithStackByArgs(TimeStr, t)
}
if ctx["U"] > 0 || ctx["u"] > 0 {
if ctx["X"] > 0 || ctx["x"] > 0 {
return ErrWrongValue.GenWithStackByArgs(TimeStr, t)
}
}

// Number of days since year 0 till 1st Jan of this year.
days := calcDaynr(t.Year(), 1, 1)
// Which day of week is 1st Jan of this year.
weekday1 := calcWeekday(days, sundayFirstDayOfWeek)
// Below we are going to sum:
// 1) number of days since year 0 till 1st day of 1st week of this year
// 2) number of days between 1st week and our week
// 3) and position of our day in the week
days = days + (weekNumber-1)*7
if sundayFirstDayOfWeek {
if weekday1 != 0 {
days = days + 7 - weekday1 + weekDay%7
} else {
days = days - weekday1 + weekDay%7
}
} else {
if weekday1 <= 3 {
days = days - weekday1 + (weekDay - 1)
} else {
days = days + 7 - weekday1 + (weekDay - 1)
}
}

const MaxDayNumver = 3652424
if days <= 0 || days > MaxDayNumver {
return ErrWrongValue.GenWithStackByArgs(TimeStr, t)
}
yy, mm, dd := getDateFromDaynr(uint(days))
t.setYear(uint16(yy))
t.setMonth(uint8(mm))
t.setDay(uint8(dd))
}
}
return nil
Expand Down Expand Up @@ -2926,14 +2971,14 @@ var dateFormatParserTable = map[string]dateFormatParser{
// TODO: Add the following...
// "%a": abbreviatedWeekday, // Abbreviated weekday name (Sun..Sat)
// "%D": dayOfMonthWithSuffix, // Day of the month with English suffix (0th, 1st, 2nd, 3rd)
// "%U": weekMode0, // Week (00..53), where Sunday is the first day of the week; WEEK() mode 0
// "%u": weekMode1, // Week (00..53), where Monday is the first day of the week; WEEK() mode 1
// "%V": weekMode2, // Week (01..53), where Sunday is the first day of the week; WEEK() mode 2; used with %X
// "%v": weekMode3, // Week (01..53), where Monday is the first day of the week; WEEK() mode 3; used with %x
// "%W": weekdayName, // Weekday name (Sunday..Saturday)
// "%w": dayOfWeek, // Day of the week (0=Sunday..6=Saturday)
// "%X": yearOfWeek, // Year for the week where Sunday is the first day of the week, numeric, four digits; used with %V
// "%x": yearOfWeek, // Year for the week, where Monday is the first day of the week, numeric, four digits; used with %v
"%U": weekModeU, // Week (00..53), where Sunday is the first day of the week; WEEK() mode 0
"%u": weekModeu, // Week (00..53), where Monday is the first day of the week; WEEK() mode 1
"%V": weekModeV, // Week (01..53), where Sunday is the first day of the week; WEEK() mode 2; used with %X
"%v": weekModev, // Week (01..53), where Monday is the first day of the week; WEEK() mode 3; used with %x
"%W": weekdayName, // Weekday name (Sunday..Saturday)
"%w": dayOfWeek, // Day of the week (0=Sunday..6=Saturday)
"%X": yearOfWeekX, // Year for the week where Sunday is the first day of the week, numeric, four digits; used with %V
"%x": yearOfWeekx, // Year for the week, where Monday is the first day of the week, numeric, four digits; used with %v
}

// GetFormatType checks the type(Duration, Date or Datetime) of a format string.
Expand Down Expand Up @@ -3272,6 +3317,123 @@ func fullNameMonth(t *CoreTime, input string, ctx map[string]int) (string, bool)
return input, false
}

func yearOfWeekX(t *CoreTime, input string, ctx map[string]int) (string, bool) {
v, succ := parseDigits(input, 4)
if !succ {
return input, false
}
t.setYear(uint16(v))
ctx["X"] = 1
return input[4:], true
}

func yearOfWeekx(t *CoreTime, input string, ctx map[string]int) (string, bool) {
v, succ := parseDigits(input, 4)
if !succ {
return input, false
}
t.setYear(uint16(v))
ctx["x"] = 1
return input[4:], true
}

func weekModeV(t *CoreTime, input string, ctx map[string]int) (string, bool) {
v, succ := parseDigits(input, 2)
// v should be in [01, 53]
if !succ || v <= 0 || v > 53 {
return input, false
}
ctx["WeekNumber"] = v
ctx["SundayFirst"] = 1
ctx["V"] = 1
return input[2:], true
}

func weekModeU(t *CoreTime, input string, ctx map[string]int) (string, bool) {
v, succ := parseDigits(input, 2)
// v should be in [00, 53]
if !succ || v < 0 || v > 53 {
return input, false
}
ctx["WeekNumber"] = v
ctx["SundayFirst"] = 1
ctx["U"] = 1
return input[2:], true
}

func weekModev(t *CoreTime, input string, ctx map[string]int) (string, bool) {
v, succ := parseDigits(input, 2)
// v should be in [01, 53]
if !succ || v <= 0 || v > 53 {
return input, false
}
ctx["WeekNumber"] = v
ctx["v"] = 1
return input[2:], true
}

func weekModeu(t *CoreTime, input string, ctx map[string]int) (string, bool) {
v, succ := parseDigits(input, 2)
// v should be in [00, 53]
if !succ || v < 0 || v > 53 {
return input, false
}
ctx["WeekNumber"] = v
ctx["u"] = 1
return input[2:], true
}
Comment on lines +3320 to +3384
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that most of the codes are duplicates, can we simplify them?

Copy link
Contributor Author

@tiancaiamao tiancaiamao Jun 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not duplicated.
Take a close look, you will find the tiny different between those functions. @wjhuang2016

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Can we take the difference as a parameter? Will it hurt the performance?


func weekdayName(t *CoreTime, input string, ctx map[string]int) (string, bool) {
for i, weekday := range WeekdayNames {
// The behavior is weird in MySQL, it not full string prefix match like:
// strings.HasPrefix(input, weekday)
// It's kind of a case insensitive match and success as long as the matched size > 2
// s => false
// sn => false
// sun => true
// sUn => true
// sunda => true
// sUnday => true
matched := 0
for matched < len(weekday) && matched < len(input) {
if !byteEqualCI(input[matched], weekday[matched]) {
break
}
matched++
}
if matched >= 2 {
// Use 1-7
ctx["WeekDay"] = i + 1
return input[matched:], true
}
}
return input, false
}

func byteEqualCI(x, y byte) bool {
if x == y {
return true
}
if unicode.ToUpper(rune(x)) == unicode.ToUpper(rune(y)) {
return true
}
return false
}

func dayOfWeek(t *CoreTime, input string, ctx map[string]int) (string, bool) {
// 0=Sunday.. 6=Saturday
v, succ := parseDigits(input, 1)
if !succ || v < 0 || v > 6 {
return input, false
}
if v == 0 {
// Should use 1-7 for %w as for %W
v = 7
}
ctx["WeekDay"] = v
return input[1:], true
}

func monthNumeric(t *CoreTime, input string, ctx map[string]int) (string, bool) {
v, step := parseNDigits(input, 2) // 1..12
if step <= 0 || v > 12 {
Expand Down