Add zone parsing, and test code separately
parent
8a8924a2d3
commit
8d9828d390
|
@ -8,6 +8,22 @@ on:
|
||||||
|
|
||||||
jobs:
|
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:
|
vet:
|
||||||
name: Vet
|
name: Vet
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -68,10 +84,7 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make
|
run: make TAGS=production
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
|
|
||||||
# vim: set et ts=2 sw=2:
|
# vim: set et ts=2 sw=2:
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -21,7 +21,7 @@ datetime: *.go
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
$(GO) test -v
|
$(GO) test -cover -bench=. -v
|
||||||
|
|
||||||
|
|
||||||
DATASETS = \
|
DATASETS = \
|
||||||
|
|
2
app.go
2
app.go
|
@ -16,7 +16,7 @@ type Datetime struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatetime creates an application instance. It assumes certain resources
|
// NewDatetime creates an application instance. It assumes certain resources
|
||||||
// like templates and data exist.
|
// like templates and data exist
|
||||||
func NewDatetime() (*Datetime, error) {
|
func NewDatetime() (*Datetime, error) {
|
||||||
// Data
|
// Data
|
||||||
tmpl, err := template.ParseGlob("templates/*")
|
tmpl, err := template.ParseGlob("templates/*")
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
func TestChooseTemplate(t *testing.T) {
|
func TestChooseTemplate(t *testing.T) {
|
||||||
tmpl, err := template.ParseGlob("templates/*")
|
tmpl, err := template.ParseGlob("templates/*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to load templates: %v", err)
|
panic(err)
|
||||||
}
|
}
|
||||||
app := &Datetime{tmpl: tmpl}
|
app := &Datetime{tmpl: tmpl}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
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 {
|
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)
|
t.Errorf("expecting html, got %v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
url_test.go
33
url_test.go
|
@ -21,7 +21,7 @@ func TestURLParse(t *testing.T) {
|
||||||
u := mustURLParse("http://test/2020-06-02T14:00+08:00/Singapore,Malaysia")
|
u := mustURLParse("http://test/2020-06-02T14:00+08:00/Singapore,Malaysia")
|
||||||
got, err := ParseRequest(u)
|
got, err := ParseRequest(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("mismatch: got error %v", err)
|
t.Errorf("got error %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
want := Request{
|
want := Request{
|
||||||
|
@ -29,13 +29,13 @@ func TestURLParse(t *testing.T) {
|
||||||
[]string{"Singapore", "Malaysia"},
|
[]string{"Singapore", "Malaysia"},
|
||||||
}
|
}
|
||||||
if !cmp.Equal(got, want) {
|
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")
|
u = mustURLParse("http://test/2019-04-30T18:00:00Z/Nowhere")
|
||||||
got, err = ParseRequest(u)
|
got, err = ParseRequest(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("mismatch: got error %v", err)
|
t.Errorf("got error %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
want = Request{
|
want = Request{
|
||||||
|
@ -43,7 +43,7 @@ func TestURLParse(t *testing.T) {
|
||||||
[]string{"Nowhere"},
|
[]string{"Nowhere"},
|
||||||
}
|
}
|
||||||
if !cmp.Equal(got, want) {
|
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/")
|
u := mustURLParse("http://test/2002-08-30T14:00+06:00/")
|
||||||
_, err := ParseRequest(u)
|
_, err := ParseRequest(u)
|
||||||
if !errors.Is(err, ErrMissingComponent) {
|
if !errors.Is(err, ErrMissingComponent) {
|
||||||
t.Errorf("mismatch: got error %v, want error %v", err, ErrMissingComponent)
|
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u = mustURLParse("http://test/")
|
u = mustURLParse("http://test/")
|
||||||
_, err = ParseRequest(u)
|
_, err = ParseRequest(u)
|
||||||
if !errors.Is(err, ErrMissingComponent) {
|
if !errors.Is(err, ErrMissingComponent) {
|
||||||
t.Errorf("mismatch: got error %v, want error %v", err, ErrMissingComponent)
|
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
|
||||||
return
|
}
|
||||||
|
|
||||||
|
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")
|
u = mustURLParse("http://test/2000-01-13T00:00Z08:00/hi")
|
||||||
_, err = ParseRequest(u)
|
_, err = ParseRequest(u)
|
||||||
_, isParseError := err.(*time.ParseError)
|
_, isParseError := err.(*time.ParseError)
|
||||||
if !isParseError {
|
if !isParseError {
|
||||||
t.Errorf("mismatch: got error %v, want time.ParseError", err)
|
t.Errorf("got error %v, want time.ParseError", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u = mustURLParse("http://test/2000-01-13 00:00+08:00/hi")
|
u = mustURLParse("http://test/2000-01-13 00:00+08:00/hi")
|
||||||
_, err = ParseRequest(u)
|
_, err = ParseRequest(u)
|
||||||
_, isParseError = err.(*time.ParseError)
|
_, isParseError = err.(*time.ParseError)
|
||||||
if !isParseError {
|
if !isParseError {
|
||||||
t.Errorf("mismatch: got error %v, want time.ParseError", err)
|
t.Errorf("got error %v, want time.ParseError", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue