Split files into smaller parts
parent
84825d48ca
commit
22395c0f3e
42
app_test.go
42
app_test.go
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
34
template.go
34
template.go
|
@ -7,40 +7,6 @@ import (
|
||||||
"go.uber.org/zap"
|
"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
|
// 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.
|
// 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 {
|
func (app Datetime) loadTemplate(name string, w http.ResponseWriter, req *http.Request) *template.Template {
|
||||||
|
|
|
@ -1,27 +1,42 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTemplateFuncFormatOffset(t *testing.T) {
|
func TestChooseTemplate(t *testing.T) {
|
||||||
want, got := "+06:06", templateFuncFormatOffset(6*60*60+6*60)
|
tmpl, err := template.New("templates").Funcs(templateFuncs).ParseGlob("templates/*")
|
||||||
if want != got {
|
if err != nil {
|
||||||
t.Fatalf("got offset %v, want offset %v", got, want)
|
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))
|
for _, test := range tests {
|
||||||
if want != got {
|
tmpl, contentType, acceptable := app.chooseTemplate(test.accept, "index")
|
||||||
t.Fatalf("got offset %v, want offset %v", got, want)
|
fn := fmt.Sprintf("chooseTemplate(\"%s\")", test.accept)
|
||||||
}
|
if contentType != test.contentType {
|
||||||
|
t.Errorf("%s; contentType = %v; wanted %v", fn, contentType, test.contentType)
|
||||||
want, got = "\u00B100:00", templateFuncFormatOffset(-(0*60*60 + 0*60))
|
}
|
||||||
if want != got {
|
if acceptable != test.acceptable {
|
||||||
t.Fatalf("got offset %v, want offset %v", got, want)
|
t.Errorf("%s; acceptable = %v; wanted %v", fn, acceptable, test.acceptable)
|
||||||
}
|
}
|
||||||
|
if tmpl != app.tmpl.Lookup(test.template) {
|
||||||
want, got = "+00:01", templateFuncFormatOffset(0*60*60+1*60)
|
t.Errorf("%s; tmpl = %v; wanted template for %v", fn, tmpl.Name(), test.template)
|
||||||
if want != got {
|
}
|
||||||
t.Fatalf("got offset %v, want offset %v", got, want)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
64
zone.go
|
@ -1,76 +1,12 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/serverwentdown/datetime.link/data"
|
"github.com/serverwentdown/datetime.link/data"
|
||||||
"go.uber.org/zap"
|
"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
|
// Zone represents any form of zone offset: this could be a city or a fixed
|
||||||
// offset from UTC
|
// offset from UTC
|
||||||
type Zone struct {
|
type Zone struct {
|
||||||
|
|
116
zone_test.go
116
zone_test.go
|
@ -7,104 +7,6 @@ import (
|
||||||
"github.com/serverwentdown/datetime.link/data"
|
"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) {
|
func TestResolveZone(t *testing.T) {
|
||||||
cities, err := data.ReadCities()
|
cities, err := data.ReadCities()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -147,21 +49,3 @@ func TestResolveZone(t *testing.T) {
|
||||||
t.Errorf("want error %v, got error %v", ErrZoneNotFound, err)
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue