diff --git a/CHANGELOG.md b/CHANGELOG.md index feb53a1e8..9bb92c1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## HEAD + +Changes: + + - Use decimals field from MySQL to format time types + +Bugfixes: + + - Enable microsecond resolution on TIME, DATETIME and TIMESTAMP + + ## Version 1.2 (2014-06-03) Changes: diff --git a/const.go b/const.go index 379eabec1..5fcc3e98b 100644 --- a/const.go +++ b/const.go @@ -11,7 +11,7 @@ package mysql const ( minProtocolVersion byte = 10 maxPacketSize = 1<<24 - 1 - timeFormat = "2006-01-02 15:04:05" + timeFormat = "2006-01-02 15:04:05.999999" ) // MySQL constants documentation: diff --git a/driver_test.go b/driver_test.go index ef5b371cf..a26027aa5 100644 --- a/driver_test.go +++ b/driver_test.go @@ -327,97 +327,281 @@ func TestString(t *testing.T) { }) } -func TestDateTime(t *testing.T) { - type testmode struct { - selectSuffix string - args []interface{} +type timeTests struct { + dbtype string + tlayout string + tests []timeTest +} + +type timeTest struct { + s string // leading "!": do not use t as value in queries + t time.Time +} + +type timeMode byte + +func (t timeMode) String() string { + switch t { + case binaryString: + return "binary:string" + case binaryTime: + return "binary:time.Time" + case textString: + return "text:string" } - type timetest struct { - in interface{} - sOut string - tOut time.Time - tIsZero bool + panic("unsupported timeMode") +} + +func (t timeMode) Binary() bool { + switch t { + case binaryString, binaryTime: + return true } - type tester func(dbt *DBTest, rows *sql.Rows, - test *timetest, sqltype, resulttype, mode string) - type setup struct { - vartype string - dsnSuffix string - test tester + return false +} + +const ( + binaryString timeMode = iota + binaryTime + textString +) + +func (t timeTest) genQuery(dbtype string, mode timeMode) string { + var inner string + if mode.Binary() { + inner = "?" + } else { + inner = `"%s"` } - var ( - modes = map[string]*testmode{ - "text": &testmode{}, - "binary": &testmode{" WHERE 1 = ?", []interface{}{1}}, - } - timetests = map[string][]*timetest{ - "DATE": { - {sDate, sDate, tDate, false}, - {sDate0, sDate0, tDate0, true}, - {tDate, sDate, tDate, false}, - {tDate0, sDate0, tDate0, true}, - }, - "DATETIME": { - {sDateTime, sDateTime, tDateTime, false}, - {sDateTime0, sDateTime0, tDate0, true}, - {tDateTime, sDateTime, tDateTime, false}, - {tDate0, sDateTime0, tDate0, true}, - }, - } - setups = []*setup{ - {"string", "&parseTime=false", func( - dbt *DBTest, rows *sql.Rows, test *timetest, sqltype, resulttype, mode string) { - var sOut string - if err := rows.Scan(&sOut); err != nil { - dbt.Errorf("%s (%s %s): %s", sqltype, resulttype, mode, err.Error()) - } else if test.sOut != sOut { - dbt.Errorf("%s (%s %s): %s != %s", sqltype, resulttype, mode, test.sOut, sOut) - } - }}, - {"time.Time", "&parseTime=true", func( - dbt *DBTest, rows *sql.Rows, test *timetest, sqltype, resulttype, mode string) { - var tOut time.Time - if err := rows.Scan(&tOut); err != nil { - dbt.Errorf("%s (%s %s): %s", sqltype, resulttype, mode, err.Error()) - } else if test.tOut != tOut || test.tIsZero != tOut.IsZero() { - dbt.Errorf("%s (%s %s): %s [%t] != %s [%t]", sqltype, resulttype, mode, test.tOut, test.tIsZero, tOut, tOut.IsZero()) - } - }}, + return `SELECT cast(` + inner + ` as ` + dbtype + `)` +} + +func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode timeMode) { + var rows *sql.Rows + query := t.genQuery(dbtype, mode) + switch mode { + case binaryString: + rows = dbt.mustQuery(query, t.s) + case binaryTime: + rows = dbt.mustQuery(query, t.t) + case textString: + query = fmt.Sprintf(query, t.s) + rows = dbt.mustQuery(query) + default: + panic("unsupported mode") + } + defer rows.Close() + var err error + if !rows.Next() { + err = rows.Err() + if err == nil { + err = fmt.Errorf("no data") } - ) + dbt.Errorf("%s [%s]: %s", dbtype, mode, err) + return + } + var dst interface{} + err = rows.Scan(&dst) + if err != nil { + dbt.Errorf("%s [%s]: %s", dbtype, mode, err) + return + } + switch val := dst.(type) { + case []uint8: + str := string(val) + if str == t.s { + return + } + if mode.Binary() && dbtype == "DATETIME" && len(str) == 26 && str[:19] == t.s { + // a fix mainly for TravisCI: + // accept full microsecond resolution in result for DATETIME columns + // where the binary protocol was used + return + } + dbt.Errorf("%s [%s] to string: expected %q, got %q", + dbtype, mode, + t.s, str, + ) + case time.Time: + if val == t.t { + return + } + dbt.Errorf("%s [%s] to string: expected %q, got %q", + dbtype, mode, + t.s, val.Format(tlayout), + ) + default: + fmt.Printf("%#v\n", []interface{}{dbtype, tlayout, mode, t.s, t.t}) + dbt.Errorf("%s [%s]: unhandled type %T (is '%v')", + dbtype, mode, + val, val, + ) + } +} - var s *setup - testTime := func(dbt *DBTest) { - var rows *sql.Rows - for sqltype, tests := range timetests { - dbt.mustExec("CREATE TABLE test (value " + sqltype + ")") - for _, test := range tests { - for mode, q := range modes { - dbt.mustExec("TRUNCATE test") - dbt.mustExec("INSERT INTO test VALUES (?)", test.in) - rows = dbt.mustQuery("SELECT value FROM test"+q.selectSuffix, q.args...) - if rows.Next() { - s.test(dbt, rows, test, sqltype, s.vartype, mode) - } else { - if err := rows.Err(); err != nil { - dbt.Errorf("%s (%s %s): %s", - sqltype, s.vartype, mode, err.Error()) - } else { - dbt.Errorf("%s (%s %s): no data", - sqltype, s.vartype, mode) - } +func TestDateTime(t *testing.T) { + afterTime := func(t time.Time, d string) time.Time { + dur, err := time.ParseDuration(d) + if err != nil { + panic(err) + } + return t.Add(dur) + } + // NOTE: MySQL rounds DATETIME(x) up - but that's not included in the tests + format := "2006-01-02 15:04:05.999999" + t0 := time.Time{} + tstr0 := "0000-00-00 00:00:00.000000" + testcases := []timeTests{ + {"DATE", format[:10], []timeTest{ + {t: time.Date(2011, 11, 20, 0, 0, 0, 0, time.UTC)}, + {t: t0, s: tstr0[:10]}, + }}, + {"DATETIME", format[:19], []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)}, + {t: t0, s: tstr0[:19]}, + }}, + {"DATETIME(0)", format[:21], []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)}, + {t: t0, s: tstr0[:19]}, + }}, + {"DATETIME(1)", format[:21], []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 100000000, time.UTC)}, + {t: t0, s: tstr0[:21]}, + }}, + {"DATETIME(6)", format, []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 123456000, time.UTC)}, + {t: t0, s: tstr0}, + }}, + {"TIME", format[11:19], []timeTest{ + {t: afterTime(t0, "12345s")}, + {s: "!-12:34:56"}, + {s: "!-838:59:59"}, + {s: "!838:59:59"}, + {t: t0, s: tstr0[11:19]}, + }}, + {"TIME(0)", format[11:19], []timeTest{ + {t: afterTime(t0, "12345s")}, + {s: "!-12:34:56"}, + {s: "!-838:59:59"}, + {s: "!838:59:59"}, + {t: t0, s: tstr0[11:19]}, + }}, + {"TIME(1)", format[11:21], []timeTest{ + {t: afterTime(t0, "12345600ms")}, + {s: "!-12:34:56.7"}, + {s: "!-838:59:58.9"}, + {s: "!838:59:58.9"}, + {t: t0, s: tstr0[11:21]}, + }}, + {"TIME(6)", format[11:], []timeTest{ + {t: afterTime(t0, "1234567890123000ns")}, + {s: "!-12:34:56.789012"}, + {s: "!-838:59:58.999999"}, + {s: "!838:59:58.999999"}, + {t: t0, s: tstr0[11:]}, + }}, + } + dsns := []string{ + dsn + "&parseTime=true", + dsn + "&parseTime=false", + } + for _, testdsn := range dsns { + runTests(t, testdsn, func(dbt *DBTest) { + microsecsSupported := false + zeroDateSupported := false + var rows *sql.Rows + var err error + rows, err = dbt.db.Query(`SELECT cast("00:00:00.1" as TIME(1)) = "00:00:00.1"`) + if err == nil { + rows.Scan(µsecsSupported) + rows.Close() + } + rows, err = dbt.db.Query(`SELECT cast("0000-00-00" as DATE) = "0000-00-00"`) + if err == nil { + rows.Scan(&zeroDateSupported) + rows.Close() + } + for _, setups := range testcases { + if t := setups.dbtype; !microsecsSupported && t[len(t)-1:] == ")" { + // skip fractional second tests if unsupported by server + continue + } + for _, setup := range setups.tests { + allowBinTime := true + if setup.s == "" { + // fill time string whereever Go can reliable produce it + setup.s = setup.t.Format(setups.tlayout) + } else if setup.s[0] == '!' { + // skip tests using setup.t as source in queries + allowBinTime = false + // fix setup.s - remove the "!" + setup.s = setup.s[1:] + } + if !zeroDateSupported && setup.s == tstr0[:len(setup.s)] { + // skip disallowed 0000-00-00 date + continue + } + setup.run(dbt, setups.dbtype, setups.tlayout, textString) + setup.run(dbt, setups.dbtype, setups.tlayout, binaryString) + if allowBinTime { + setup.run(dbt, setups.dbtype, setups.tlayout, binaryTime) } } } - dbt.mustExec("DROP TABLE IF EXISTS test") - } + }) } +} - timeDsn := dsn + "&sql_mode=ALLOW_INVALID_DATES" - for _, v := range setups { - s = v - runTests(t, timeDsn+s.dsnSuffix, testTime) - } +func TestTimestampMicros(t *testing.T) { + format := "2006-01-02 15:04:05.999999" + f0 := format[:19] + f1 := format[:21] + f6 := format[:26] + runTests(t, dsn, func(dbt *DBTest) { + // check if microseconds are supported. + // Do not use timestamp(x) for that check - before 5.5.6, x would mean display width + // and not precision. + // Se last paragraph at http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html + microsecsSupported := false + if rows, err := dbt.db.Query(`SELECT cast("00:00:00.1" as TIME(1)) = "00:00:00.1"`); err == nil { + rows.Scan(µsecsSupported) + rows.Close() + } + if !microsecsSupported { + // skip test + return + } + _, err := dbt.db.Exec(` + CREATE TABLE test ( + value0 TIMESTAMP NOT NULL DEFAULT '` + f0 + `', + value1 TIMESTAMP(1) NOT NULL DEFAULT '` + f1 + `', + value6 TIMESTAMP(6) NOT NULL DEFAULT '` + f6 + `' + )`, + ) + if err != nil { + dbt.Error(err) + } + defer dbt.mustExec("DROP TABLE IF EXISTS test") + dbt.mustExec("INSERT INTO test SET value0=?, value1=?, value6=?", f0, f1, f6) + var res0, res1, res6 string + rows := dbt.mustQuery("SELECT * FROM test") + if !rows.Next() { + dbt.Errorf("test contained no selectable values") + } + err = rows.Scan(&res0, &res1, &res6) + if err != nil { + dbt.Error(err) + } + if res0 != f0 { + dbt.Errorf("expected %q, got %q", f0, res0) + } + if res1 != f1 { + dbt.Errorf("expected %q, got %q", f1, res1) + } + if res6 != f6 { + dbt.Errorf("expected %q, got %q", f6, res6) + } + }) } func TestNULL(t *testing.T) { @@ -766,6 +950,17 @@ func TestFoundRows(t *testing.T) { func TestStrict(t *testing.T) { // ALLOW_INVALID_DATES to get rid of stricter modes - we want to test for warnings, not errors relaxedDsn := dsn + "&sql_mode=ALLOW_INVALID_DATES" + // make sure the MySQL version is recent enough with a separate connection + // before running the test + conn, err := MySQLDriver{}.Open(relaxedDsn) + if conn != nil { + conn.Close() + } + if me, ok := err.(*MySQLError); ok && me.Number == 1231 { + // Error 1231: Variable 'sql_mode' can't be set to the value of 'ALLOW_INVALID_DATES' + // => skip test, MySQL server version is too old + return + } runTests(t, relaxedDsn, func(dbt *DBTest) { dbt.mustExec("CREATE TABLE test (a TINYINT NOT NULL, b CHAR(4))") @@ -956,7 +1151,7 @@ func TestCollation(t *testing.T) { "latin1_general_ci", "binary", "utf8_unicode_ci", - "utf8mb4_general_ci", + "cp1257_bin", } for _, collation := range testCollations { @@ -1011,8 +1206,8 @@ func TestTimezoneConversion(t *testing.T) { // Insert local time into database (should be converted) usCentral, _ := time.LoadLocation("US/Central") - now := time.Now().In(usCentral) - dbt.mustExec("INSERT INTO test VALUE (?)", now) + reftime := time.Date(2014, 05, 30, 18, 03, 17, 0, time.UTC).In(usCentral) + dbt.mustExec("INSERT INTO test VALUE (?)", reftime) // Retrieve time from DB rows := dbt.mustQuery("SELECT ts FROM test") @@ -1020,17 +1215,17 @@ func TestTimezoneConversion(t *testing.T) { dbt.Fatal("Didn't get any rows out") } - var nowDB time.Time - err := rows.Scan(&nowDB) + var dbTime time.Time + err := rows.Scan(&dbTime) if err != nil { dbt.Fatal("Err", err) } // Check that dates match - if now.Unix() != nowDB.Unix() { + if reftime.Unix() != dbTime.Unix() { dbt.Errorf("Times don't match.\n") - dbt.Errorf(" Now(%v)=%v\n", usCentral, now) - dbt.Errorf(" Now(UTC)=%v\n", nowDB) + dbt.Errorf(" Now(%v)=%v\n", usCentral, reftime) + dbt.Errorf(" Now(UTC)=%v\n", dbTime) } } @@ -1039,42 +1234,6 @@ func TestTimezoneConversion(t *testing.T) { } } -// This tests for https://github.com/go-sql-driver/mysql/pull/139 -// -// An extra (invisible) nil byte was being added to the beginning of positive -// time strings. -func TestTimeSign(t *testing.T) { - runTests(t, dsn, func(dbt *DBTest) { - var sTimes = []struct { - value string - fieldType string - }{ - {"12:34:56", "TIME"}, - {"-12:34:56", "TIME"}, - // As described in http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html - // they *should* work, but only in 5.6+. - // { "12:34:56.789", "TIME(3)" }, - // { "-12:34:56.789", "TIME(3)" }, - } - - for _, sTime := range sTimes { - dbt.db.Exec("DROP TABLE IF EXISTS test") - dbt.mustExec("CREATE TABLE test (id INT, time_field " + sTime.fieldType + ")") - dbt.mustExec("INSERT INTO test (id, time_field) VALUES(1, '" + sTime.value + "')") - rows := dbt.mustQuery("SELECT time_field FROM test WHERE id = ?", 1) - if rows.Next() { - var oTime string - rows.Scan(&oTime) - if oTime != sTime.value { - dbt.Errorf(`time values differ: got %q, expected %q.`, oTime, sTime.value) - } - } else { - dbt.Error("expecting at least one row.") - } - } - }) -} - // Special cases func TestRowsClose(t *testing.T) { diff --git a/packets.go b/packets.go index 618098146..f2e385bf8 100644 --- a/packets.go +++ b/packets.go @@ -557,20 +557,21 @@ func (mc *mysqlConn) readColumns(count int) ([]mysqlField, error) { return nil, err } - // Filler [1 byte] - // Charset [16 bit uint] - // Length [32 bit uint] + // Filler [uint8] + // Charset [charset, collation uint8] + // Length [uint32] pos += n + 1 + 2 + 4 - // Field type [byte] + // Field type [uint8] columns[i].fieldType = data[pos] pos++ - // Flags [16 bit uint] + // Flags [uint16] columns[i].flags = fieldFlag(binary.LittleEndian.Uint16(data[pos : pos+2])) - //pos += 2 + pos += 2 - // Decimals [8 bit uint] + // Decimals [uint8] + columns[i].decimals = data[pos] //pos++ // Default value [len coded binary] @@ -1055,88 +1056,53 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { } return err - // Date YYYY-MM-DD - case fieldTypeDate, fieldTypeNewDate: + case + fieldTypeDate, fieldTypeNewDate, // Date YYYY-MM-DD + fieldTypeTime, // Time [-][H]HH:MM:SS[.fractal] + fieldTypeTimestamp, fieldTypeDateTime: // Timestamp YYYY-MM-DD HH:MM:SS[.fractal] + num, isNull, n := readLengthEncodedInteger(data[pos:]) pos += n - if isNull { + switch { + case isNull: dest[i] = nil continue - } - - if rows.mc.parseTime { + case rows.columns[i].fieldType == fieldTypeTime: + // database/sql does not support an equivalent to TIME, return a string + var dstlen uint8 + switch decimals := rows.columns[i].decimals; decimals { + case 0x00, 0x1f: + dstlen = 8 + case 1, 2, 3, 4, 5, 6: + dstlen = 8 + 1 + decimals + default: + return fmt.Errorf( + "MySQL protocol error, illegal decimals value %d", + rows.columns[i].decimals, + ) + } + dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, true) + case rows.mc.parseTime: dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc) - } else { - dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], false) - } - - if err == nil { - pos += int(num) - continue - } else { - return err - } - - // Time [-][H]HH:MM:SS[.fractal] - case fieldTypeTime: - num, isNull, n := readLengthEncodedInteger(data[pos:]) - pos += n - - if num == 0 { - if isNull { - dest[i] = nil - continue + default: + var dstlen uint8 + if rows.columns[i].fieldType == fieldTypeDate { + dstlen = 10 } else { - dest[i] = []byte("00:00:00") - continue + switch decimals := rows.columns[i].decimals; decimals { + case 0x00, 0x1f: + dstlen = 19 + case 1, 2, 3, 4, 5, 6: + dstlen = 19 + 1 + decimals + default: + return fmt.Errorf( + "MySQL protocol error, illegal decimals value %d", + rows.columns[i].decimals, + ) + } } - } - - var sign string - if data[pos] == 1 { - sign = "-" - } - - switch num { - case 8: - dest[i] = []byte(fmt.Sprintf( - sign+"%02d:%02d:%02d", - uint16(data[pos+1])*24+uint16(data[pos+5]), - data[pos+6], - data[pos+7], - )) - pos += 8 - continue - case 12: - dest[i] = []byte(fmt.Sprintf( - sign+"%02d:%02d:%02d.%06d", - uint16(data[pos+1])*24+uint16(data[pos+5]), - data[pos+6], - data[pos+7], - binary.LittleEndian.Uint32(data[pos+8:pos+12]), - )) - pos += 12 - continue - default: - return fmt.Errorf("Invalid TIME-packet length %d", num) - } - - // Timestamp YYYY-MM-DD HH:MM:SS[.fractal] - case fieldTypeTimestamp, fieldTypeDateTime: - num, isNull, n := readLengthEncodedInteger(data[pos:]) - - pos += n - - if isNull { - dest[i] = nil - continue - } - - if rows.mc.parseTime { - dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc) - } else { - dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], true) + dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, false) } if err == nil { diff --git a/rows.go b/rows.go index df4ef06cb..ab2571f36 100644 --- a/rows.go +++ b/rows.go @@ -14,9 +14,10 @@ import ( ) type mysqlField struct { - fieldType byte - flags fieldFlag name string + flags fieldFlag + fieldType byte + decimals byte } type mysqlRows struct { diff --git a/utils.go b/utils.go index b6f200389..56f1b082e 100644 --- a/utils.go +++ b/utils.go @@ -451,17 +451,13 @@ func (nt NullTime) Value() (driver.Value, error) { } func parseDateTime(str string, loc *time.Location) (t time.Time, err error) { + base := "0000-00-00 00:00:00.0000000" switch len(str) { - case 10: // YYYY-MM-DD - if str == "0000-00-00" { + case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM" + if str == base[:len(str)] { return } - t, err = time.Parse(timeFormat[:10], str) - case 19: // YYYY-MM-DD HH:MM:SS - if str == "0000-00-00 00:00:00" { - return - } - t, err = time.Parse(timeFormat, str) + t, err = time.Parse(timeFormat[:len(str)], str) default: err = fmt.Errorf("Invalid Time-String: %s", str) return @@ -519,80 +515,143 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va // if the DATE or DATETIME has the zero value. // It must never be changed. // The current behavior depends on database/sql copying the result. -var zeroDateTime = []byte("0000-00-00 00:00:00") +var zeroDateTime = []byte("0000-00-00 00:00:00.000000") -func formatBinaryDateTime(src []byte, withTime bool) (driver.Value, error) { +func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value, error) { + // length expects the deterministic length of the zero value, + // negative time and 100+ hours are automatically added if needed + const digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" + const digits10 = "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999" if len(src) == 0 { - if withTime { - return zeroDateTime, nil + if justTime { + return zeroDateTime[11 : 11+length], nil } - return zeroDateTime[:10], nil - } - var dst []byte - if withTime { - if len(src) == 11 { - dst = []byte("0000-00-00 00:00:00.000000") + return zeroDateTime[:length], nil + } + var dst []byte // return value + var pt, p1, p2, p3 byte // current digit pair + var zOffs byte // offset of value in zeroDateTime + if justTime { + switch length { + case + 8, // time (can be up to 10 when negative and 100+ hours) + 10, 11, 12, 13, 14, 15: // time with fractional seconds + default: + return nil, fmt.Errorf("illegal TIME length %d", length) + } + switch len(src) { + case 8, 12: + default: + return nil, fmt.Errorf("Invalid TIME-packet length %d", len(src)) + } + // +2 to enable negative time and 100+ hours + dst = make([]byte, 0, length+2) + if src[0] == 1 { + dst = append(dst, '-') + } + if src[1] != 0 { + hour := uint16(src[1])*24 + uint16(src[5]) + pt = byte(hour / 100) + p1 = byte(hour - 100*uint16(pt)) + dst = append(dst, digits01[pt]) } else { - dst = []byte("0000-00-00 00:00:00") + p1 = src[5] } + zOffs = 11 + src = src[6:] } else { - dst = []byte("0000-00-00") - } - switch len(src) { - case 11: - microsecs := binary.LittleEndian.Uint32(src[7:11]) - tmp32 := microsecs / 10 - dst[25] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[24] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[23] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[22] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[21] += byte(microsecs - 10*tmp32) - dst[20] += byte(microsecs / 10) - fallthrough - case 7: - second := src[6] - tmp := second / 10 - dst[18] += second - 10*tmp - dst[17] += tmp - minute := src[5] - tmp = minute / 10 - dst[15] += minute - 10*tmp - dst[14] += tmp - hour := src[4] - tmp = hour / 10 - dst[12] += hour - 10*tmp - dst[11] += tmp - fallthrough - case 4: - day := src[3] - tmp := day / 10 - dst[9] += day - 10*tmp - dst[8] += tmp - month := src[2] - tmp = month / 10 - dst[6] += month - 10*tmp - dst[5] += tmp + switch length { + case 10, 19, 21, 22, 23, 24, 25, 26: + default: + t := "DATE" + if length > 10 { + t += "TIME" + } + return nil, fmt.Errorf("illegal %s length %d", t, length) + } + switch len(src) { + case 4, 7, 11: + default: + t := "DATE" + if length > 10 { + t += "TIME" + } + return nil, fmt.Errorf("illegal %s-packet length %d", t, len(src)) + } + dst = make([]byte, 0, length) + // start with the date year := binary.LittleEndian.Uint16(src[:2]) - tmp16 := year / 10 - dst[3] += byte(year - 10*tmp16) - tmp16, year = tmp16/10, tmp16 - dst[2] += byte(year - 10*tmp16) - tmp16, year = tmp16/10, tmp16 - dst[1] += byte(year - 10*tmp16) - dst[0] += byte(tmp16) + pt = byte(year / 100) + p1 = byte(year - 100*uint16(pt)) + p2, p3 = src[2], src[3] + dst = append(dst, + digits10[pt], digits01[pt], + digits10[p1], digits01[p1], '-', + digits10[p2], digits01[p2], '-', + digits10[p3], digits01[p3], + ) + if length == 10 { + return dst, nil + } + if len(src) == 4 { + return append(dst, zeroDateTime[10:length]...), nil + } + dst = append(dst, ' ') + p1 = src[4] // hour + src = src[5:] + } + // p1 is 2-digit hour, src is after hour + p2, p3 = src[0], src[1] + dst = append(dst, + digits10[p1], digits01[p1], ':', + digits10[p2], digits01[p2], ':', + digits10[p3], digits01[p3], + ) + if length <= byte(len(dst)) { return dst, nil } - var t string - if withTime { - t = "DATETIME" - } else { - t = "DATE" + src = src[2:] + if len(src) == 0 { + return append(dst, zeroDateTime[19:zOffs+length]...), nil + } + microsecs := binary.LittleEndian.Uint32(src[:4]) + p1 = byte(microsecs / 10000) + microsecs -= 10000 * uint32(p1) + p2 = byte(microsecs / 100) + microsecs -= 100 * uint32(p2) + p3 = byte(microsecs) + switch decimals := zOffs + length - 20; decimals { + default: + return append(dst, '.', + digits10[p1], digits01[p1], + digits10[p2], digits01[p2], + digits10[p3], digits01[p3], + ), nil + case 1: + return append(dst, '.', + digits10[p1], + ), nil + case 2: + return append(dst, '.', + digits10[p1], digits01[p1], + ), nil + case 3: + return append(dst, '.', + digits10[p1], digits01[p1], + digits10[p2], + ), nil + case 4: + return append(dst, '.', + digits10[p1], digits01[p1], + digits10[p2], digits01[p2], + ), nil + case 5: + return append(dst, '.', + digits10[p1], digits01[p1], + digits10[p2], digits01[p2], + digits10[p3], + ), nil } - return nil, fmt.Errorf("invalid %s-packet length %d", t, len(src)) } /****************************************************************************** diff --git a/utils_test.go b/utils_test.go index 301d81a62..6e50b09b9 100644 --- a/utils_test.go +++ b/utils_test.go @@ -191,22 +191,22 @@ func TestFormatBinaryDateTime(t *testing.T) { rawDate[5] = 46 // minutes rawDate[6] = 23 // seconds binary.LittleEndian.PutUint32(rawDate[7:], 987654) // microseconds - expect := func(expected string, length int, withTime bool) { - actual, _ := formatBinaryDateTime(rawDate[:length], withTime) + expect := func(expected string, inlen, outlen uint8) { + actual, _ := formatBinaryDateTime(rawDate[:inlen], outlen, false) bytes, ok := actual.([]byte) if !ok { t.Errorf("formatBinaryDateTime must return []byte, was %T", actual) } if string(bytes) != expected { t.Errorf( - "expected %q, got %q for length %d, withTime %v", - bytes, actual, length, withTime, + "expected %q, got %q for length in %d, out %d", + bytes, actual, inlen, outlen, ) } } - expect("0000-00-00", 0, false) - expect("0000-00-00 00:00:00", 0, true) - expect("1978-12-30", 4, false) - expect("1978-12-30 15:46:23", 7, true) - expect("1978-12-30 15:46:23.987654", 11, true) + expect("0000-00-00", 0, 10) + expect("0000-00-00 00:00:00", 0, 19) + expect("1978-12-30", 4, 10) + expect("1978-12-30 15:46:23", 7, 19) + expect("1978-12-30 15:46:23.987654", 11, 26) }