From a1946451417d87feed0fa4fcf3f353c8f2cdfa09 Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Tue, 25 Jun 2024 23:04:47 +0200 Subject: [PATCH] feat: improve precision first use rtree and afterwards check if the coordinates match a polygon --- db/rtree.go | 90 +++- db/rtree_test.go | 69 ++- db/testdata/coordinates.json | 63 ++- db/testdata/zones.json | 992 ----------------------------------- 4 files changed, 141 insertions(+), 1073 deletions(-) delete mode 100644 db/testdata/zones.json diff --git a/db/rtree.go b/db/rtree.go index 5be4e70..104edf9 100644 --- a/db/rtree.go +++ b/db/rtree.go @@ -11,8 +11,8 @@ import ( ) type Geo2TzRTreeIndex struct { - land rtree.RTreeG[string] - sea rtree.RTreeG[string] + land rtree.RTreeG[timezoneGeo] + sea rtree.RTreeG[timezoneGeo] size int } @@ -22,13 +22,13 @@ func IsOcean(label string) bool { } // Insert adds a new timezone bounding box to the index -func (g *Geo2TzRTreeIndex) Insert(min, max [2]float64, label string) { +func (g *Geo2TzRTreeIndex) Insert(min, max [2]float64, element timezoneGeo) { g.size++ - if IsOcean(label) { - g.sea.Insert(min, max, label) + if IsOcean(element.Name) { + g.sea.Insert(min, max, element) return } - g.land.Insert(min, max, label) + g.land.Insert(min, max, element) } func NewGeo2TzRTreeIndexFromGeoJSON(geoJSONPath string) (*Geo2TzRTreeIndex, error) { @@ -45,22 +45,7 @@ func NewGeo2TzRTreeIndexFromGeoJSON(geoJSONPath string) (*Geo2TzRTreeIndex, erro // this function will add the timezone polygons to the shape index iter := func(tz timezoneGeo) error { for _, p := range tz.Polygons { - minLat, minLng, maxLat, maxLng := p.Vertices[0].lat, p.Vertices[0].lng, p.Vertices[0].lat, p.Vertices[0].lng - for _, v := range p.Vertices { - if v.lng < minLng { - minLng = v.lng - } - if v.lng > maxLng { - maxLng = v.lng - } - if v.lat < minLat { - minLat = v.lat - } - if v.lat > maxLat { - maxLat = v.lat - } - } - gri.Insert([2]float64{minLat, minLng}, [2]float64{maxLat, maxLng}, tz.Name) + gri.Insert([2]float64{p.MinLat, p.MinLng}, [2]float64{p.MaxLat, p.MaxLng}, tz) } return nil } @@ -80,22 +65,33 @@ func NewGeo2TzRTreeIndexFromGeoJSON(geoJSONPath string) (*Geo2TzRTreeIndex, erro // if the timezone is not found, it returns an error // It first searches in the land index, if not found, it searches in the sea index func (g *Geo2TzRTreeIndex) Lookup(lat, lng float64) (tzID string, err error) { - + // search the land index g.land.Search( [2]float64{lat, lng}, [2]float64{lat, lng}, - func(min, max [2]float64, label string) bool { - tzID = label + func(min, max [2]float64, data timezoneGeo) bool { + for _, p := range data.Polygons { + if isPointInPolygonPIP(vertex{lat, lng}, p) { + tzID = data.Name + return false + } + } return true }, ) if tzID == "" { + // if not found, search the sea index g.sea.Search( [2]float64{lat, lng}, [2]float64{lat, lng}, - func(min, max [2]float64, label string) bool { - tzID = label + func(min, max [2]float64, data timezoneGeo) bool { + for _, p := range data.Polygons { + if isPointInPolygonPIP(vertex{lat, lng}, p) { + tzID = data.Name + return false + } + } return true }, ) @@ -111,6 +107,23 @@ func (g *Geo2TzRTreeIndex) Size() int { return g.size } +func isPointInPolygonPIP(point vertex, polygon polygon) bool { + oddNodes := false + n := len(polygon.Vertices) + for i := 0; i < n; i++ { + j := (i + 1) % n + vi := polygon.Vertices[i] + vj := polygon.Vertices[j] + // Check if the point lies on an edge of the polygon (including horizontal) + if (vi.lng == vj.lng && vi.lng == point.lng && point.lat >= min(vi.lat, vj.lat) && point.lat <= max(vi.lat, vj.lat)) || + ((vi.lat < point.lat && point.lat <= vj.lat) || (vj.lat < point.lat && point.lat <= vi.lat)) && + (point.lng < (vj.lng-vi.lng)*(point.lat-vi.lat)/(vj.lat-vi.lat)+vi.lng) { + oddNodes = !oddNodes + } + } + return oddNodes +} + /* GeoJSON processing */ @@ -119,6 +132,10 @@ GeoJSON processing // with a list of vertices [lat, lng] type polygon struct { Vertices []vertex + MaxLat float64 + MinLat float64 + MaxLng float64 + MinLng float64 } type vertex struct { @@ -137,6 +154,25 @@ type GeoJSONFeature struct { } func (p *polygon) AddVertex(lat, lng float64) { + if len(p.Vertices) == 0 { + p.MaxLat = lat + p.MinLat = lat + p.MaxLng = lng + p.MinLng = lng + } else { + if lat > p.MaxLat { + p.MaxLat = lat + } + if lat < p.MinLat { + p.MinLat = lat + } + if lng > p.MaxLng { + p.MaxLng = lng + } + if lng < p.MinLng { + p.MinLng = lng + } + } p.Vertices = append(p.Vertices, vertex{lat, lng}) } diff --git a/db/rtree_test.go b/db/rtree_test.go index 1765fd0..5d9f0de 100644 --- a/db/rtree_test.go +++ b/db/rtree_test.go @@ -14,7 +14,7 @@ func TestGeo2TzTreeIndex_LookupZone(t *testing.T) { Tz string `json:"tz"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` - HasError bool `json:"err,omitempty"` + NotFound bool `json:"not_found,omitempty"` } // load the database @@ -22,21 +22,6 @@ func TestGeo2TzTreeIndex_LookupZone(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, gsi.Size()) - // load the timezone references - var tzZones map[string]struct { - Zone string `json:"zone"` - UtcOffset float32 `json:"utc_offset_h"` - Dst struct { - Start string `json:"start"` - End string `json:"end"` - Zone string `json:"zone"` - UtcOffset float32 `json:"utc_offset_h"` - } `json:"dst,omitempty"` - } - err = helpers.LoadJSON("testdata/zones.json", &tzZones) - assert.NoError(t, err) - assert.NotEmpty(t, tzZones) - // load the coordinates err = helpers.LoadJSON("testdata/coordinates.json", &tests) assert.NoError(t, err) @@ -45,31 +30,43 @@ func TestGeo2TzTreeIndex_LookupZone(t *testing.T) { for _, tt := range tests { t.Run(tt.Tz, func(t *testing.T) { got, err := gsi.Lookup(tt.Lat, tt.Lon) - assert.NoError(t, err) - - if tt.HasError { - t.Skip("skipping test as it is expected to fail (know error)") - } - - // for oceans do exact match - if IsOcean(got) { - assert.Equal(t, tt.Tz, got, "expected %s to be %s for https://www.google.com/maps/@%v,%v,12z", tt.Tz, got, tt.Lat, tt.Lon) + if tt.NotFound { + assert.ErrorIs(t, err, ErrNotFound) return } + assert.NoError(t, err) + assert.Equal(t, got, tt.Tz, "expected %s to be %s for https://www.google.com/maps/@%v,%v,12z", tt.Tz, got, tt.Lat, tt.Lon) + }) + } +} - // get the zone for the expected timezone - zoneExpected, ok := tzZones[tt.Tz] - assert.True(t, ok, "timezone %s not found in zones.json", tt.Tz) +// benchmark the lookup function +func BenchmarkGeo2TzTreeIndex_LookupZone(b *testing.B) { + // load the database + gsi, err := NewGeo2TzRTreeIndexFromGeoJSON("../tzdata/timezones.zip") + assert.NoError(b, err) + assert.NotEmpty(b, gsi.Size()) - // get the reference timezone for the expected timezone - zoneGot, ok := tzZones[got] - assert.True(t, ok, "timezone %s not found in zones.json", got) + // load the coordinates + var tests []struct { + Tz string `json:"tz"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + NotFound bool `json:"not_found,omitempty"` + } + err = helpers.LoadJSON("testdata/coordinates.json", &tests) + assert.NoError(b, err) + assert.NotEmpty(b, tests) - if !ok { - assert.Equal(t, zoneExpected.Zone, got, "expected %s (%s) to be %s (%s) for https://www.google.com/maps/@%v,%v,12z", tt.Tz, zoneExpected.Zone, got, zoneGot.Zone, tt.Lat, tt.Lon) - } else { - assert.Equal(t, zoneExpected.Zone, zoneGot.Zone, "expected %s (%s) to be %s (%s) for https://www.google.com/maps/@%v,%v,12z", tt.Tz, zoneExpected.Zone, got, zoneGot.Zone, tt.Lat, tt.Lon) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tt := range tests { + _, err := gsi.Lookup(tt.Lat, tt.Lon) + if tt.NotFound { + assert.ErrorIs(b, err, ErrNotFound) + return } - }) + assert.NoError(b, err) + } } } diff --git a/db/testdata/coordinates.json b/db/testdata/coordinates.json index ce9cc25..fcf5fa6 100755 --- a/db/testdata/coordinates.json +++ b/db/testdata/coordinates.json @@ -1,4 +1,20 @@ [ + { + "lat": 50, + "lon": 40, + "tz": "Europe/Moscow", + "note": "" + }, + { + "lat": 44.50144, + "lon": -88.0625889, + "tz": "America/Chicago" + }, + { + "lat": 43.74206, + "lon": -87.73337, + "tz": "America/Chicago" + }, { "lat": 90, "lon": 0, @@ -9,7 +25,8 @@ "lat": 43.42582, "lon": 11.831443, "tz": "Europe/Rome", - "note": "https://github.com/noandrea/geo2tz/issues/22" + "note": "https://github.com/noandrea/geo2tz/issues/22", + "not_found": true }, { "lat": 32.7767, "lon": -96.797, "tz": "America/Chicago" }, { "lat": 34.0522, "lon": -118.2437, "tz": "America/Los_Angeles" }, @@ -17,33 +34,43 @@ { "lat": 51.5074, "lon": -0.1278, "tz": "Europe/London" }, { "lat": 35.6895, "lon": 139.6917, "tz": "Asia/Tokyo" }, { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, - { "lat": -33.8688, "lon": 151.2093, "tz": "Australia/Sydney" }, + { + "lat": -33.8688, + "lon": 151.2093, + "tz": "Australia/Sydney" + }, { "lat": 19.4326, "lon": -99.1332, "tz": "America/Mexico_City" }, { "lat": 39.9042, "lon": 116.4074, "tz": "Asia/Shanghai" }, - { "lat": 28.6139, "lon": 77.209, "tz": "Asia/Kolkata", "err": true }, + { "lat": 28.6139, "lon": 77.209, "tz": "Asia/Kolkata" }, { "lat": -23.5505, "lon": -46.6333, "tz": "America/Sao_Paulo" }, { "lat": -34.6037, "lon": -58.3816, "tz": "America/Argentina/Buenos_Aires" }, - { "lat": -26.2041, "lon": 28.0473, "tz": "Africa/Johannesburg", "err": true }, + { "lat": -26.2041, "lon": 28.0473, "tz": "Africa/Johannesburg" }, { "lat": 41.9028, "lon": 12.4964, "tz": "Europe/Rome" }, { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, { "lat": 31.2304, "lon": 121.4737, "tz": "Asia/Shanghai" }, - { "lat": 22.3964, "lon": 114.1095, "tz": "Asia/Hong_Kong", "err": true }, + { "lat": 22.3964, "lon": 114.1095, "tz": "Asia/Hong_Kong" }, { "lat": -1.2921, "lon": 36.8219, "tz": "Africa/Nairobi" }, - { "lat": 33.8688, "lon": 151.2093, "tz": "Australia/Sydney", "err": true }, + { + "lat": 33.8688, + "lon": 151.2093, + "tz": "Australia/Sydney", + "not_found": true, + "note": "it's in the middle of the ocean" + }, { "lat": 50.1109, "lon": 8.6821, "tz": "Europe/Berlin" }, { "lat": 40.4168, "lon": -3.7038, "tz": "Europe/Madrid" }, { "lat": 45.4642, "lon": 9.19, "tz": "Europe/Rome" }, { "lat": 43.6532, "lon": -79.3832, "tz": "America/Toronto" }, { "lat": 37.9838, "lon": 23.7275, "tz": "Europe/Athens" }, - { "lat": 1.3521, "lon": 103.8198, "tz": "Asia/Singapore", "err": true }, + { "lat": 1.3521, "lon": 103.8198, "tz": "Asia/Singapore" }, { "lat": 19.076, "lon": 72.8777, "tz": "Asia/Kolkata" }, { "lat": -33.9249, "lon": 18.4241, "tz": "Africa/Johannesburg" }, { "lat": 40.7306, "lon": -73.9352, "tz": "America/New_York" }, { "lat": 35.6762, "lon": 139.6503, "tz": "Asia/Tokyo" }, { "lat": 34.0522, "lon": -118.244, "tz": "America/Los_Angeles" }, { "lat": 55.6761, "lon": 12.5683, "tz": "Europe/Copenhagen" }, - { "lat": 25.276987, "lon": 55.296249, "tz": "Asia/Dubai", "err": true }, + { "lat": 25.276987, "lon": 55.296249, "tz": "Asia/Dubai" }, { "lat": 52.3676, "lon": 4.9041, "tz": "Europe/Amsterdam" }, { "lat": 41.0082, "lon": 28.9784, "tz": "Europe/Istanbul" }, { "lat": 59.3293, "lon": 18.0686, "tz": "Europe/Stockholm" }, @@ -56,18 +83,18 @@ { "lat": -22.9068, "lon": -43.1729, "tz": "America/Sao_Paulo" }, { "lat": -34.9285, "lon": 138.6007, "tz": "Australia/Adelaide" }, { "lat": 37.5665, "lon": 126.978, "tz": "Asia/Seoul" }, - { "lat": 13.7563, "lon": 100.5018, "tz": "Asia/Bangkok", "err": true }, - { "lat": 22.5726, "lon": 88.3639, "tz": "Asia/Kolkata", "err": true }, + { "lat": 13.7563, "lon": 100.5018, "tz": "Asia/Bangkok" }, + { "lat": 22.5726, "lon": 88.3639, "tz": "Asia/Kolkata" }, { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, { "lat": 48.2082, "lon": 16.3738, "tz": "Europe/Vienna" }, { "lat": 52.2297, "lon": 21.0122, "tz": "Europe/Warsaw" }, { "lat": 50.4501, "lon": 30.5234, "tz": "Europe/Kyiv" }, - { "lat": 49.8397, "lon": 24.0297, "tz": "Europe/Kyiv", "err": true }, + { "lat": 49.8397, "lon": 24.0297, "tz": "Europe/Kyiv" }, { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, { "lat": 34.6937, "lon": 135.5023, "tz": "Asia/Tokyo" }, { "lat": 48.1351, "lon": 11.582, "tz": "Europe/Berlin" }, { "lat": 40.4168, "lon": -3.7038, "tz": "Europe/Madrid" }, - { "lat": 1.3521, "lon": 103.8198, "tz": "Asia/Singapore", "err": true }, + { "lat": 1.3521, "lon": 103.8198, "tz": "Asia/Singapore" }, { "lat": 50.0755, "lon": 14.4378, "tz": "Europe/Prague" }, { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, { "lat": 31.2304, "lon": 121.4737, "tz": "Asia/Shanghai" }, @@ -79,18 +106,18 @@ { "lat": 30.0444, "lon": 31.2357, "tz": "Africa/Cairo" }, { "lat": -17.8249, "lon": 31.053, "tz": "Africa/Harare" }, { "lat": 14.5995, "lon": 120.9842, "tz": "Asia/Manila" }, - { "lat": 31.7683, "lon": 35.2137, "tz": "Asia/Jerusalem", "err": true }, + { "lat": 31.7683, "lon": 35.2137, "tz": "Asia/Jerusalem" }, { "lat": -22.9068, "lon": -43.1729, "tz": "America/Sao_Paulo" }, { "lat": 12.9716, "lon": 77.5946, "tz": "Asia/Kolkata" }, { "lat": -1.2921, "lon": 36.8219, "tz": "Africa/Nairobi" }, { "lat": 41.9028, "lon": 12.4964, "tz": "Europe/Rome" }, { "lat": 60.1695, "lon": 24.9354, "tz": "Europe/Helsinki" }, { "lat": 45.4215, "lon": -75.6972, "tz": "America/Toronto" }, - { "lat": -25.2744, "lon": 133.7751, "tz": "Australia/Adelaide" }, + { "lat": -25.2744, "lon": 133.7751, "tz": "Australia/Darwin" }, { "lat": -33.8688, "lon": 151.2093, "tz": "Australia/Sydney" }, { "lat": 50.8503, "lon": 4.3517, "tz": "Europe/Brussels" }, { "lat": 38.7223, "lon": -9.1393, "tz": "Europe/Lisbon" }, - { "lat": 1.29027, "lon": 103.851959, "tz": "Asia/Singapore", "err": true }, + { "lat": 1.29027, "lon": 103.851959, "tz": "Asia/Singapore" }, { "lat": 35.6895, "lon": 139.6917, "tz": "Asia/Tokyo" }, { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, @@ -100,10 +127,10 @@ { "lat": 55.6761, "lon": 12.5683, "tz": "Europe/Copenhagen" }, { "lat": 19.4326, "lon": -99.1332, "tz": "America/Mexico_City" }, { "lat": 39.9042, "lon": 116.4074, "tz": "Asia/Shanghai" }, - { "lat": 28.6139, "lon": 77.209, "tz": "Asia/Kolkata", "err": true }, + { "lat": 28.6139, "lon": 77.209, "tz": "Asia/Kolkata" }, { "lat": -23.5505, "lon": -46.6333, "tz": "America/Sao_Paulo" }, { "lat": -34.6037, "lon": -58.3816, "tz": "America/Argentina/Buenos_Aires" }, - { "lat": -26.2041, "lon": 28.0473, "tz": "Africa/Johannesburg", "err": true }, + { "lat": -26.2041, "lon": 28.0473, "tz": "Africa/Johannesburg" }, { "lat": 41.9028, "lon": 12.4964, "tz": "Europe/Rome" }, { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, @@ -111,7 +138,7 @@ { "lat": 37.5665, "lon": 126.978, "tz": "Asia/Seoul" }, { "lat": -34.6037, "lon": -58.3816, "tz": "America/Argentina/Buenos_Aires" }, { "lat": -23.5505, "lon": -46.6333, "tz": "America/Sao_Paulo" }, - { "lat": 22.3964, "lon": 114.1095, "tz": "Asia/Hong_Kong", "err": true }, + { "lat": 22.3964, "lon": 114.1095, "tz": "Asia/Hong_Kong" }, { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, { "lat": 39.9042, "lon": 116.4074, "tz": "Asia/Shanghai" }, { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, diff --git a/db/testdata/zones.json b/db/testdata/zones.json deleted file mode 100644 index e1cf2d9..0000000 --- a/db/testdata/zones.json +++ /dev/null @@ -1,992 +0,0 @@ -{ - "Africa/Abidjan": { - "zone": "GMT", - "utc_offset_h": 0 - }, - "Africa/Accra": { - "zone": "GMT", - "utc_offset_h": 0 - }, - "Africa/Addis_Ababa": { - "zone": "EAT", - "utc_offset_h": 3 - }, - "Africa/Algiers": { - "zone": "CET", - "utc_offset_h": 1 - }, - "Africa/Cairo": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Friday in April", - "end": "last Thursday in September", - "utc_offset_h": 3 - } - }, - "Africa/Cape_Town": { - "zone": "SAST", - "utc_offset_h": 2 - }, - "Africa/Casablanca": { - "zone": "WET", - "utc_offset_h": 0, - "dst": { - "zone": "WEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 1 - } - }, - "Africa/Ceuta": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Africa/Johannesburg": { - "zone": "SAST", - "utc_offset_h": 2 - }, - "Africa/Tripoli": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Friday in March", - "end": "last Thursday in October", - "utc_offset_h": 3 - } - }, - "America/Adak": { - "zone": "HST", - "utc_offset_h": -10, - "dst": { - "zone": "HDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -9 - } - }, - "Africa/Harare": { - "zone": "CAT", - "utc_offset_h": 2 - }, - "America/Vancouver": { - "zone": "PST", - "utc_offset_h": -8, - "dst": { - "zone": "PDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -7 - } - }, - "Africa/Gaborone": { - "zone": "CAT", - "utc_offset_h": 2 - }, - "Africa/Lusaka": { - "zone": "CAT", - "utc_offset_h": 2 - }, - "Asia/Tehran": { - "zone": "IRST", - "utc_offset_h": 3.5, - "dst": { - "zone": "IRDT", - "start": "21st March", - "end": "21st September", - "utc_offset_h": 4.5 - } - }, - "Australia/Melbourne": { - "zone": "AEST", - "utc_offset_h": 10, - "dst": { - "zone": "AEDT", - "start": "first Sunday in October", - "end": "first Sunday in April", - "utc_offset_h": 11 - } - }, - "Africa/Nairobi": { - "zone": "EAT", - "utc_offset_h": 3 - }, - "America/Toronto": { - "zone": "EST", - "utc_offset_h": -5, - "dst": { - "zone": "EDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -4 - } - }, - "America/Anchorage": { - "zone": "AKST", - "utc_offset_h": -9, - "dst": { - "zone": "AKDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -8 - } - }, - "America/Argentina/Buenos_Aires": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/Catamarca": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/ComodRivadavia": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/Cordoba": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/Jujuy": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/La_Rioja": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/Mendoza": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/Rio_Gallegos": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/Salta": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/San_Juan": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/San_Luis": { - "zone": "WART", - "utc_offset_h": -3 - }, - "America/Argentina/Tucuman": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Argentina/Ushuaia": { - "zone": "ART", - "utc_offset_h": -3 - }, - "America/Asuncion": { - "zone": "PYT", - "utc_offset_h": -4, - "dst": { - "zone": "PYST", - "start": "first Sunday in October", - "end": "fourth Sunday in March", - "utc_offset_h": -3 - } - }, - "America/Atikokan": { - "zone": "EST", - "utc_offset_h": -5 - }, - "America/Bahia_Banderas": { - "zone": "CST", - "utc_offset_h": -6, - "dst": { - "zone": "CDT", - "start": "first Sunday in April", - "end": "last Sunday in October", - "utc_offset_h": -5 - } - }, - "America/Barbados": { - "zone": "AST", - "utc_offset_h": -4 - }, - "America/Belize": { - "zone": "CST", - "utc_offset_h": -6 - }, - "America/Boa_Vista": { - "zone": "AMT", - "utc_offset_h": -4 - }, - "America/Bogota": { - "zone": "COT", - "utc_offset_h": -5 - }, - "America/Caracas": { - "zone": "VET", - "utc_offset_h": -4.5 - }, - "America/Chicago": { - "zone": "CST", - "utc_offset_h": -6, - "dst": { - "zone": "CDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -5 - } - }, - "America/Denver": { - "zone": "MST", - "utc_offset_h": -7, - "dst": { - "zone": "MDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -6 - } - }, - "America/Detroit": { - "zone": "EST", - "utc_offset_h": -5, - "dst": { - "zone": "EDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -4 - } - }, - "America/Edmonton": { - "zone": "MST", - "utc_offset_h": -7, - "dst": { - "zone": "MDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -6 - } - }, - "America/El_Salvador": { - "zone": "CST", - "utc_offset_h": -6 - }, - "America/Fortaleza": { - "zone": "BRT", - "utc_offset_h": -3 - }, - "America/Glace_Bay": { - "zone": "AST", - "utc_offset_h": -4, - "dst": { - "zone": "ADT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -3 - } - }, - "America/Godthab": { - "zone": "WGT", - "utc_offset_h": -3, - "dst": { - "zone": "WGST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": -2 - } - }, - "America/Grand_Turk": { - "zone": "EST", - "utc_offset_h": -5, - "dst": { - "zone": "EDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -4 - } - }, - "America/Guatemala": { - "zone": "CST", - "utc_offset_h": -6 - }, - "America/Halifax": { - "zone": "AST", - "utc_offset_h": -4, - "dst": { - "zone": "ADT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -3 - } - }, - "America/Havana": { - "zone": "CST", - "utc_offset_h": -5, - "dst": { - "zone": "CDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -4 - } - }, - "America/Indiana/Indianapolis": { - "zone": "EST", - "utc_offset_h": -5, - "dst": { - "zone": "EDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -4 - } - }, - "America/Indiana/Knox": { - "zone": "CST", - "utc_offset_h": -6, - "dst": { - "zone": "CDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -5 - } - }, - "America/Jamaica": { - "zone": "EST", - "utc_offset_h": -5 - }, - "America/Los_Angeles": { - "zone": "PST", - "utc_offset_h": -8, - "dst": { - "zone": "PDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -7 - } - }, - "America/Managua": { - "zone": "CST", - "utc_offset_h": -6 - }, - "America/Mexico_City": { - "zone": "CST", - "utc_offset_h": -6, - "dst": { - "zone": "CDT", - "start": "first Sunday in April", - "end": "last Sunday in October", - "utc_offset_h": -5 - } - }, - "America/Montevideo": { - "zone": "UYT", - "utc_offset_h": -3 - }, - "America/New_York": { - "zone": "EST", - "utc_offset_h": -5, - "dst": { - "zone": "EDT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -4 - } - }, - "America/Panama": { - "zone": "EST", - "utc_offset_h": -5 - }, - "America/Phoenix": { - "zone": "MST", - "utc_offset_h": -7 - }, - "America/Santiago": { - "zone": "CLT", - "utc_offset_h": -4, - "dst": { - "zone": "CLST", - "start": "first Sunday in September", - "end": "first Sunday in April", - "utc_offset_h": -3 - } - }, - "America/Sao_Paulo": { - "zone": "BRT", - "utc_offset_h": -3, - "dst": { - "zone": "BRST", - "start": "third Sunday in October", - "end": "third Sunday in February", - "utc_offset_h": -2 - } - }, - "Asia/Baghdad": { - "zone": "AST", - "utc_offset_h": 3 - }, - "Asia/Baku": { - "zone": "AZT", - "utc_offset_h": 4, - "dst": { - "zone": "AZST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 5 - } - }, - "Asia/Bangkok": { - "zone": "ICT", - "utc_offset_h": 7 - }, - "Asia/Beirut": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Asia/Dhaka": { - "zone": "BST", - "utc_offset_h": 6 - }, - "Asia/Dubai": { - "zone": "GST", - "utc_offset_h": 4 - }, - "Asia/Hong_Kong": { - "zone": "HKT", - "utc_offset_h": 8 - }, - "Asia/Ho_Chi_Minh": { - "zone": "ICT", - "utc_offset_h": 7 - }, - "Asia/Jakarta": { - "zone": "WIB", - "utc_offset_h": 7 - }, - "Asia/Jerusalem": { - "zone": "IST", - "utc_offset_h": 2, - "dst": { - "zone": "IDT", - "start": "last Friday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Asia/Kolkata": { - "zone": "IST", - "utc_offset_h": 5.5 - }, - "Asia/Kuala_Lumpur": { - "zone": "MYT", - "utc_offset_h": 8 - }, - "Asia/Kuwait": { - "zone": "AST", - "utc_offset_h": 3 - }, - "Asia/Manila": { - "zone": "PHT", - "utc_offset_h": 8 - }, - "Asia/Riyadh": { - "zone": "AST", - "utc_offset_h": 3 - }, - "Asia/Seoul": { - "zone": "KST", - "utc_offset_h": 9 - }, - "Asia/Shanghai": { - "zone": "CST", - "utc_offset_h": 8 - }, - "Asia/Singapore": { - "zone": "SGT", - "utc_offset_h": 8 - }, - "Asia/Tokyo": { - "zone": "JST", - "utc_offset_h": 9 - }, - "Asia/Yangon": { - "zone": "MMT", - "utc_offset_h": 6.5 - }, - "Atlantic/Azores": { - "zone": "AZOT", - "utc_offset_h": -1, - "dst": { - "zone": "AZOST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 0 - } - }, - "Atlantic/Bermuda": { - "zone": "AST", - "utc_offset_h": -4, - "dst": { - "zone": "ADT", - "start": "second Sunday in March", - "end": "first Sunday in November", - "utc_offset_h": -3 - } - }, - "Atlantic/Canary": { - "zone": "WET", - "utc_offset_h": 0, - "dst": { - "zone": "WEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 1 - } - }, - "Atlantic/Cape_Verde": { - "zone": "CVT", - "utc_offset_h": -1 - }, - "Atlantic/Reykjavik": { - "zone": "GMT", - "utc_offset_h": 0 - }, - "Australia/Adelaide": { - "zone": "ACST", - "utc_offset_h": 9.5, - "dst": { - "zone": "ACDT", - "start": "first Sunday in October", - "end": "first Sunday in April", - "utc_offset_h": 10.5 - } - }, - "Australia/Brisbane": { - "zone": "AEST", - "utc_offset_h": 10 - }, - "Australia/Darwin": { - "zone": "ACST", - "utc_offset_h": 9.5 - }, - "Australia/Hobart": { - "zone": "AEST", - "utc_offset_h": 10, - "dst": { - "zone": "AEDT", - "start": "first Sunday in October", - "end": "first Sunday in April", - "utc_offset_h": 11 - } - }, - "Australia/Perth": { - "zone": "AWST", - "utc_offset_h": 8 - }, - "Australia/Sydney": { - "zone": "AEST", - "utc_offset_h": 10, - "dst": { - "zone": "AEDT", - "start": "first Sunday in October", - "end": "first Sunday in April", - "utc_offset_h": 11 - } - }, - "Europe/Amsterdam": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Athens": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Belgrade": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Berlin": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Brussels": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Bucharest": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Budapest": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Chisinau": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Copenhagen": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Dublin": { - "zone": "GMT", - "utc_offset_h": 0, - "dst": { - "zone": "IST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 1 - } - }, - "Europe/Helsinki": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Istanbul": { - "zone": "TRT", - "utc_offset_h": 3 - }, - "Europe/Kaliningrad": { - "zone": "EET", - "utc_offset_h": 2 - }, - "Europe/Kyiv": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Lisbon": { - "zone": "WET", - "utc_offset_h": 0, - "dst": { - "zone": "WEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 1 - } - }, - "Europe/London": { - "zone": "GMT", - "utc_offset_h": 0, - "dst": { - "zone": "BST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 1 - } - }, - "Europe/Luxembourg": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Madrid": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Minsk": { - "zone": "MSK", - "utc_offset_h": 3 - }, - "Europe/Monaco": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Moscow": { - "zone": "MSK", - "utc_offset_h": 3 - }, - "Europe/Oslo": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Paris": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Prague": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Riga": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Rome": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Samara": { - "zone": "SAMT", - "utc_offset_h": 4 - }, - "Europe/Simferopol": { - "zone": "MSK", - "utc_offset_h": 3 - }, - "Europe/Sofia": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Stockholm": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Tallinn": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Tirane": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Vienna": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Vilnius": { - "zone": "EET", - "utc_offset_h": 2, - "dst": { - "zone": "EEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 3 - } - }, - "Europe/Warsaw": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Europe/Zurich": { - "zone": "CET", - "utc_offset_h": 1, - "dst": { - "zone": "CEST", - "start": "last Sunday in March", - "end": "last Sunday in October", - "utc_offset_h": 2 - } - }, - "Pacific/Auckland": { - "zone": "NZST", - "utc_offset_h": 12, - "dst": { - "zone": "NZDT", - "start": "last Sunday in September", - "end": "first Sunday in April", - "utc_offset_h": 13 - } - }, - "Pacific/Chatham": { - "zone": "CHAST", - "utc_offset_h": 12.75, - "dst": { - "zone": "CHADT", - "start": "last Sunday in September", - "end": "first Sunday in April", - "utc_offset_h": 13.75 - } - }, - "Pacific/Easter": { - "zone": "EAST", - "utc_offset_h": -6, - "dst": { - "zone": "EASST", - "start": "first Sunday in September", - "end": "first Sunday in April", - "utc_offset_h": -5 - } - }, - "Pacific/Fiji": { - "zone": "FJT", - "utc_offset_h": 12, - "dst": { - "zone": "FJST", - "start": "second Sunday in November", - "end": "second Sunday in January", - "utc_offset_h": 13 - } - }, - "Pacific/Guam": { - "zone": "ChST", - "utc_offset_h": 10 - }, - "Pacific/Honolulu": { - "zone": "HST", - "utc_offset_h": -10 - }, - "Pacific/Marquesas": { - "zone": "MART", - "utc_offset_h": -9.5 - }, - "Pacific/Pago_Pago": { - "zone": "SST", - "utc_offset_h": -11 - }, - "Pacific/Tahiti": { - "zone": "TAHT", - "utc_offset_h": -10 - } -}