September 7, 2019

what time is it? (refactored)

Since I realized that my previous implementation of a timezone utility didn’t need to sort, I decided to refactor. Here’s the result.

package timezone

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const DefaultLayout = "Jan 2, 2006 at " + time.Kitchen

func DefaultDate() string {
	return time.Now().Format("Jan 2, 2006")
}

type TimeZone struct {
	ZoneinfoEntry string
	LocaleName    string

	location *time.Location
	time     time.Time
}

func (tz *TimeZone) SetTime(kitchenTime string) error {
	t, err := time.ParseInLocation(DefaultLayout, DefaultDate()+" at "+kitchenTime, tz.location)
	tz.time = t
	return err
}

func (tz TimeZone) AdjustTo(other *TimeZone) {
	other.time = tz.time.In(other.location)
}

func (catalog TimeZoneCatalog) AdjustTimeZones(local *TimeZone, locations ...string) ([]*TimeZone, error) {
	var adjusted []*TimeZone

	for _, location := range locations {
		tz, err := catalog.Find(location)
		if err != nil {
			return adjusted, err
		}
		adjusted = append(adjusted, tz)

		local.AdjustTo(tz)
	}

	return adjusted, nil
}

func NewTimeZoneFromZoneinfo(entry string) (*TimeZone, error) {
	tz := &TimeZone{}

	tz.ZoneinfoEntry = entry

	return tz, postInitialize(tz)
}

func (tz TimeZone) Timestring() string {
	return tz.time.Format(DefaultLayout)
}

func postInitialize(tz *TimeZone) error {
	var err error
	var locale string
	var location *time.Location

	entry := strings.ReplaceAll(tz.ZoneinfoEntry, "/usr/share/zoneinfo/", "")
	parts := strings.Split(entry, "/")
	locale = parts[len(parts)-1]

	location, err = time.LoadLocation(entry)

	tz.LocaleName = locale
	tz.location = location

	return err
}

type TimeZoneCatalog map[string]*TimeZone

func (catalog TimeZoneCatalog) Find(locale string) (*TimeZone, error) {
	var err error
	tz := catalog[locale]
	if tz == nil {
		err = fmt.Errorf("Timezone not found for %s", locale)
	}

	return tz, err
}

func BuildTimeZoneCatalog(zoneinfoPath string) (TimeZoneCatalog, error) {
	catalog := make(TimeZoneCatalog)

	entries, err := filepath.Glob(zoneinfoPath + "**/**")
	if err != nil {
		return catalog, err
	}

	for _, entry := range entries {
		tz, err := NewTimeZoneFromZoneinfo(entry)

		if err != nil {
			// attempt deeper traversal
			deepCatalog, err2 := BuildTimeZoneCatalog(entry + "/**")
			if err2 != nil {
				return catalog, err2
			}

			for locale, data := range deepCatalog {
				catalog[locale] = data
			}

			continue
		}

		catalog[tz.LocaleName] = tz
	}

	return catalog, nil
}

func main() {
	var kitchenTime string
	var homeLocale string
	var elsewhere []string

	kitchenTime = os.Args[1]
	homeLocale = os.Args[2]
	elsewhere = os.Args[3:]

	catalog, err := BuildTimeZoneCatalog("/usr/share/zoneinfo/")
	if err != nil {
		log.Fatal(err)
	}

	local, err := catalog.Find(homeLocale)
	if err != nil {
		log.Println(catalog)
		log.Fatal(err)
	}
	local.SetTime(kitchenTime)

	times, err := catalog.AdjustTimeZones(local, elsewhere...)
	if err != nil {
		log.Fatal(err)
	}
	times = append([]*TimeZone{local}, times...)

	fmt.Printf("\nLocation     | Time\n-------------|-------\n")
	for _, t := range times {
		fmt.Printf("%-12s | %16s\n", t.LocaleName, t.Timestring())
	}
}

and it’s output:

go run timezone.go 12:00PM Tokyo New_York Phoenix Los_Angeles Madrid Hong_Kong

Location     | Time
-------------|-------
Tokyo        | Sep 7, 2019 at 12:00PM
New_York     | Sep 6, 2019 at 11:00PM
Phoenix      | Sep 6, 2019 at 8:00PM
Los_Angeles  | Sep 6, 2019 at 8:00PM
Madrid       | Sep 7, 2019 at 5:00AM
Hong_Kong    | Sep 7, 2019 at 11:00AM

Content by © Jared Davis 2019
Theme by © Emir Ribic 2017

Powered by Hugo & Kiss.