1
0
Fork 0
datetime.link/zone.go

142 lines
3.3 KiB
Go

package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"time"
"github.com/serverwentdown/datetime.link/data"
"go.uber.org/zap"
)
// ErrZoneNotFound is thrown when a zone string has no match
var ErrZoneNotFound = errors.New("zone not found")
// ErrZoneOffsetInvalid is thrown when a zone string is an invalid zone offset
var ErrZoneOffsetInvalid = errors.New("offset zone invalid")
const zoneHour = 60 * 60
const zoneMinute = 60
var zoneOffsetRegexp = regexp.MustCompile(`^[+-][0-9]{2}:[0-9]{2}$`)
// SearchCities looks up a city by it's reference
func SearchCities(cities map[string]*data.City, city string) (*data.City, error) {
// For now, simple map read will do
if city, ok := cities[city]; ok {
return city, nil
}
return nil, ErrZoneNotFound
}
// ParseZoneOffset parses a zone string into a time.Location
func ParseZoneOffset(zone string) (int, error) {
if !zoneOffsetRegexp.MatchString(zone) {
return 0, ErrZoneOffsetInvalid
}
// Assume that if it satisfies the regex, it satisfies the length and won't
// fail to parse
d := 0
if zone[0] == '+' {
d = 1
}
if zone[0] == '-' {
d = -1
}
h, _ := strconv.ParseUint(zone[1:1+2], 10, 64)
// Allow hour offsets greater that 24
m, _ := strconv.ParseUint(zone[1+3:1+3+2], 10, 64)
if m >= 60 {
return 0, ErrZoneOffsetInvalid
}
offset := d * (int(h)*zoneHour + int(m)*zoneMinute)
return offset, nil
}
// FormatZoneOffset formats an offset into a string
func FormatZoneOffset(offset int) string {
neg := offset < 0
s := '+'
if neg {
s = '-'
offset = -offset
}
if offset == 0 {
return "\u00B100:00"
}
h := offset / zoneHour
m := (offset % zoneHour) / zoneMinute
return fmt.Sprintf("%c%02d:%02d", s, h, m)
}
// Zone represents any form of zone offset: this could be a city or a fixed
// offset from UTC
type Zone struct {
// City is optional
City *data.City
// Offset is optional
Offset *time.Location
}
// IsOffset is true when the Zone is an offset instead of a city
func (z Zone) IsOffset() bool {
return z.Offset != nil
}
// Name returns the name of the zone
func (z Zone) Name() string {
if z.IsOffset() {
return z.Offset.String()
} else if z.City != nil {
return z.City.FullName()
}
return ""
}
// Location returns the time.Location of the zone. Useful for other functions
func (z Zone) Location() *time.Location {
if z.IsOffset() {
return z.Offset
} else if z.City != nil {
loc, err := time.LoadLocation(z.City.Timezone)
if err != nil {
// This is a really bad situation
// TODO: validate this at boot time
l.Error("unable to find timezone", zap.String("timezone", z.City.Timezone))
}
return loc
}
return nil
}
// TimeOffset returns the timezone offset at a specific time
func (z Zone) TimeOffset(t time.Time) int {
if l := z.Location(); l != nil {
_, offset := t.In(l).Zone()
return offset
}
return -1 // TODO: better invalid handling
}
// ResolveZone resolves a zone string into a Zone
func ResolveZone(cities map[string]*data.City, zone string) (Zone, error) {
// Try parsing as a zone offset
offset, err := ParseZoneOffset(zone)
if err == nil {
offsetZone := time.FixedZone("UTC "+FormatZoneOffset(offset), offset)
return Zone{
Offset: offsetZone,
}, nil
}
// Parse as a city
city, err := SearchCities(cities, zone)
if err == nil {
return Zone{
City: city,
}, nil
}
return Zone{}, err
}