Skip to content
Merged
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
encode: fix 2400 time encoding for time/timetz
Note that Postgres supports 24:00 for both time and timetz operations.

When evaluating "24:00" for both Time and TimeTZ datatypes, the
time.Time library does not recognise 24 as a legitimate hour. This
requires special parsing for it to work. As such, work around the
problem by subtracting a day, and adding it back later when we recognize
it as 24:00 time.
  • Loading branch information
otan committed May 7, 2020
commit 3dcc14870e7acba326cde6dd9ca25a365a42ecd9
20 changes: 20 additions & 0 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
"sync"
Expand All @@ -16,6 +17,8 @@ import (
"github.com/lib/pq/oid"
)

var time2400Regex = regexp.MustCompile(`^(24:00(?::00(?:\.0+)?)?)(?:[Z+-].*)?$`)

func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte {
switch v := x.(type) {
case []byte:
Expand Down Expand Up @@ -202,10 +205,27 @@ func mustParse(f string, typ oid.Oid, s []byte) time.Time {
str[len(str)-3] == ':' {
f += ":00"
}
// Special case for 24:00 time.
// Unfortunately, golang does not parse 24:00 as a proper time.
// In this case, we want to try "round to the next day", to differentiate.
// As such, we find if the 24:00 time matches at the beginning; if so,
// we default it back to 00:00 but add a day later.
var is2400Time bool
switch typ {
case oid.T_timetz, oid.T_time:
if matches := time2400Regex.FindStringSubmatch(str); matches != nil {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(alternative parsing suggestions welcome)

// Concatenate timezone information at the back.
str = "00:00:00" + str[len(matches[1]):]
is2400Time = true
}
}
t, err := time.Parse(f, str)
if err != nil {
errorf("decode: %s", err)
}
if is2400Time {
t = t.Add(24 * time.Hour)
}
return t
}

Expand Down
79 changes: 79 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,85 @@ func TestFormatTsBackend(t *testing.T) {
}
}

func TestTimeWithoutTimezone(t *testing.T) {
db := openTestConn(t)
defer db.Close()

tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()

for _, tc := range []struct {
refTime string
expectedTime time.Time
}{
{"11:59:59", time.Date(0, 1, 1, 11, 59, 59, 0, time.UTC)},
{"24:00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
{"24:00:00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
{"24:00:00.0", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
{"24:00:00.000000", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
} {
t.Run(
fmt.Sprintf("%s => %s", tc.refTime, tc.expectedTime.Format(time.RFC3339)),
func(t *testing.T) {
var gotTime time.Time
row := tx.QueryRow("select $1::time", tc.refTime)
err = row.Scan(&gotTime)
if err != nil {
t.Fatal(err)
}

if !tc.expectedTime.Equal(gotTime) {
t.Errorf("timestamps not equal: %s != %s", tc.expectedTime, gotTime)
}
},
)
}
}

func TestTimeWithTimezone(t *testing.T) {
db := openTestConn(t)
defer db.Close()

tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()

for _, tc := range []struct {
refTime string
expectedTime time.Time
}{
{"11:59:59+00:00", time.Date(0, 1, 1, 11, 59, 59, 0, time.UTC)},
{"11:59:59+04:00", time.Date(0, 1, 1, 11, 59, 59, 0, time.FixedZone("+04", 4*60*60))},
{"24:00+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
{"24:00Z", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
{"24:00-04:00", time.Date(0, 1, 2, 0, 0, 0, 0, time.FixedZone("-04", -4*60*60))},
{"24:00:00+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
{"24:00:00.0+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
{"24:00:00.000000+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
} {
t.Run(
fmt.Sprintf("%s => %s", tc.refTime, tc.expectedTime.Format(time.RFC3339)),
func(t *testing.T) {
var gotTime time.Time
row := tx.QueryRow("select $1::timetz", tc.refTime)
err = row.Scan(&gotTime)
if err != nil {
t.Fatal(err)
}

if !tc.expectedTime.Equal(gotTime) {
t.Errorf("timestamps not equal: %s != %s", tc.expectedTime, gotTime)
}
},
)
}
}

func TestTimestampWithTimeZone(t *testing.T) {
db := openTestConn(t)
defer db.Close()
Expand Down