1
0
Fork 0

Split files into smaller parts

main
Ambrose Chua 2020-11-10 19:12:15 +08:00
parent 84825d48ca
commit 22395c0f3e
11 changed files with 300 additions and 273 deletions

View File

@ -1,42 +0,0 @@
package main
import (
"fmt"
"html/template"
"testing"
)
func TestChooseTemplate(t *testing.T) {
tmpl, err := template.New("templates").Funcs(templateFuncs).ParseGlob("templates/*")
if err != nil {
panic(err)
}
app := &Datetime{tmpl: tmpl}
type chooseTest struct {
accept string
acceptable bool
contentType string
template string
}
tests := []chooseTest{
{"text/html", true, "text/html", "index.html"},
{"text/html;q=0.9,text/plain", true, "text/plain", "index.txt"},
{"image/png", false, "", ""},
{"*/*", true, "text/plain", "index.txt"},
}
for _, test := range tests {
tmpl, contentType, acceptable := app.chooseTemplate(test.accept, "index")
fn := fmt.Sprintf("chooseTemplate(\"%s\")", test.accept)
if contentType != test.contentType {
t.Errorf("%s; contentType = %v; wanted %v", fn, contentType, test.contentType)
}
if acceptable != test.acceptable {
t.Errorf("%s; acceptable = %v; wanted %v", fn, acceptable, test.acceptable)
}
if tmpl != app.tmpl.Lookup(test.template) {
t.Errorf("%s; tmpl = %v; wanted template for %v", fn, tmpl.Name(), test.template)
}
}
}

View File

@ -7,40 +7,6 @@ import (
"go.uber.org/zap"
)
var templateFuncs = map[string]interface{}{
"statusText": templateFuncStatusText,
"thisIsSafe": templateFuncThisIsSafe,
// Formatting
"formatOffset": templateFuncFormatOffset,
// Logic
"resolveZone": templateFuncResolveZone,
}
func templateFuncStatusText(s int) string {
return http.StatusText(s)
}
func templateFuncThisIsSafe(s string) template.HTML {
return template.HTML(s)
}
func templateFuncFormatOffset(offset int) string {
return FormatZoneOffset(offset)
}
// ResolvedZone holds a resolved zone or an error
type ResolvedZone struct {
Zone
Error error
}
func templateFuncResolveZone(app Datetime, zone string) ResolvedZone {
z, err := ResolveZone(app.cities, zone)
if err != nil {
l.Debug("unable to resolve zone", zap.Reflect("zone", zone), zap.Error(err))
}
return ResolvedZone{z, err}
}
// loadTemplate returns a matching template for the request. It also causes an
// error if the template is not found or the Accept parameters are incorrect.
func (app Datetime) loadTemplate(name string, w http.ResponseWriter, req *http.Request) *template.Template {

View File

@ -1,27 +1,42 @@
package main
import (
"fmt"
"html/template"
"testing"
)
func TestTemplateFuncFormatOffset(t *testing.T) {
want, got := "+06:06", templateFuncFormatOffset(6*60*60+6*60)
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
func TestChooseTemplate(t *testing.T) {
tmpl, err := template.New("templates").Funcs(templateFuncs).ParseGlob("templates/*")
if err != nil {
panic(err)
}
app := &Datetime{tmpl: tmpl}
type chooseTest struct {
accept string
acceptable bool
contentType string
template string
}
tests := []chooseTest{
{"text/html", true, "text/html", "index.html"},
{"text/html;q=0.9,text/plain", true, "text/plain", "index.txt"},
{"image/png", false, "", ""},
{"*/*", true, "text/plain", "index.txt"},
}
want, got = "-12:15", templateFuncFormatOffset(-(12*60*60 + 15*60))
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
}
want, got = "\u00B100:00", templateFuncFormatOffset(-(0*60*60 + 0*60))
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
}
want, got = "+00:01", templateFuncFormatOffset(0*60*60+1*60)
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
for _, test := range tests {
tmpl, contentType, acceptable := app.chooseTemplate(test.accept, "index")
fn := fmt.Sprintf("chooseTemplate(\"%s\")", test.accept)
if contentType != test.contentType {
t.Errorf("%s; contentType = %v; wanted %v", fn, contentType, test.contentType)
}
if acceptable != test.acceptable {
t.Errorf("%s; acceptable = %v; wanted %v", fn, acceptable, test.acceptable)
}
if tmpl != app.tmpl.Lookup(test.template) {
t.Errorf("%s; tmpl = %v; wanted template for %v", fn, tmpl.Name(), test.template)
}
}
}

