How to properly parse timezone codes

27,663

Solution 1

When you Parse a time, you are parsing it in your current location, which is OK as long as that's what you're expecting, and the timezone abbreviation is known from within your location.

If you can forgo timezones, it's far easier to normalize all the times you're dealing with into UTC.

The next easiest is handling everything with explicit offsets, like -05:00.

If you want to deal with times originating in other timezones, you need to use time.Location. You can load Locations from the local timezone db with time.LoadLocation, and parse times there with time.ParseInLocation.

Solution 2

Question: How to properly parse time with abbreviated timezone names like UTC, CET, BRT, etc.?

Answer: You better should not. As JimB and others in this question Why doesn't Go's time.Parse() parse the timezone identifier? carefully suggest, you can expect that Go correctly parses only two timezones: UTC and the local one.
What they don't make quite explicit is that you can't expect Go to correctly parse time with any other timezone. At least that is so in my personal experience (go1.16.1, Ubuntu 20.04).

Also, abbreviated timezones are ambiguous. IST could mean India Standard Time, Irish Standard Time or Israel Standard Time. There's no way to disambiguate unless you know zone location, and, if you know location, you should use time.ParseInLocation.

If this is user input and you have control, you should change format requirements for users to input time with explicit offsets as JimB is also suggesting in their answer. Make sure you don't forget about minutes, i.e. use -0700, -07:00, Z0700 or Z07:00 but not -07 or Z07 in layout. Not all offsets are whole hours. For instance, Inidia Standard Time is UTC+5:30.

If you have no other choice and forced to parse such times, you can do something like that:

func parseTimeWithTimezone(layout, value string) (time.Time, error) {
    tt, err := time.Parse(layout, value)
    if err != nil {
        return time.Time{}, err
    }
    loc := tt.Location()
    zone, offset := tt.Zone()
    // Offset will be 0 if timezone is not recognized (or UTC, but that's ok).
    // Read carefully https://pkg.go.dev/time#Parse
    // In this case we'll try to load location from zone name.
    // Timezones that are recognized: local, UTC, GMT, GMT-1, GMT-2, ..., GMT+1, GMT+2, ...
    if offset == 0 {
        // Make sure you have timezone database available in your system for
        // time.LoadLocation to work. Read https://pkg.go.dev/time#LoadLocation
        // about where Go looks for timezone database.
        // Perhaps the simplest solution is to `import _ "time/tzdata"`, but
        // note that it increases binary size by few hundred kilobytes.
        // See https://golang.org/doc/go1.15#time/tzdata
        loc, err = time.LoadLocation(zone)
        if err != nil {
            return time.Time{}, err // or `return tt, nil` if you more prefer
            // the original Go semantics of returning time with named zone
            // but zero offset when timezone is not recognized.
        }
    }
    return time.ParseInLocation(layout, value, loc)
}

Note that zone names that aren't present as files in timezone database will fail parsing. These are quite many. You can see what is present by checking

Share:
27,663
faersons
Author by

faersons

Updated on March 15, 2021

Comments

  • faersons
    faersons about 3 years

    In the example bellow the result is always "[date] 05:00:00 +0000 UTC" regardless the timezone you choose for the parseAndPrint function. What is wrong with this code? The time should change depending on the timezone you choose. (Go Playground servers are apparently configured in UTC timezone).

    http://play.golang.org/p/wP207BWYEd

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        now := time.Now()
        parseAndPrint(now, "BRT")
        parseAndPrint(now, "EDT")
        parseAndPrint(now, "UTC")
    }
    
    func parseAndPrint(now time.Time, timezone string) {
        test, err := time.Parse("15:04:05 MST", fmt.Sprintf("05:00:00 %s", timezone))
        if err != nil {
            fmt.Println(err)
            return
        }
    
        test = time.Date(
            now.Year(),
            now.Month(),
            now.Day(),
            test.Hour(),
            test.Minute(),
            test.Second(),
            test.Nanosecond(),
            test.Location(),
        )
    
        fmt.Println(test.UTC())
    }
    
  • faersons
    faersons over 9 years
    Thanks! I will read the documentation more carefully next time. =)
  • user2099484
    user2099484 about 8 years
    Try: Now := time.Now().UTC().Format("2006-01-02 15:04:05.000000")
  • RubyTuesdayDONO
    RubyTuesdayDONO about 4 years
    this seems to contradict the docs for Time.Parse: "In the absence of a time zone indicator, Parse returns a time in UTC."
  • JimB
    JimB about 4 years
    @RubyTuesdayDONO, No, see comments on the question above. Time zone abbreviations are ambiguous, and mean different things depending on your current location.
  • RubyTuesdayDONO
    RubyTuesdayDONO about 4 years
    thanks for the clarification! and sorry for my confusion. i was thinking of what happens when the parse input date has no timezone code. which is clearly different than the question's example. my bad! 😅