Skip to content

Commit 6082fb5

Browse files
committed
datetime: add TZ support
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
1 parent f5cbb11 commit 6082fb5

8 files changed

+1852
-68
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1111
### Added
1212

1313
- Optional msgpack.v5 usage (#124)
14+
- TZ support for datetime (#163)
1415

1516
### Changed
1617

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ clean:
2424
deps: clean
2525
( cd ./queue; tarantoolctl rocks install queue 1.1.0 )
2626

27+
.PHONY: datetime-timezones
28+
datetime-timezones:
29+
(cd ./datetime; ./gen-timezones.sh)
30+
2731
.PHONY: format
2832
format:
2933
goimports -l -w .

datetime/datetime.go

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,11 @@ type datetime struct {
4747
// Nanoseconds, fractional part of seconds. Tarantool uses int32_t, see
4848
// a definition in src/lib/core/datetime.h.
4949
nsec int32
50-
// Timezone offset in minutes from UTC (not implemented in Tarantool,
51-
// see gh-163). Tarantool uses a int16_t type, see a structure
52-
// definition in src/lib/core/datetime.h.
50+
// Timezone offset in minutes from UTC. Tarantool uses a int16_t type,
51+
// see a structure definition in src/lib/core/datetime.h.
5352
tzOffset int16
54-
// Olson timezone id (not implemented in Tarantool, see gh-163).
55-
// Tarantool uses a int16_t type, see a structure definition in
56-
// src/lib/core/datetime.h.
53+
// Olson timezone id. Tarantool uses a int16_t type, see a structure
54+
// definition in src/lib/core/datetime.h.
5755
tzIndex int16
5856
}
5957

@@ -79,16 +77,45 @@ type Datetime struct {
7977
time time.Time
8078
}
8179

80+
const (
81+
// NoTimezone allows to create a datetime without UTC timezone for
82+
// Tarantool. The problem is that Golang by default creates a time value
83+
// with UTC timezone. So it is a way to create a datetime without timezone.
84+
NoTimezone = ""
85+
)
86+
87+
var noTimezoneLoc = time.FixedZone(NoTimezone, 0)
88+
89+
const (
90+
offsetMin = -12 * 60 * 60
91+
offsetMax = 14 * 60 * 60
92+
)
93+
8294
// NewDatetime returns a pointer to a new datetime.Datetime that contains a
83-
// specified time.Time. It may returns an error if the Time value is out of
84-
// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z]
95+
// specified time.Time. It may return an error if the Time value is out of
96+
// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] or
97+
// an invalid timezone or offset value is out of supported range:
98+
// [-12 * 60 * 60, 14 * 60 * 60].
8599
func NewDatetime(t time.Time) (*Datetime, error) {
86100
seconds := t.Unix()
87101

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

106+
zone, offset := t.Zone()
107+
if zone != NoTimezone {
108+
if _, ok := timezoneToIndex[zone]; !ok {
109+
return nil, fmt.Errorf("Unknown timezone %s with offset %d",
110+
zone, offset)
111+
}
112+
}
113+
if offset < offsetMin || offset > offsetMax {
114+
return nil, fmt.Errorf("Offset must be between %d and %d hours",
115+
offsetMin, offsetMax)
116+
}
117+
offset /= 60
118+
92119
dt := new(Datetime)
93120
dt.time = t
94121
return dt, nil
@@ -105,8 +132,14 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
105132
var dt datetime
106133
dt.seconds = tm.Unix()
107134
dt.nsec = int32(tm.Nanosecond())
108-
dt.tzIndex = 0 // It is not implemented, see gh-163.
109-
dt.tzOffset = 0 // It is not implemented, see gh-163.
135+
136+
zone, offset := tm.Zone()
137+
if zone != NoTimezone {
138+
// The zone value already checked in NewDatetime() or
139+
// UnmarshalMsgpack() calls.
140+
dt.tzIndex = int16(timezoneToIndex[zone])
141+
}
142+
dt.tzOffset = int16(offset / 60)
110143

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

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

143-
tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
176+
tt := time.Unix(dt.seconds, int64(dt.nsec))
177+
178+
loc := noTimezoneLoc
179+
if dt.tzIndex != 0 || dt.tzOffset != 0 {
180+
zone := NoTimezone
181+
offset := int(dt.tzOffset) * 60
182+
183+
if dt.tzIndex != 0 {
184+
if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok {
185+
return fmt.Errorf("Unknown timezone index %d", dt.tzIndex)
186+
}
187+
zone = indexToTimezone[int(dt.tzIndex)]
188+
}
189+
loc = time.FixedZone(zone, offset)
190+
}
191+
tt = tt.In(loc)
192+
144193
dtp, err := NewDatetime(tt)
145194
if dtp != nil {
146195
*tm = *dtp

0 commit comments

Comments
 (0)