From fbb87a3b84d3938876a77525e3293b3f3179b380 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Tue, 10 Feb 2015 07:22:33 +0000 Subject: [PATCH 1/3] Function to decode timestamptz to time.Time --- encode.go | 90 +++++++++++++++++++++++++++++++++++++++++++++++++- encode_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/encode.go b/encode.go index 718267d77..57f2eeafd 100644 --- a/encode.go +++ b/encode.go @@ -51,7 +51,7 @@ func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} case oid.T_bytea: return parseBytea(s) case oid.T_timestamptz: - return parseTs(parameterStatus.currentLocation, string(s)) + return decodeTimestamptzISO(s, parameterStatus.currentLocation) case oid.T_timestamp, oid.T_date: return parseTs(nil, string(s)) case oid.T_time: @@ -207,6 +207,94 @@ func (c *locationCache) getLocation(offset int) *time.Location { return location } +// Decode a Time from the "ISO" format. +func decodeTimestamptzISO(src []byte, sessionLocation *time.Location) time.Time { + atoi := func(s []byte) (result int) { + for i := 0; i < len(s); i++ { + result = result*10 + int(s[i]-'0') + } + return + } + + sepYearMonth := bytes.IndexByte(src, '-') + year := atoi(src[:sepYearMonth]) + src = src[sepYearMonth:] + + // Skips a separator and converts two digits + nextTwoDigits := func() (result int) { + result = atoi(src[1:3]) + src = src[3:] + return + } + + month := nextTwoDigits() + day := nextTwoDigits() + hour := nextTwoDigits() + minute := nextTwoDigits() + second := nextTwoDigits() + nanosecond, offset := 0, 0 + + // Time before current era is suffixed with BC + if src[len(src)-1] == 'C' { + // Negate the year and add one. + // See http://www.postgresql.org/docs/current/static/datetime-input-rules.html + year = 1 - year + + // Strip " BC" + src = src[:len(src)-3] + } + + // Offset from UTC is formatted ±hh[:mm[:ss]] + switch { + case len(src) > 6 && src[len(src)-6] == ':': + offset += atoi(src[len(src)-2:]) + src = src[:len(src)-3] + fallthrough + + case len(src) > 3 && src[len(src)-3] == ':': + offset += 60 * atoi(src[len(src)-2:]) + src = src[:len(src)-3] + fallthrough + + default: + offset += 3600 * atoi(src[len(src)-2:]) + if src[len(src)-3] == '-' { + offset = -offset + } + src = src[:len(src)-3] + } + + // Fractional seconds + if len(src) > 1 { + // Skip fraction separator + i := 1 + for ; i < len(src); i++ { + nanosecond = nanosecond*10 + int(src[i]-'0') + } + // Scale to nanosecnds + for ; i < 10; i++ { + nanosecond *= 10 + } + } + + result := time.Date( + year, time.Month(month), day, + hour, minute, second, nanosecond, + globalLocationCache.getLocation(offset)) + + if sessionLocation != nil { + // Set the location based on session TimeZone, but only when it reports + // the same offset from UTC. + sessionTime := result.In(sessionLocation) + _, sessionOffset := sessionTime.Zone() + if sessionOffset == offset { + result = sessionTime + } + } + + return result +} + // This is a time function specific to the Postgres default DateStyle // setting ("ISO, MDY"), the only one we currently support. This // accounts for the discrepancies between the parsing available with diff --git a/encode_test.go b/encode_test.go index a83f7744f..3d756adca 100644 --- a/encode_test.go +++ b/encode_test.go @@ -95,6 +95,92 @@ func TestParseTs(t *testing.T) { } } +type timeTest struct { + raw time.Time + fromBackend []byte + toBackend []byte +} + +var timestamptzISOTests = []timeTest{ + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06.123456789+00`), + []byte(`0001-02-03T04:05:06.123456789Z`)}, + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), + []byte(`0001-02-03 04:05:06.123456789+02`), + []byte(`0001-02-03T04:05:06.123456789+02:00`)}, + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), + []byte(`0001-02-03 04:05:06.123456789-06`), + []byte(`0001-02-03T04:05:06.123456789-06:00`)}, + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 7*60*60+30*60+9)), + []byte(`0001-02-03 04:05:06.123456789+07:30:09`), + []byte(`0001-02-03T04:05:06.123456789+07:30:09`)}, + + {time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06+00`), + []byte(`0001-02-03T04:05:06Z`)}, + {time.Date(1, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06.000001+00`), + []byte(`0001-02-03T04:05:06.000001Z`)}, + {time.Date(1, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06.001+00`), + []byte(`0001-02-03T04:05:06.001Z`)}, + + {time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), + []byte(`10000-02-03 04:05:06.123456789+00`), + []byte(`10000-02-03T04:05:06.123456789Z`)}, + {time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), + []byte(`10000-02-03 04:05:06.123456789+02`), + []byte(`10000-02-03T04:05:06.123456789+02:00`)}, + {time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), + []byte(`10000-02-03 04:05:06.123456789-06`), + []byte(`10000-02-03T04:05:06.123456789-06:00`)}, + {time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 7*60*60+30*60+9)), + []byte(`10000-02-03 04:05:06.123456789+07:30:09`), + []byte(`10000-02-03T04:05:06.123456789+07:30:09`)}, + + {time.Date(10000, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0)), + []byte(`10000-02-03 04:05:06+00`), + []byte(`10000-02-03T04:05:06Z`)}, + {time.Date(10000, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0)), + []byte(`10000-02-03 04:05:06.000001+00`), + []byte(`10000-02-03T04:05:06.000001Z`)}, + {time.Date(10000, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0)), + []byte(`10000-02-03 04:05:06.001+00`), + []byte(`10000-02-03T04:05:06.001Z`)}, + + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06.123456789+00 BC`), + []byte(`0001-02-03T04:05:06.123456789Z BC`)}, + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), + []byte(`0001-02-03 04:05:06.123456789+02 BC`), + []byte(`0001-02-03T04:05:06.123456789+02:00 BC`)}, + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), + []byte(`0001-02-03 04:05:06.123456789-06 BC`), + []byte(`0001-02-03T04:05:06.123456789-06:00 BC`)}, + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 7*60*60+30*60+9)), + []byte(`0001-02-03 04:05:06.123456789+07:30:09 BC`), + []byte(`0001-02-03T04:05:06.123456789+07:30:09 BC`)}, + + {time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06+00 BC`), + []byte(`0001-02-03T04:05:06Z BC`)}, + {time.Date(0, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06.000001+00 BC`), + []byte(`0001-02-03T04:05:06.000001Z BC`)}, + {time.Date(0, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0)), + []byte(`0001-02-03 04:05:06.001+00 BC`), + []byte(`0001-02-03T04:05:06.001Z BC`)}, +} + +func TestDecodeTimestamptzISO(t *testing.T) { + for _, tt := range timestamptzISOTests { + result := decodeTimestamptzISO(tt.fromBackend, nil) + if !tt.raw.Equal(result) || tt.raw.Format("-0700 MST") != result.Format("-0700 MST") { + t.Errorf("Expected %v, got %v", tt.raw, result) + } + } +} + // Now test that sending the value into the database and parsing it back // returns the same time.Time value. func TestEncodeAndParseTs(t *testing.T) { From 08ad7912ec350a8bda290e1e070702875ce38b24 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Wed, 11 Feb 2015 05:12:30 +0000 Subject: [PATCH 2/3] Assert expected bytes as they are processed --- encode.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/encode.go b/encode.go index 57f2eeafd..216b82881 100644 --- a/encode.go +++ b/encode.go @@ -211,6 +211,9 @@ func (c *locationCache) getLocation(offset int) *time.Location { func decodeTimestamptzISO(src []byte, sessionLocation *time.Location) time.Time { atoi := func(s []byte) (result int) { for i := 0; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { + errorf("unable to parse timestamptz; expected number at %q", s) + } result = result*10 + int(s[i]-'0') } return @@ -220,18 +223,21 @@ func decodeTimestamptzISO(src []byte, sessionLocation *time.Location) time.Time year := atoi(src[:sepYearMonth]) src = src[sepYearMonth:] - // Skips a separator and converts two digits - nextTwoDigits := func() (result int) { + // Asserts a separator then converts the two following digits + nextTwoDigits := func(sep byte) (result int) { + if src[0] != sep { + errorf("unable to parse timestamptz; expected '%v' at %q", sep, src) + } result = atoi(src[1:3]) src = src[3:] return } - month := nextTwoDigits() - day := nextTwoDigits() - hour := nextTwoDigits() - minute := nextTwoDigits() - second := nextTwoDigits() + month := nextTwoDigits('-') + day := nextTwoDigits('-') + hour := nextTwoDigits(' ') + minute := nextTwoDigits(':') + second := nextTwoDigits(':') nanosecond, offset := 0, 0 // Time before current era is suffixed with BC @@ -266,9 +272,16 @@ func decodeTimestamptzISO(src []byte, sessionLocation *time.Location) time.Time // Fractional seconds if len(src) > 1 { + if src[0] != '.' { + errorf("unable to parse timestamptz; expected '.' at %q", src) + } + // Skip fraction separator i := 1 for ; i < len(src); i++ { + if src[i] < '0' || src[i] > '9' { + errorf("unable to parse timestamptz; expected number at %q", src[i:]) + } nanosecond = nanosecond*10 + int(src[i]-'0') } // Scale to nanosecnds From 013da70304f1c41c67b21e5cabc7f373878688c8 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Sun, 15 Feb 2015 21:09:12 +0000 Subject: [PATCH 3/3] Reduce some duplication when decoding timestamptz --- encode.go | 66 +++++++++++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/encode.go b/encode.go index 216b82881..781d97299 100644 --- a/encode.go +++ b/encode.go @@ -219,26 +219,17 @@ func decodeTimestamptzISO(src []byte, sessionLocation *time.Location) time.Time return } - sepYearMonth := bytes.IndexByte(src, '-') - year := atoi(src[:sepYearMonth]) - src = src[sepYearMonth:] - - // Asserts a separator then converts the two following digits - nextTwoDigits := func(sep byte) (result int) { - if src[0] != sep { - errorf("unable to parse timestamptz; expected '%v' at %q", sep, src) + // Asserts a separator then converts the remaining digits. + readDigits := func(sep byte, s []byte) int { + if s[0] != sep { + errorf("unable to parse timestamptz; expected '%v' at %q", sep, s) } - result = atoi(src[1:3]) - src = src[3:] - return + return atoi(s[1:]) } - month := nextTwoDigits('-') - day := nextTwoDigits('-') - hour := nextTwoDigits(' ') - minute := nextTwoDigits(':') - second := nextTwoDigits(':') - nanosecond, offset := 0, 0 + sepYearMonth := bytes.IndexByte(src, '-') + year := atoi(src[:sepYearMonth]) + src = src[sepYearMonth:] // Time before current era is suffixed with BC if src[len(src)-1] == 'C' { @@ -250,42 +241,41 @@ func decodeTimestamptzISO(src []byte, sessionLocation *time.Location) time.Time src = src[:len(src)-3] } + month := readDigits('-', src[0:3]) + day := readDigits('-', src[3:6]) + hour := readDigits(' ', src[6:9]) + minute := readDigits(':', src[9:12]) + second := readDigits(':', src[12:15]) + src = src[15:] + // Offset from UTC is formatted ±hh[:mm[:ss]] + offset := 0 switch { case len(src) > 6 && src[len(src)-6] == ':': - offset += atoi(src[len(src)-2:]) + offset += readDigits(':', src[len(src)-3:]) src = src[:len(src)-3] fallthrough case len(src) > 3 && src[len(src)-3] == ':': - offset += 60 * atoi(src[len(src)-2:]) + offset += 60 * readDigits(':', src[len(src)-3:]) src = src[:len(src)-3] - fallthrough + } - default: - offset += 3600 * atoi(src[len(src)-2:]) - if src[len(src)-3] == '-' { - offset = -offset - } - src = src[:len(src)-3] + if src[len(src)-3] == '+' { + offset += 3600 * readDigits('+', src[len(src)-3:]) + } else { + offset += 3600 * readDigits('-', src[len(src)-3:]) + offset = -offset } + src = src[:len(src)-3] // Fractional seconds + nanosecond := 0 if len(src) > 1 { - if src[0] != '.' { - errorf("unable to parse timestamptz; expected '.' at %q", src) - } + nanosecond = readDigits('.', src) - // Skip fraction separator - i := 1 - for ; i < len(src); i++ { - if src[i] < '0' || src[i] > '9' { - errorf("unable to parse timestamptz; expected number at %q", src[i:]) - } - nanosecond = nanosecond*10 + int(src[i]-'0') - } // Scale to nanosecnds - for ; i < 10; i++ { + for i := len(src); i < 10; i++ { nanosecond *= 10 } }