Skip to content

Commit

Permalink
expression: rewrite builtin function: TIME_FORMAT (#4051)
Browse files Browse the repository at this point in the history
  • Loading branch information
liangjiaxing authored and zz-jason committed Aug 16, 2017
1 parent 276defa commit 16a5f74
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 2 deletions.
46 changes: 45 additions & 1 deletion expression/builtin_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/juju/errors"
"github.com/ngaut/log"
"github.com/pingcap/tidb/ast"
"github.com/pingcap/tidb/context"
"github.com/pingcap/tidb/mysql"
Expand Down Expand Up @@ -2679,7 +2680,50 @@ type timeFormatFunctionClass struct {
}

func (c *timeFormatFunctionClass) getFunction(args []Expression, ctx context.Context) (builtinFunc, error) {
return nil, errFunctionNotExists.GenByArgs("TIME_FORMAT")
if err := c.verifyArgs(args); err != nil {
return nil, errors.Trace(err)
}
bf, err := newBaseBuiltinFuncWithTp(args, ctx, tpString, tpDuration, tpString)
if err != nil {
return nil, errors.Trace(err)
}
// worst case: formatMask=%r%r%r...%r, each %r takes 11 characters
bf.tp.Flen = (args[1].GetType().Flen + 1) / 2 * 11
sig := &builtinTimeFormatSig{baseStringBuiltinFunc{bf}}
return sig.setSelf(sig), nil
}

type builtinTimeFormatSig struct {
baseStringBuiltinFunc
}

// evalString evals a builtinTimeFormatSig.
// See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_time-format
func (b *builtinTimeFormatSig) evalString(row []types.Datum) (string, bool, error) {
dur, isNull, err := b.args[0].EvalDuration(row, b.ctx.GetSessionVars().StmtCtx)
// if err != nil, then dur is ZeroDuration, outputs 00:00:00 in this case which follows the behavior of mysql.
if err != nil {
log.Warnf("Expression.EvalDuration() in time_format() failed, due to %s", err.Error())
}
if isNull {
return "", isNull, errors.Trace(err)
}
formatMask, isNull, err := b.args[1].EvalString(row, b.ctx.GetSessionVars().StmtCtx)
if err != nil || isNull {
return "", isNull, errors.Trace(err)
}
res, err := b.formatTime(dur, formatMask, b.ctx)
return res, isNull, errors.Trace(err)
}

// See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_time-format
func (b *builtinTimeFormatSig) formatTime(t types.Duration, formatMask string, ctx context.Context) (res string, err error) {
t2 := types.Time{
Time: types.FromDate(0, 0, 0, t.Hour(), t.Minute(), t.Second(), t.MicroSecond()),
Type: mysql.TypeDate, Fsp: 0}

str, err := t2.DateFormat(formatMask)
return str, errors.Trace(err)
}

type timeToSecFunctionClass struct {
Expand Down
42 changes: 42 additions & 0 deletions expression/builtin_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,48 @@ func (s *testEvaluatorSuite) TestPeriodAdd(c *C) {
}
}

func (s *testEvaluatorSuite) TestTimeFormat(c *C) {
defer testleak.AfterTest(c)()

// SELECT TIME_FORMAT(null,'%H %k %h %I %l')
args := []types.Datum{types.NewDatum(nil), types.NewStringDatum("%H %k %h %I %l")}
fc := funcs[ast.TimeFormat]
f, err := fc.getFunction(datumsToConstants(args), s.ctx)
c.Assert(err, IsNil)
v, err := f.eval(nil)
c.Assert(err, IsNil)
c.Assert(v.IsNull(), Equals, true)

tblDate := []struct {
Input []string
Expect interface{}
}{
{[]string{"100:00:00", "%H %k %h %I %l"},
"100 100 04 04 4"},
{[]string{"23:00:00", "%H %k %h %I %l"},
"23 23 11 11 11"},
{[]string{"11:00:00", "%H %k %h %I %l"},
"11 11 11 11 11"},
{[]string{"17:42:03.000001", "%r %T %h:%i%p %h:%i:%s %p %H %i %s"},
"05:42:03 PM 17:42:03 05:42PM 05:42:03 PM 17 42 03"},
{[]string{"07:42:03.000001", "%f"},
"000001"},
{[]string{"1990-05-07 19:30:10", "%H %i %s"},
"19 30 10"},
}
dtblDate := tblToDtbl(tblDate)
for i, t := range dtblDate {
fc := funcs[ast.TimeFormat]
f, err := fc.getFunction(datumsToConstants(t["Input"]), s.ctx)
c.Assert(f.isDeterministic(), IsTrue)
c.Assert(err, IsNil)
v, err := f.eval(nil)
c.Assert(err, IsNil)
c.Assert(v, testutil.DatumEquals, t["Expect"][0], Commentf("no.%d \nobtain:%v \nexpect:%v\n", i,
v.GetValue(), t["Expect"][0].GetValue()))
}
}

func (s *testEvaluatorSuite) TestTimeToSec(c *C) {
tests := []struct {
t string
Expand Down
8 changes: 8 additions & 0 deletions expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,14 @@ func (s *testIntegrationSuite) TestTimeBuiltin(c *C) {
tk.MustExec("INSERT INTO t VALUES (0), (20030101010160), (20030101016001), (20030101240101), (20030132010101), (20031301010101), (20031200000000), (20030000000000);")
result = tk.MustQuery("SELECT CAST(ix AS SIGNED) FROM t;")
result.Check(testkit.Rows("0", "0", "0", "0", "0", "0", "0", "0"))
result = tk.MustQuery("SELECT TIME_FORMAT('150:02:28', '%H:%i:%s %p');")
result.Check(testkit.Rows("150:02:28 AM"))
result = tk.MustQuery("SELECT TIME_FORMAT('bad string', '%H:%i:%s %p');")
result.Check(testkit.Rows("00:00:00 AM"))
result = tk.MustQuery("SELECT TIME_FORMAT(null, '%H:%i:%s %p');")
result.Check(testkit.Rows("<nil>"))
result = tk.MustQuery("SELECT TIME_FORMAT(123, '%H:%i:%s %p');")
result.Check(testkit.Rows("00:01:23 AM"))
}

func (s *testIntegrationSuite) TestOpBuiltin(c *C) {
Expand Down
4 changes: 4 additions & 0 deletions plan/typeinfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,10 @@ func (s *testPlanSuite) createTestCase4OtherFuncs() []typeInferTestCase {

func (s *testPlanSuite) createTestCase4TimeFuncs() []typeInferTestCase {
return []typeInferTestCase{
{"time_format('150:02:28', '%r%r%r%r')", mysql.TypeVarString, charset.CharsetUTF8, 0, 44, types.UnspecifiedLength},
{"time_format(123456, '%r%r%r%r')", mysql.TypeVarString, charset.CharsetUTF8, 0, 44, types.UnspecifiedLength},
{"time_format('bad string', '%r%r%r%r')", mysql.TypeVarString, charset.CharsetUTF8, 0, 44, types.UnspecifiedLength},
{"time_format(null, '%r%r%r%r')", mysql.TypeVarString, charset.CharsetUTF8, 0, 44, types.UnspecifiedLength},
{"timestampadd(HOUR, c_int, c_timestamp)", mysql.TypeString, charset.CharsetUTF8, 0, 19, -1},
{"timestampadd(minute, c_double, c_timestamp)", mysql.TypeString, charset.CharsetUTF8, 0, 19, -1},
{"timestampadd(SeconD, c_int, c_char)", mysql.TypeString, charset.CharsetUTF8, 0, 19, -1},
Expand Down
3 changes: 2 additions & 1 deletion util/types/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -1785,7 +1785,8 @@ func (t Time) convertDateFormat(b rune, buf *bytes.Buffer) error {
case 'i':
fmt.Fprintf(buf, "%02d", t.Time.Minute())
case 'p':
if t.Time.Hour() < 12 {
hour := t.Time.Hour()
if hour/12%2 == 0 {
buf.WriteString("AM")
} else {
buf.WriteString("PM")
Expand Down

0 comments on commit 16a5f74

Please sign in to comment.