-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathzone.go
More file actions
347 lines (302 loc) · 8.48 KB
/
zone.go
File metadata and controls
347 lines (302 loc) · 8.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
// Package gotz provides direct access to IANA timezone data, exposing
// transitions, zone types, and POSIX TZ rules that Go's time.Location
// keeps private.
//
// Timezone data is compiled from the official IANA source and embedded
// in the package. Use Load to get a Zone by IANA name:
//
// z, err := gotz.Load("America/New_York")
// for _, t := range z.Transitions() {
// fmt.Println(t.When, z.Types()[t.Type].Abbrev)
// }
package gotz
import (
"archive/zip"
"bytes"
_ "embed"
"fmt"
"strings"
"sync"
"time"
)
//go:embed zoneinfo.zip
var zoneinfoZip []byte
var (
cache sync.Map // map[string]*Zone
zipOnce sync.Once
zipR *zip.Reader
zipErr error
lcOnce sync.Once
lcMap map[string]string // lowercase name → canonical name
)
func getZipReader() (*zip.Reader, error) {
zipOnce.Do(func() {
zipR, zipErr = zip.NewReader(bytes.NewReader(zoneinfoZip), int64(len(zoneinfoZip)))
})
return zipR, zipErr
}
// Zone represents a parsed IANA timezone with all raw data exposed.
type Zone struct {
name string
version int
types []ZoneType
transitions []Transition
leapSeconds []LeapSecond
extend *PosixTZ
extendRaw string
rawData []byte
}
// ZoneType describes a local time type (e.g., EST, EDT).
type ZoneType struct {
Abbrev string // abbreviated name
Offset int // seconds east of UTC
IsDST bool // true if daylight saving time
}
// Transition represents a moment when the timezone rule changes.
type Transition struct {
When int64 // Unix timestamp
Type int // index into Zone.Types()
IsStd bool // transition time is standard (not wall clock)
IsUT bool // transition time is UT (not local)
}
// LeapSecond represents a leap second record.
type LeapSecond struct {
When int64 // Unix timestamp
Correction int32 // cumulative correction
}
// Load returns a Zone for the given IANA timezone name.
// Results are cached; subsequent calls for the same name return the same *Zone.
func Load(name string) (*Zone, error) {
if name == "" || name == "UTC" {
return loadUTC(), nil
}
if v, ok := cache.Load(name); ok {
return v.(*Zone), nil
}
data, err := readFromZip(name)
if err != nil {
return nil, fmt.Errorf("gotz: zone %q: %w", name, err)
}
z, err := Parse(name, data)
if err != nil {
return nil, err
}
if actual, loaded := cache.LoadOrStore(name, z); loaded {
return actual.(*Zone), nil
}
return z, nil
}
// Parse parses TZif-format binary data into a Zone.
func Parse(name string, data []byte) (*Zone, error) {
return parseData(name, data)
}
func readFromZip(name string) ([]byte, error) {
r, err := getZipReader()
if err != nil {
return nil, err
}
for _, f := range r.File {
if f.Name == name {
rc, err := f.Open()
if err != nil {
return nil, err
}
defer rc.Close()
buf := make([]byte, f.UncompressedSize64)
n, err := rc.Read(buf)
if err != nil && err.Error() != "EOF" {
return nil, err
}
return buf[:n], nil
}
}
return nil, fmt.Errorf("not found in embedded data")
}
func buildLCMap() map[string]string {
r, err := getZipReader()
if err != nil {
return nil
}
m := make(map[string]string, len(r.File))
for _, f := range r.File {
m[strings.ToLower(f.Name)] = f.Name
}
return m
}
// Names returns all IANA timezone names available in the embedded database.
func Names() []string {
r, err := getZipReader()
if err != nil {
return nil
}
names := make([]string, 0, len(r.File))
for _, f := range r.File {
names = append(names, f.Name)
}
return names
}
// LoadInsensitive loads a timezone by name using case-insensitive matching.
func LoadInsensitive(name string) (*Zone, error) {
// Try exact match first.
z, err := Load(name)
if err == nil {
return z, nil
}
lcOnce.Do(func() { lcMap = buildLCMap() })
if canonical, ok := lcMap[strings.ToLower(name)]; ok {
return Load(canonical)
}
return nil, fmt.Errorf("gotz: zone %q: not found", name)
}
func loadUTC() *Zone {
if v, ok := cache.Load("UTC"); ok {
return v.(*Zone)
}
z := &Zone{
name: "UTC",
version: 2,
types: []ZoneType{{Abbrev: "UTC", Offset: 0, IsDST: false}},
}
if actual, loaded := cache.LoadOrStore("UTC", z); loaded {
return actual.(*Zone)
}
return z
}
// Name returns the IANA timezone name.
func (z *Zone) Name() string { return z.name }
// Version returns the TZif format version (1, 2, 3, or 4).
func (z *Zone) Version() int { return z.version }
// Types returns a copy of the zone type definitions.
func (z *Zone) Types() []ZoneType {
out := make([]ZoneType, len(z.types))
copy(out, z.types)
return out
}
// Transitions returns a copy of the transition records.
func (z *Zone) Transitions() []Transition {
out := make([]Transition, len(z.transitions))
copy(out, z.transitions)
return out
}
// LeapSeconds returns a copy of the leap second records.
func (z *Zone) LeapSeconds() []LeapSecond {
out := make([]LeapSecond, len(z.leapSeconds))
copy(out, z.leapSeconds)
return out
}
// Extend returns the parsed POSIX TZ rule for computing future transitions,
// or nil if the TZif file has no footer string.
func (z *Zone) Extend() *PosixTZ { return z.extend }
// ExtendRaw returns the raw POSIX TZ footer string.
func (z *Zone) ExtendRaw() string { return z.extendRaw }
// String returns the timezone name.
func (z *Zone) String() string { return z.name }
// TransitionsForRange returns all transitions in the half-open interval [start, end),
// combining stored transitions from the TZif file with dynamically generated
// ones from the POSIX TZ extend rule.
func (z *Zone) TransitionsForRange(start, end time.Time) []Transition {
startUnix := start.Unix()
endUnix := end.Unix()
var out []Transition
// Collect stored transitions in range.
for _, t := range z.transitions {
if t.When >= endUnix {
break
}
if t.When >= startUnix {
out = append(out, t)
}
}
// Generate transitions from the POSIX extend rule.
if z.extend == nil || !z.extend.HasDST() {
return out
}
// Determine last stored transition time.
var lastStored int64
if len(z.transitions) > 0 {
lastStored = z.transitions[len(z.transitions)-1].When
}
// Find the type indices for std and DST in z.types, or append synthetic ones.
stdIdx := z.findOrAddType(ZoneType{Abbrev: z.extend.StdAbbrev, Offset: z.extend.StdOffset, IsDST: false})
dstIdx := z.findOrAddType(ZoneType{Abbrev: z.extend.DSTAbbrev, Offset: z.extend.DSTOffset, IsDST: true})
startYear := start.Year()
endYear := end.Year()
for year := startYear; year <= endYear; year++ {
dstStart, dstEnd, ok := z.extend.TransitionsForYear(year)
if !ok {
continue
}
// DST start: std -> dst
if dstStart >= startUnix && dstStart < endUnix && dstStart > lastStored {
out = append(out, Transition{When: dstStart, Type: dstIdx})
}
// DST end: dst -> std
if dstEnd >= startUnix && dstEnd < endUnix && dstEnd > lastStored {
out = append(out, Transition{When: dstEnd, Type: stdIdx})
}
}
// Sort by timestamp (stored + generated may interleave for boundary years).
sortTransitions(out)
return out
}
// findOrAddType returns the index of a matching type in z.types,
// or appends it and returns the new index.
func (z *Zone) findOrAddType(zt ZoneType) int {
for i, t := range z.types {
if t.Abbrev == zt.Abbrev && t.Offset == zt.Offset && t.IsDST == zt.IsDST {
return i
}
}
z.types = append(z.types, zt)
return len(z.types) - 1
}
func sortTransitions(ts []Transition) {
// Simple insertion sort — transitions are nearly sorted already.
for i := 1; i < len(ts); i++ {
t := ts[i]
j := i
for j > 0 && ts[j-1].When > t.When {
ts[j] = ts[j-1]
j--
}
ts[j] = t
}
}
// Lookup returns the zone type in effect at the given time.
// It searches transitions and falls back to the POSIX TZ rule
// for times after the last transition.
func (z *Zone) Lookup(t time.Time) ZoneType {
unix := t.Unix()
if len(z.transitions) == 0 {
if len(z.types) > 0 {
return z.types[0]
}
return ZoneType{Abbrev: "UTC"}
}
// Binary search for the transition.
lo, hi := 0, len(z.transitions)
for lo < hi {
mid := lo + (hi-lo)/2
if z.transitions[mid].When <= unix {
lo = mid + 1
} else {
hi = mid
}
}
if lo == 0 {
// Before the first transition: use the first non-DST type,
// or type 0 if none.
for _, zt := range z.types {
if !zt.IsDST {
return zt
}
}
return z.types[0]
}
if lo == len(z.transitions) && z.extend != nil {
// After the last transition: use the POSIX TZ rule.
abbrev, offset, isDST := z.extend.Lookup(unix)
return ZoneType{Abbrev: abbrev, Offset: offset, IsDST: isDST}
}
return z.types[z.transitions[lo-1].Type]
}