Skip to content

Commit

Permalink
feat: improve timezone lookup and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
noandrea committed Jun 24, 2024
1 parent 0691346 commit 5c00920
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 880 deletions.
58 changes: 40 additions & 18 deletions db/rtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,26 @@ import (
)

type Geo2TzRTreeIndex struct {
rtree.RTreeG[string]
land rtree.RTreeG[string]
sea rtree.RTreeG[string]
size int
}

// IsOcean checks if the timezone is for oceans
func IsOcean(label string) bool {
return strings.HasPrefix(label, "Etc/GMT")
}

// Insert adds a new timezone bounding box to the index
func (g *Geo2TzRTreeIndex) Insert(min, max [2]float64, label string) {
g.size++
if IsOcean(label) {
g.sea.Insert(min, max, label)
return
}
g.land.Insert(min, max, label)
}

func NewGeo2TzRTreeIndexFromGeoJSON(geoJSONPath string) (*Geo2TzRTreeIndex, error) {
// open the zip file
zipFile, err := zip.OpenReader(geoJSONPath)
Expand Down Expand Up @@ -45,11 +61,6 @@ func NewGeo2TzRTreeIndexFromGeoJSON(geoJSONPath string) (*Geo2TzRTreeIndex, erro
}
}
gri.Insert([2]float64{minLat, minLng}, [2]float64{maxLat, maxLng}, tz.Name)
if tz.Name == "Europe/Madrid" {
a := tz.Name
_ = a
}
gri.size++
}
return nil
}
Expand All @@ -65,20 +76,31 @@ func NewGeo2TzRTreeIndexFromGeoJSON(geoJSONPath string) (*Geo2TzRTreeIndex, erro
return gri, nil
}

// Lookup returns the timezone ID for a given latitude and longitude
// 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) (string, error) {

var tzID string
g.Search([2]float64{lat, lng}, [2]float64{lat, lng}, func(min, max [2]float64, label string) bool {
tzID = label
return true
})

// g.Nearby(
// rtree.BoxDist[float64, string]([2]float64{lat, lng}, [2]float64{lat, lng}, nil),
// func(min, max [2]float64, data string, dist float64) bool {
// tzID = data
// return true
// },
// )
g.land.Search(
[2]float64{lat, lng},
[2]float64{lat, lng},
func(min, max [2]float64, label string) bool {
tzID = label
return true
},
)

if len(tzID) == 0 {
g.sea.Search(
[2]float64{lat, lng},
[2]float64{lat, lng},
func(min, max [2]float64, label string) bool {
tzID = label
return true
},
)
}

if len(tzID) == 0 {
return "", ErrNotFound
Expand Down
49 changes: 36 additions & 13 deletions db/rtree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import (
"github.com/stretchr/testify/assert"
)

func TestGeo2TzTreeIndex_Lookup(t *testing.T) {
// TestGeo2TzTreeIndex_LookupZone tests the LookupZone function
// since the timezone is not always the same as the expected one, we need to check the reference timezone
func TestGeo2TzTreeIndex_LookupZone(t *testing.T) {
var tests []struct {
Tz string `json:"tz"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Tz string `json:"tz"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
HasError bool `json:"err,omitempty"`
}

// load the database
Expand All @@ -20,10 +23,19 @@ func TestGeo2TzTreeIndex_Lookup(t *testing.T) {
assert.NotEmpty(t, gsi.Size())

// load the timezone references
var tz2etc map[string]string
err = helpers.LoadJSON("testdata/tz2etc.json", &tz2etc)
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, tz2etc)
assert.NotEmpty(t, tzZones)

// load the coordinates
err = helpers.LoadJSON("testdata/coordinates.json", &tests)
Expand All @@ -35,17 +47,28 @@ func TestGeo2TzTreeIndex_Lookup(t *testing.T) {
got, err := gsi.Lookup(tt.Lat, tt.Lon)
assert.NoError(t, err)

// check if the expected timezone is in the same etc reference
etcExpected, ok := tz2etc[tt.Tz]
assert.True(t, ok, "timezone %s not found in tz2etc.json", tt.Tz)
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)
return
}

// 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)

// get the reference timezone for the expected timezone
etcGot, ok := tz2etc[got]
zoneGot, ok := tzZones[got]
assert.True(t, ok, "timezone %s not found in zones.json", got)

if !ok {
assert.Equal(t, etcExpected, got, "expected %s (%s) to be %s (%s) for https://www.google.com/maps/@%v,%v,12z", tt.Tz, etcExpected, got, etcGot, tt.Lat, tt.Lon)
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, etcExpected, etcGot, "expected %s (%s) to be %s (%s) for https://www.google.com/maps/@%v,%v,12z", tt.Tz, etcExpected, got, etcGot, tt.Lat, tt.Lon)
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)
}
})
}
Expand Down
Loading

0 comments on commit 5c00920

Please sign in to comment.