42
templatehelpers.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"html/template"
"net/http"
"go.uber.org/zap"
)
var templateFuncs = map[string]interface{}{
"statusText": templateFuncStatusText,
"thisIsSafe": templateFuncThisIsSafe,
// Formatting
"formatOffset": templateFuncFormatOffset,
// Logic
"resolveZone": templateFuncResolveZone,
}
func templateFuncStatusText(s int) string {
return http.StatusText(s)
}
func templateFuncThisIsSafe(s string) template.HTML {
return template.HTML(s)
}
func templateFuncFormatOffset(offset int) string {
return FormatZoneOffset(offset)
}
// ResolvedZone holds a resolved zone or an error
type ResolvedZone struct {
Zone
Error error
}
func templateFuncResolveZone(app Datetime, zone string) ResolvedZone {
z, err := ResolveZone(app.cities, zone)
if err != nil {
l.Debug("unable to resolve zone", zap.Reflect("zone", zone), zap.Error(err))
}
return ResolvedZone{z, err}
}

64
zone.go
View File

@ -1,76 +1,12 @@
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 {

View File

@ -7,104 +7,6 @@ import (
"github.com/serverwentdown/datetime.link/data"
)
func TestParseZoneOffset(t *testing.T) {
offset, err := ParseZoneOffset("+08:00")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
want := 8*60*60 + 0*60
if offset != want {
t.Errorf("got %d, want %d", offset, want)
}
}
offset, err = ParseZoneOffset("-01:30")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
want := -(1*60*60 + 30*60)
if offset != want {
t.Errorf("got %d, want %d", offset, want)
}
}
_, err = ParseZoneOffset("-0030")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
_, err = ParseZoneOffset("00:30")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
_, err = ParseZoneOffset("+08:60")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
_, err = ParseZoneOffset("+08:-6")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
offset, err = ParseZoneOffset("+08:00")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
loc := time.FixedZone("UTC "+FormatZoneOffset(offset), offset)
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)
}
}
offset, err = ParseZoneOffset("-00:30")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
loc := time.FixedZone("UTC "+FormatZoneOffset(offset), offset)
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)
}
}
}
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 TestResolveZone(t *testing.T) {
cities, err := data.ReadCities()
if err != nil {
@ -147,21 +49,3 @@ func TestResolveZone(t *testing.T) {
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")
}
}

19
zonecity.go Normal file
View File

@ -0,0 +1,19 @@
package main
import (
"errors"
"github.com/serverwentdown/datetime.link/data"
)
// ErrZoneNotFound is thrown when a zone string has no match
var ErrZoneNotFound = errors.New("zone not found")
// 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
}

57
zonecity_test.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"testing"
"github.com/serverwentdown/datetime.link/data"
)
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")
}
}

56
zoneoffset.go Normal file
View File

@ -0,0 +1,56 @@
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
)
// 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}$`)
// ParseZoneOffset parses a zone string into an offset
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)
}

94
zoneoffset_test.go Normal file
View File

@ -0,0 +1,94 @@
package main
import (
"testing"
"time"
)
func TestParseZoneOffset(t *testing.T) {
offset, err := ParseZoneOffset("+08:00")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
want := 8*60*60 + 0*60
if offset != want {
t.Errorf("got %d, want %d", offset, want)
}
}
offset, err = ParseZoneOffset("-01:30")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
want := -(1*60*60 + 30*60)
if offset != want {
t.Errorf("got %d, want %d", offset, want)
}
}
_, err = ParseZoneOffset("-0030")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
_, err = ParseZoneOffset("00:30")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
_, err = ParseZoneOffset("+08:60")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
_, err = ParseZoneOffset("+08:-6")
if err != ErrZoneOffsetInvalid {
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
}
offset, err = ParseZoneOffset("+08:00")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
loc := time.FixedZone("UTC "+FormatZoneOffset(offset), offset)
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)
}
}
offset, err = ParseZoneOffset("-00:30")
if err != nil {
t.Errorf("want error %v, got error %v", nil, err)
} else {
loc := time.FixedZone("UTC "+FormatZoneOffset(offset), offset)
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)
}
}
}
func TestFormatZoneOffset(t *testing.T) {
want, got := "+06:06", FormatZoneOffset(6*60*60+6*60)
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
}
want, got = "-12:15", FormatZoneOffset(-(12*60*60 + 15*60))
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
}
want, got = "\u00B100:00", FormatZoneOffset(-(0*60*60 + 0*60))
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
}
want, got = "+00:01", FormatZoneOffset(0*60*60+1*60)
if want != got {
t.Fatalf("got offset %v, want offset %v", got, want)
}
}