1
0
Fork 0

Add zone parsing, and test code separately

main
Ambrose Chua 2020-11-09 00:27:16 +08:00
parent 8a8924a2d3
commit 8d9828d390
8 changed files with 224 additions and 20 deletions

View File

@ -8,6 +8,22 @@ on:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.15
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Test code
run: make test
vet:
name: Vet
runs-on: ubuntu-latest
@ -68,10 +84,7 @@ jobs:
uses: actions/checkout@v2
- name: Build
run: make
- name: Test
run: make test
run: make TAGS=production
# vim: set et ts=2 sw=2:

View File

@ -21,7 +21,7 @@ datetime: *.go
.PHONY: test
test:
$(GO) test -v
$(GO) test -cover -bench=. -v
DATASETS = \

2
app.go
View File

@ -16,7 +16,7 @@ type Datetime struct {
}
// NewDatetime creates an application instance. It assumes certain resources
// like templates and data exist.
// like templates and data exist
func NewDatetime() (*Datetime, error) {
// Data
tmpl, err := template.ParseGlob("templates/*")

View File

@ -9,7 +9,7 @@ import (
func TestChooseTemplate(t *testing.T) {
tmpl, err := template.ParseGlob("templates/*")
if err != nil {
t.Errorf("unable to load templates: %v", err)
panic(err)
}
app := &Datetime{tmpl: tmpl}

View File

@ -11,6 +11,10 @@ func TestChooseResponseType(t *testing.T) {
}
r = chooseResponseType("application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
if r != responseAny {
t.Errorf("expecting any, got %v", r)
}
r = chooseResponseType("text/plain;q=,text/html")
if r != responseHTML {
t.Errorf("expecting html, got %v", r)
}
}

View File

@ -21,7 +21,7 @@ func TestURLParse(t *testing.T) {
u := mustURLParse("http://test/2020-06-02T14:00+08:00/Singapore,Malaysia")
got, err := ParseRequest(u)
if err != nil {
t.Errorf("mismatch: got error %v", err)
t.Errorf("got error %v", err)
return
}
want := Request{
@ -29,13 +29,13 @@ func TestURLParse(t *testing.T) {
[]string{"Singapore", "Malaysia"},
}
if !cmp.Equal(got, want) {
t.Errorf("mismatch: \n%v", cmp.Diff(got, want))
t.Errorf("%v", cmp.Diff(got, want))
}
u = mustURLParse("http://test/2019-04-30T18:00:00Z/Nowhere")
got, err = ParseRequest(u)
if err != nil {
t.Errorf("mismatch: got error %v", err)
t.Errorf("got error %v", err)
return
}
want = Request{
@ -43,7 +43,7 @@ func TestURLParse(t *testing.T) {
[]string{"Nowhere"},
}
if !cmp.Equal(got, want) {
t.Errorf("mismatch: \n%v", cmp.Diff(got, want))
t.Errorf("%v", cmp.Diff(got, want))
}
}
@ -51,31 +51,38 @@ func TestURLParseFail(t *testing.T) {
u := mustURLParse("http://test/2002-08-30T14:00+06:00/")
_, err := ParseRequest(u)
if !errors.Is(err, ErrMissingComponent) {
t.Errorf("mismatch: got error %v, want error %v", err, ErrMissingComponent)
return
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
}
u = mustURLParse("http://test/")
_, err = ParseRequest(u)
if !errors.Is(err, ErrMissingComponent) {
t.Errorf("mismatch: got error %v, want error %v", err, ErrMissingComponent)
return
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
}
u = mustURLParse("http://test")
_, err = ParseRequest(u)
if !errors.Is(err, ErrMissingComponent) {
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
}
u = mustURLParse("http://test/hi/hi/hi")
_, err = ParseRequest(u)
if !errors.Is(err, ErrTooManyComponent) {
t.Errorf("got error %v, want error %v", err, ErrTooManyComponent)
}
u = mustURLParse("http://test/2000-01-13T00:00Z08:00/hi")
_, err = ParseRequest(u)
_, isParseError := err.(*time.ParseError)
if !isParseError {
t.Errorf("mismatch: got error %v, want time.ParseError", err)
return
t.Errorf("got error %v, want time.ParseError", err)
}
u = mustURLParse("http://test/2000-01-13 00:00+08:00/hi")
_, err = ParseRequest(u)
_, isParseError = err.(*time.ParseError)
if !isParseError {
t.Errorf("mismatch: got error %v, want time.ParseError", err)
return
t.Errorf("got error %v, want time.ParseError", err)
}
}

78
zone.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"errors"
"regexp"
"strconv"
"time"
"github.com/serverwentdown/datetime.link/data"
)
// 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")
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) (*time.Location, error) {
if !zoneOffsetRegexp.MatchString(zone) {
return nil, 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 nil, ErrZoneOffsetInvalid
}
return time.FixedZone("UTC"+zone, d*(int(h)*60*60+int(m)*60)), nil
}
// 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
}
// 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 {
return Zone{
Offset: offset,
}, nil
}
// Parse as a city
city, err := SearchCities(cities, zone)
if err == nil {
return Zone{
City: city,
}, nil
}
return Zone{}, err
}

102
zone_test.go Normal file
View File

@ -0,0 +1,102 @@
package main
import (
"testing"
"time"
"github.com/serverwentdown/datetime.link/data"
)
func TestParseZoneOffset(t *testing.T) {
loc, err := ParseZoneOffset("+08:00")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
time := time.Date(2020, time.November, 8, 23, 9, 0, 0, loc).Unix()
want := int64(1604848140)
if time != want {
t.Errorf("got %d, want %d", time, want)
}
}
loc, err = ParseZoneOffset("-00:30")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
time := time.Date(2020, time.November, 8, 14, 39, 0, 0, loc).Unix()
want := int64(1604848140)
if time != want {
t.Errorf("got %d, want %d", time, want)
}
}
loc, err = ParseZoneOffset("-0030")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
loc, err = ParseZoneOffset("00:30")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
loc, err = ParseZoneOffset("+08:60")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
loc, err = ParseZoneOffset("+08:-6")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
}
func TestSearchCities(t *testing.T) {
cities, err := data.ReadCities()
if err != nil {
panic(err)
}
city, err := SearchCities(cities, "Singapore-SG")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
}
wantName := "Singapore"
wantZone := "Asia/Singapore"
if city.Name != wantName || city.Timezone != wantZone {
t.Errorf("want %v %v, got %v", wantName, wantZone, city)
}
city, err = SearchCities(cities, "Yuzhno_Sakhalinsk-Sakhalin_Oblast-RU")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
}
wantName = "Yuzhno-Sakhalinsk"
wantZone = "Asia/Sakhalin"
if city.Name != wantName || city.Timezone != wantZone {
t.Errorf("want %v %v, got %v", wantName, wantZone, city)
}
_, err = SearchCities(cities, "Nowhere")
if err != ErrZoneNotFound {
t.Errorf("want error %v, got error %v", ErrZoneNotFound, err)
}
}
func BenchmarkReadCities(b *testing.B) {
// This does take quite a while
for i := 0; i < b.N; i++ {
_, _ = data.ReadCities()
}
}
func BenchmarkSearchCities(b *testing.B) {
cities, err := data.ReadCities()
if err != nil {
panic(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = SearchCities(cities, "Yuzhno_Sakhalinsk-Sakhalin_Oblast-RU")
}
}