From 3dcc14870e7acba326cde6dd9ca25a365a42ecd9 Mon Sep 17 00:00:00 2001 From: Oliver Tan Date: Thu, 7 May 2020 14:22:17 -0700 Subject: [PATCH] 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. --- encode.go | 20 +++++++++++++ encode_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/encode.go b/encode.go index 73cafb89..c4dafe27 100644 --- a/encode.go +++ b/encode.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "math" + "regexp" "strconv" "strings" "sync" @@ -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: @@ -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 { + // 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 } diff --git a/encode_test.go b/encode_test.go index 813643c2..3406c315 100644 --- a/encode_test.go +++ b/encode_test.go @@ -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()