Skip to content

Commit

Permalink
datetime: add TZ support
Browse files Browse the repository at this point in the history
The patch adds timezone support [1] for datetime. For the purpose of
compatibility we got constants for timezones from Tarantool [2]. The
patch adds a script to generate the data according to the
instructions [3].

1. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#timezone-support
2. https://github.com/tarantool/tarantool/blob/9ee45289e01232b8df1413efea11db170ae3b3b4/src/lib/tzcode/timezones.h
3. https://github.com/tarantool/tarantool/blob/9ee45289e01232b8df1413efea11db170ae3b3b4/src/lib/tzcode/gen-zone-abbrevs.pl#L35-L39

Closes #163
  • Loading branch information
oleg-jukovec committed Aug 4, 2022
1 parent 65a6de4 commit 2ba553e
Show file tree
Hide file tree
Showing 8 changed files with 1,813 additions and 68 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
### Added

- Optional msgpack.v5 usage (#124)
- TZ support for datetime (#163)

### Changed

Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ clean:
deps: clean
( cd ./queue; tarantoolctl rocks install queue 1.1.0 )

.PHONY: datetime-timezones
datetime-timezones:
(cd ./datetime; ./gen-timezones.sh)

.PHONY: format
format:
goimports -l -w .
Expand Down
67 changes: 55 additions & 12 deletions datetime/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package datetime
import (
"encoding/binary"
"fmt"
"math"
"time"
)

Expand Down Expand Up @@ -47,13 +48,11 @@ type datetime struct {
// Nanoseconds, fractional part of seconds. Tarantool uses int32_t, see
// a definition in src/lib/core/datetime.h.
nsec int32
// Timezone offset in minutes from UTC (not implemented in Tarantool,
// see gh-163). Tarantool uses a int16_t type, see a structure
// definition in src/lib/core/datetime.h.
// Timezone offset in minutes from UTC. Tarantool uses a int16_t type,
// see a structure definition in src/lib/core/datetime.h.
tzOffset int16
// Olson timezone id (not implemented in Tarantool, see gh-163).
// Tarantool uses a int16_t type, see a structure definition in
// src/lib/core/datetime.h.
// Olson timezone id. Tarantool uses a int16_t type, see a structure
// definition in src/lib/core/datetime.h.
tzIndex int16
}

Expand All @@ -79,16 +78,40 @@ type Datetime struct {
time time.Time
}

const (
noTimezoneZone = ""
noTimezoneOffset = 0
)

// NoTimezone allows to create a datetime without UTC timezone for
// Tarantool. The problem is that Golang by default creates a time value with
// UTC timezone. So it is a way to create a datetime without timezone.
var NoTimezone = time.FixedZone(noTimezoneZone, noTimezoneOffset)

// NewDatetime returns a pointer to a new datetime.Datetime that contains a
// specified time.Time. It may returns an error if the Time value is out of
// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z]
// specified time.Time. It may return an error if the Time value is out of
// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] or
// an invalid timezone or offset value is out of supported range:
// [math.MinInt16, math.MaxInt16]
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()
if zone != noTimezoneZone {
if _, ok := timezoneToIndex[zone]; !ok {
return nil, fmt.Errorf("Unknown timezone %s with offset %d",
zone, offset)
}
}
offset /= 60
if offset < math.MinInt16 || offset > math.MaxInt16 {
return nil, fmt.Errorf("Offset must be between %d and %d", math.MinInt16, math.MaxInt16)
}

dt := new(Datetime)
dt.time = t
return dt, nil
Expand All @@ -105,8 +128,12 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
var dt datetime
dt.seconds = tm.Unix()
dt.nsec = int32(tm.Nanosecond())
dt.tzIndex = 0 // It is not implemented, see gh-163.
dt.tzOffset = 0 // It is not implemented, see gh-163.

zone, offset := tm.Zone()
if zone != noTimezoneZone {
dt.tzIndex = int16(timezoneToIndex[zone])
}
dt.tzOffset = int16(offset / 60)

var bytesSize = secondsSize
if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 {
Expand All @@ -127,7 +154,7 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
func (tm *Datetime) UnmarshalMsgpack(b []byte) error {
l := len(b)
if l != maxSize && l != secondsSize {
return fmt.Errorf("invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize)
return fmt.Errorf("Invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize)
}

var dt datetime
Expand All @@ -140,7 +167,23 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error {
dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:]))
}

tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
tt := time.Unix(dt.seconds, int64(dt.nsec))

loc := NoTimezone
if dt.tzIndex != 0 || dt.tzOffset != 0 {
zone := noTimezoneZone
offset := int(dt.tzOffset) * 60

if dt.tzIndex != 0 {
if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok {
return fmt.Errorf("Unknown timezone index %d", dt.tzIndex)
}
zone = indexToTimezone[int(dt.tzIndex)]
}
loc = time.FixedZone(zone, offset)
}
tt = tt.In(loc)

dtp, err := NewDatetime(tt)
if dtp != nil {
*tm = *dtp
Expand Down
Loading

0 comments on commit 2ba553e

Please sign in to comment.