Timo Huovinen
Timo Huovinen

Reputation: 55653

How to get the full name of the local timezone?

For example the server timezone is "Europe/Madrid" and I do this:

now := time.Now()
fmt.Printf("%v", now.Location().String()) // prints "Local"

zone, _ := now.Zone()
fmt.Printf("%v", zone) // prints "CEST"

But I want "Europe/Madrid"

Timezones: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

Edit

Full cross platform (windows, linux, mac) answer provided here: https://github.com/thlib/go-timezone-local

All credit goes to colm.anseo and MrFuppes for providing the following answers:

Upvotes: 4

Views: 3123

Answers (2)

FObersteiner
FObersteiner

Reputation: 25624

On Windows, the procedure also isn't overly complex. As mentioned by @colm.anseo, you can read the respective registry key to get the time zone name that Windows uses. Then map that to the appropriate IANA time zone name.

read tz from registry

package main

import (
    "log"

    "golang.org/x/sys/windows/registry"
)

var winTZtoIANA = map[string]string{
    // just includes my time zone; to be extended...
    "W. Europe Standard Time": "Europe/Berlin",
}

func main() {

    k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\TimeZoneInformation`, registry.QUERY_VALUE)
    if err != nil {
        log.Fatal(err)
    }
    defer k.Close()

    winTZname, _, err := k.GetStringValue("TimeZoneKeyName")
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("local time zone in Windows: %v\n", winTZname)

    if IANATZname, ok := winTZtoIANA[winTZname]; ok {
        log.Printf("local time zone IANA name: %v\n", IANATZname)
    } else {
        log.Fatalf("could not find IANA tz name for %v", winTZname)
    }

}

Alternatively, you can read the output of tzutil /g:

cmd := exec.Command("tzutil", "/g")
winTZname, err := cmd.Output() // note: winTZname is []byte, need to convert to string
if err != nil {
    // handle error
}

You can find a comprehensive mapping here. It's based on Lennart Regebro's tzlocal package that solves the given question in Python.

Upvotes: 1

colm.anseo
colm.anseo

Reputation: 22117

IANA timezones are available on most OSes (*). The Go standard library ships it as well:

runtime.GOROOT() + "/lib/time/zoneinfo.zip"

The selection of a timezone by name and whether this name is recorded anywhere is left up to the OS:

ls -al /etc/localtime

# MacOS
/etc/localtime -> /var/db/timezone/zoneinfo/America/New_York

# Linux
/etc/localtime -> ../usr/share/zoneinfo/America/New_York

so in the above cases the name may be inferred.

Note: Go uses /etc/localtime by default for the local OS time (timezone.go) - but this can be overridden with the TZ environment variable.

So one could infer the name of the local OS timezone, via the symlink target path like so:

// tz.go

//go:build !windows
// +build !windows

const localZoneFile = "/etc/localtime" // symlinked file - set by OS
var ValidateLocationName = true        // set to false to disable time.LoadLocation validation check

func LocalTZLocationName() (name string, err error) {

    var ok bool
    if name, ok = os.LookupEnv("TZ"); ok {
        if name == "" { // if blank - Go treats this as UTC
            return "UTC", nil
        }
        if ValidateLocationName {
            _, err = time.LoadLocation(name) // optional validation of name
        }
        return
    }

    fi, err := os.Lstat(localZoneFile)
    if err != nil {
        err = fmt.Errorf("failed to stat %q: %w", localZoneFile, err)
        return
    }

    if (fi.Mode() & os.ModeSymlink) == 0 {
        err = fmt.Errorf("%q is not a symlink - cannot infer name", localZoneFile)
        return
    }

    p, err := os.Readlink(localZoneFile)
    if err != nil {
        return
    }

    name, err = inferFromPath(p) // handles 1 & 2 part zone names
    return
}

func inferFromPath(p string) (name string, err error) {

    dir, lname := path.Split(p)

    if len(dir) == 0 || len(lname) == 0 {
        err = fmt.Errorf("cannot infer timezone name from path: %q", p)
        return
    }

    _, fname := path.Split(dir[:len(dir)-1])

    if fname == "zoneinfo" {
        name = lname // e.g. /usr/share/zoneinfo/Japan
    } else {
        name = fname + string(os.PathSeparator) + lname // e.g. /usr/share/zoneinfo/Asia/Tokyo
    }

    if ValidateLocationName {
        _, err = time.LoadLocation(name) // optional validation of name
    }

    return
}

// tz_windows.go
//go:build windows
// +build windows

func LocalTZLocationName() (string, error) {
    return "", fmt.Errorf("local timezone name inference not available on Windows")
}

(*) for Windows according to the Go source (zoneinfo_windows.go), zone info is loaded from the Go standard library, due to:

// BUG(brainman,rsc): On Windows, the operating system does not provide complete
// time zone information.
// The implementation assumes that this year's rules for daylight savings
// time apply to all previous and future years as well.

the Window's registry key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TimeZoneInformation

stores the local timezone - but does not appear to store the long form location name:

TimeZoneKeyName=Eastern Standard Time

Upvotes: 3

Related Questions