Skip to content

Commit

Permalink
bugfix: unequal timezone after decoding
Browse files Browse the repository at this point in the history
The patch fixes unequal timezones after Datetime encoding/decoding.
It does two things for the fix:

1. After the patch, a location name is picked from
Time.Location().String() instead of Time.Zone(). It allows us to
encode a timezone from the original name.
2. A decoder function tries to load a location from system location.
It allows to handle unfixed timezones properly.

Closes #217
  • Loading branch information
oleg-jukovec committed Oct 4, 2022
1 parent 49fbabb commit 64e41c5
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 63 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
correctly (#213)
- Decimal package use a test function GetNumberLength instead of a
package-level function getNumberLength (#219)
- Datetime location after encode + decode is unequal (#217)

## [1.8.0] - 2022-08-17

Expand Down
29 changes: 26 additions & 3 deletions datetime/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,25 @@ const (
// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] or
// an invalid timezone or offset value is out of supported range:
// [-12 * 60 * 60, 14 * 60 * 60].
//
// NOTE: Tarantool's datetime.tz value is picked from t.Location().String().
// "Local" location is unsupported, see ExampleNewDatetime_localUnsupported.
func NewDatetime(t time.Time) (*Datetime, error) {
seconds := t.Unix()

if seconds < minSeconds || seconds > maxSeconds {
return nil, fmt.Errorf("time %s is out of supported range", t)
}

zone, offset := t.Zone()
zone := t.Location().String()
_, offset := t.Zone()
if zone != NoTimezone {
if _, ok := timezoneToIndex[zone]; !ok {
return nil, fmt.Errorf("unknown timezone %s with offset %d",
zone, offset)
}
}

if offset < offsetMin || offset > offsetMax {
return nil, fmt.Errorf("offset must be between %d and %d hours",
offsetMin, offsetMax)
Expand Down Expand Up @@ -219,6 +224,13 @@ func (dtime *Datetime) Interval(next *Datetime) Interval {
}

// ToTime returns a time.Time that Datetime contains.
//
// If a Datetime created from time.Time value then an original location is used
// for the time value.
//
// If a Datetime created via unmarshaling Tarantool's datetime then we try to
// create a location with time.LoadLocation() first. In case of failure, we use
// a location created with time.FixedZone().
func (dtime *Datetime) ToTime() time.Time {
return dtime.time
}
Expand All @@ -230,7 +242,8 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
dt.seconds = tm.Unix()
dt.nsec = int32(tm.Nanosecond())

zone, offset := tm.Zone()
zone := tm.Location().String()
_, offset := tm.Zone()
if zone != NoTimezone {
// The zone value already checked in NewDatetime() or
// UnmarshalMsgpack() calls.
Expand Down Expand Up @@ -283,7 +296,17 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error {
}
zone = indexToTimezone[int(dt.tzIndex)]
}
loc = time.FixedZone(zone, offset)
if zone != NoTimezone {
if loadLoc, err := time.LoadLocation(zone); err == nil {
loc = loadLoc
} else {
// Unable to load location.
loc = time.FixedZone(zone, offset)
}
} else {
// Only offset.
loc = time.FixedZone(zone, offset)
}
}
tt = tt.In(loc)

Expand Down
143 changes: 83 additions & 60 deletions datetime/datetime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ func TestCustomTimezone(t *testing.T) {

customZone := "Europe/Moscow"
customOffset := 180 * 60
// Tarantool does not use a custom offset value if a time zone is provided.
// So it will change to an actual one.
zoneOffset := 240 * 60

customLoc := time.FixedZone(customZone, customOffset)
tm, err := time.Parse(time.RFC3339, "2010-08-12T11:44:14Z")
Expand All @@ -527,11 +530,12 @@ func TestCustomTimezone(t *testing.T) {

tpl := resp.Data[0].([]interface{})
if respDt, ok := toDatetime(tpl[0]); ok {
zone, offset := respDt.ToTime().Zone()
zone := respDt.ToTime().Location().String()
_, offset := respDt.ToTime().Zone()
if zone != customZone {
t.Fatalf("Expected zone %s instead of %s", customZone, zone)
}
if offset != customOffset {
if offset != zoneOffset {
t.Fatalf("Expected offset %d instead of %d", customOffset, offset)
}

Expand Down Expand Up @@ -586,62 +590,63 @@ var datetimeSample = []struct {
fmt string
dt string
mpBuf string // MessagePack buffer.
zone string
}{
/* Cases for base encoding without a timezone. */
{time.RFC3339, "2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"},
{time.RFC3339, "2010-08-12T11:39:14Z", "d70462dd634c00000000"},
{time.RFC3339, "1984-03-24T18:04:05Z", "d7041530c31a00000000"},
{time.RFC3339, "2010-01-12T00:00:00Z", "d70480bb4b4b00000000"},
{time.RFC3339, "1970-01-01T00:00:00Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"},
{time.RFC3339, "1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"},
{time.RFC3339, "1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"},
{time.RFC3339, "1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"},
{time.RFC3339, "1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"},
{time.RFC3339, "1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"},
{time.RFC3339, "1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"},
{time.RFC3339, "1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"},
{time.RFC3339, "1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.0Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000000Z", "d7040000000000000000"},
{time.RFC3339, "1973-11-29T21:33:09Z", "d70415cd5b0700000000"},
{time.RFC3339, "2013-10-28T17:51:56Z", "d7043ca46e5200000000"},
{time.RFC3339, "9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"},
{time.RFC3339, "2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000", ""},
{time.RFC3339, "2010-08-12T11:39:14Z", "d70462dd634c00000000", ""},
{time.RFC3339, "1984-03-24T18:04:05Z", "d7041530c31a00000000", ""},
{time.RFC3339, "2010-01-12T00:00:00Z", "d70480bb4b4b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.0Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.00Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.0000Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.00000Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000000Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.0000000Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.00000000Z", "d7040000000000000000", ""},
{time.RFC3339, "1970-01-01T00:00:00.000000000Z", "d7040000000000000000", ""},
{time.RFC3339, "1973-11-29T21:33:09Z", "d70415cd5b0700000000", ""},
{time.RFC3339, "2013-10-28T17:51:56Z", "d7043ca46e5200000000", ""},
{time.RFC3339, "9999-12-31T23:59:59Z", "d7047f41f4ff3a000000", ""},
/* Cases for encoding with a timezone. */
{time.RFC3339 + " MST", "2006-01-02T15:04:00+03:00 MSK", "d804b016b9430000000000000000b400ee00"},
{time.RFC3339, "2006-01-02T15:04:00Z", "d804e040b9430000000000000000b400b303", "Europe/Moscow"},
}

func TestDatetimeInsertSelectDelete(t *testing.T) {
Expand All @@ -653,8 +658,14 @@ func TestDatetimeInsertSelectDelete(t *testing.T) {
for _, testcase := range datetimeSample {
t.Run(testcase.dt, func(t *testing.T) {
tm, err := time.Parse(testcase.fmt, testcase.dt)
if testcase.fmt == time.RFC3339 {
if testcase.zone == "" {
tm = tm.In(noTimezoneLoc)
} else {
loc, err := time.LoadLocation(testcase.zone)
if err != nil {
t.Fatalf("Unable to load location: %s", err)
}
tm = tm.In(loc)
}
if err != nil {
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
Expand Down Expand Up @@ -966,7 +977,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) {
conn := test_helpers.ConnectWithValidation(t, server, opts)
defer conn.Close()

tm := time.Unix(500, 1000)
tm := time.Unix(500, 1000).In(time.FixedZone(NoTimezone, 0))
dt, err := NewDatetime(tm)
if err != nil {
t.Fatalf("Unable to create Datetime from %s: %s", tm, err)
Expand Down Expand Up @@ -999,8 +1010,14 @@ func TestMPEncode(t *testing.T) {
for _, testcase := range datetimeSample {
t.Run(testcase.dt, func(t *testing.T) {
tm, err := time.Parse(testcase.fmt, testcase.dt)
if testcase.fmt == time.RFC3339 {
if testcase.zone == "" {
tm = tm.In(noTimezoneLoc)
} else {
loc, err := time.LoadLocation(testcase.zone)
if err != nil {
t.Fatalf("Unable to load location: %s", err)
}
tm = tm.In(loc)
}
if err != nil {
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
Expand All @@ -1016,7 +1033,7 @@ func TestMPEncode(t *testing.T) {
refBuf, _ := hex.DecodeString(testcase.mpBuf)
if reflect.DeepEqual(buf, refBuf) != true {
t.Fatalf("Failed to encode datetime '%s', actual %x, expected %x",
testcase.dt,
tm,
buf,
refBuf)
}
Expand All @@ -1028,8 +1045,14 @@ func TestMPDecode(t *testing.T) {
for _, testcase := range datetimeSample {
t.Run(testcase.dt, func(t *testing.T) {
tm, err := time.Parse(testcase.fmt, testcase.dt)
if testcase.fmt == time.RFC3339 {
if testcase.zone == "" {
tm = tm.In(noTimezoneLoc)
} else {
loc, err := time.LoadLocation(testcase.zone)
if err != nil {
t.Fatalf("Unable to load location: %s", err)
}
tm = tm.In(loc)
}
if err != nil {
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
Expand Down
52 changes: 52 additions & 0 deletions datetime/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ func Example() {
fmt.Printf("Data: %v\n", respDt.ToTime())
}

// ExampleNewDatetime_localUnsupported demonstrates that "Local" location is
// unsupported.
func ExampleNewDatetime_localUnsupported() {
tm := time.Now().Local()
loc := tm.Location()
fmt.Println("Location:", loc)
if _, err := NewDatetime(tm); err != nil {
fmt.Printf("Could not create a Datetime with %s location.\n", loc)
} else {
fmt.Printf("A Datetime with %s location created.\n", loc)
}
// Output:
// Location: Local
// Could not create a Datetime with Local location.
}

// Example demonstrates how to create a datetime for Tarantool without UTC
// timezone in datetime.
func ExampleNewDatetime_noTimezone() {
Expand Down Expand Up @@ -165,6 +181,42 @@ func ExampleDatetime_Add() {
// New time: 2014-02-28 17:57:29.000000009 +0000 UTC
}

// ExampleDatetime_Add_dst demonstrates how to add an Interval to a
// Datetime value with a DST location.
func ExampleDatetime_Add_dst() {
loc, err := time.LoadLocation("Europe/Moscow")
if err != nil {
fmt.Printf("Unable to load location: %s", err)
return
}
tm := time.Date(2008, 1, 1, 1, 1, 1, 1, loc)
dt, err := NewDatetime(tm)
if err != nil {
fmt.Printf("Unable to create Datetime: %s", err)
return
}

fmt.Printf("Datetime time:\n")
fmt.Printf("%s\n", dt.ToTime())
fmt.Printf("Datetime time + 6 month:\n")
fmt.Printf("%s\n", dt.ToTime().AddDate(0, 6, 0))
dt, err = dt.Add(Interval{Month: 6})
if err != nil {
fmt.Printf("Unable to add 6 month: %s", err)
return
}
fmt.Printf("Datetime + 6 month time:\n")
fmt.Printf("%s\n", dt.ToTime())

// Output:
// Datetime time:
// 2008-01-01 01:01:01.000000001 +0300 MSK
// Datetime time + 6 month:
// 2008-07-01 01:01:01.000000001 +0400 MSD
// Datetime + 6 month time:
// 2008-07-01 01:01:01.000000001 +0400 MSD
}

// ExampleDatetime_Sub demonstrates how to subtract an Interval from a
// Datetime value.
func ExampleDatetime_Sub() {
Expand Down

0 comments on commit 64e41c5

Please sign in to comment.