Get a list of valid time zones in Go
Asked Answered
L

4

24

I'd like to write a method that will populate a Go Language array with the common timezones that are accepted by the time.Format() call, for use in an HTML template (Form select to allow them to read and choose their timezone). Is there a common way to do this?

Lacewing answered 19/10, 2016 at 0:15 Comment(3)
func (t Time) Format(layout string) doesn't take a time zone (other than "MST" to indicate where you want the time zone to go in the output).Nessa
The documentation for time.LoadLocation describes where Go looks for time zone information.Melnick
If you can call a C++ library I can show how to do this.Spitzer
S
36

To get a list of time zones, you can use something like:

package main

import (
    "fmt"
    "os"
    "strings"
)

var zoneDirs = []string{
    // Update path according to your OS
    "/usr/share/zoneinfo/",
    "/usr/share/lib/zoneinfo/",
    "/usr/lib/locale/TZ/",
}

var zoneDir string

func main() {
    for _, zoneDir = range zoneDirs {
        ReadFile("")
    }
}

func ReadFile(path string) {
    files, _ := os.ReadDir(zoneDir + path)
    for _, f := range files {
        if f.Name() != strings.ToUpper(f.Name()[:1]) + f.Name()[1:] {
            continue
        }
        if f.IsDir() {
            ReadFile(path + "/" + f.Name())
        } else {
            fmt.Println((path + "/" + f.Name())[1:])
        }
    }
}

output:

Africa/Abidjan
Africa/Accra
Africa/Addis_Ababa
Africa/Algiers
Africa/Asmara
Africa/Asmera
Africa/Bamako
Africa/Bangui
...
Seventh answered 19/10, 2016 at 12:1 Comment(6)
Fantastic! Though, I think there is a typo in ReadFile("") -- Shouldn't it be ReadFile(zoneDir)?Lacewing
Nope, no typo) It is because of recursion. Also it is more like prototype, so you can refine and integrate somewhere, but you probably need to store results instead of just printing.Seventh
Any idea how to include the default fallback data located inside of $GOROOT/lib/time?Centrist
In MacOS 11, there is a file +VERSION in /usr/share/zoneinfo/. You have to skip this file.Pillar
There are some files in directories which are not TZ files on macOS and on some Linux distros. The best way to test it is to try load the zone with time.LoadLocation() and check for errors. If no error, the file is TZ file. if _, err := time.LoadLocation(tzFilename); err != nil { continue }Flori
@Centrist I’m not sure if such a solution deserves a full-fledged answer, so would rather use comments section. Here you can have a look at how I extracted the strings from $GOROOT/lib/time: github.com/esimonov/locache/blob/main/cache_test.go. Heavily influenced by the internals of time package.Protease
M
4

Here is an example: https://play.golang.org/p/KFGQiW5A1P-

package main

import (
    "fmt"
    "io/ioutil"
    "strings"
    "unicode"
)

func main() {
    fmt.Println(GetOsTimeZones())
}

func GetOsTimeZones() []string {
    var zones []string
    var zoneDirs = []string{
        // Update path according to your OS
        "/usr/share/zoneinfo/",
        "/usr/share/lib/zoneinfo/",
        "/usr/lib/locale/TZ/",
    }

    for _, zd := range zoneDirs {
        zones = walkTzDir(zd, zones)

        for idx, zone := range zones {
            zones[idx] = strings.ReplaceAll(zone, zd+"/", "")
        }
    }

    return zones
}

func walkTzDir(path string, zones []string) []string {
    fileInfos, err := ioutil.ReadDir(path)
    if err != nil {
        return zones
    }

    isAlpha := func(s string) bool {
        for _, r := range s {
            if !unicode.IsLetter(r) {
                return false
            }
        }
        return true
    }

    for _, info := range fileInfos {
        if info.Name() != strings.ToUpper(info.Name()[:1])+info.Name()[1:] {
            continue
        }

        if !isAlpha(info.Name()[:1]) {
            continue
        }

        newPath := path + "/" + info.Name()

        if info.IsDir() {
            zones = walkTzDir(newPath, zones)
        } else {
            zones = append(zones, newPath)
        }
    }

    return zones
}
Mandiemandingo answered 3/10, 2020 at 1:44 Comment(0)
S
2

Go's time pkg uses a timezone database.

You can load a timezone location like this:

loc, err := time.LoadLocation("America/Chicago")
if err != nil {
    // handle error
}

t := time.Now().In(loc)

The Format function is not related to setting the time zone, this function takes a fixed reference time that allows you to format the date how you would like. Take a look at the time pkg docs.

For instance:

fmt.Println(t.Format("MST")) // outputs CST

Here is a running example

Scheldt answered 19/10, 2016 at 3:6 Comment(2)
It doesn't answer the question. Why is it so much upvoted.Betts
The questions is a bit unclear. The question mistakes how the Format method works. This answer explains how to load the correct location and display the time using the loaded location. Displaying a list in an html template seems to be the easy part, so I assume people find what wrote useful, hence the upvotes.Scheldt
E
0

Refined the answer provided by @Marsel,

Checks for Valid Time Zones, Automates OS lookup, and has no recursion (for readability) "Matter of preference".

Checked on "Ubuntu Server" and "Mint"

Hope it helps

package main

import (
    "fmt"
    "log"
    "os"
    "runtime"
    "strings"
    "time"
)

var zoneDirs = map[string]string{
    "android":   "/system/usr/share/zoneinfo/",
    "darwin":    "/usr/share/zoneinfo/",
    "dragonfly": "/usr/share/zoneinfo/",
    "freebsd":   "/usr/share/zoneinfo/",
    "linux":     "/usr/share/zoneinfo/",
    "netbsd":    "/usr/share/zoneinfo/",
    "openbsd":   "/usr/share/zoneinfo/",
    "solaris":   "/usr/share/lib/zoneinfo/",
}

var timeZones []string

func main() {
    // Reads the Directory corresponding to the OS
    dirFile, _ := os.ReadDir(zoneDirs[runtime.GOOS])
    for _, i := range dirFile {
        // Checks if starts with Capital Letter
        if i.Name() == (strings.ToUpper(i.Name()[:1]) + i.Name()[1:]) {
            if i.IsDir() {
                // Recursive read if directory
                subFiles, err := os.ReadDir(zoneDirs[runtime.GOOS] + i.Name())
                if err != nil {
                    log.Fatal(err)
                }
                for _, s := range subFiles {
                    // Appends the path to timeZones var
                    timeZones = append(timeZones, i.Name()+"/"+s.Name())
                }
            }
            // Appends the path to timeZones var
            timeZones = append(timeZones, i.Name())
        }
    }
    // Loop over timezones and Check Validity, Delete entry if invalid.
    // Range function doesnt work with changing length.
    for i := 0; i < len(timeZones); i++ {
        _, err := time.LoadLocation(timeZones[i])
        if err != nil {
            // newSlice = timeZones[:n]  timeZones[n+1:]
            timeZones = append(timeZones[:i], timeZones[i+1:]...)
            continue
        }
    }
    // Now we Can range timeZones for printing
    for _, i := range timeZones {
        fmt.Println(i)
    }
}

Produces Output:

Africa/Abidjan
Africa/Accra
Africa/Addis_Ababa
Africa/Algiers
Africa/Asmara
Africa/Asmera
Africa/Bamako
Africa/Bangui
Africa/Banjul
Africa/Bissau
Africa/Blantyre
Africa/Brazzaville
...

Playground Link: https://go.dev/play/p/DoisnTppfnp

Elmira answered 13/7, 2024 at 23:47 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.