time.Parse and Location

8,050 views
Skip to first unread message

Marvin Renich

unread,
Jan 18, 2012, 4:57:22 PM1/18/12
to golan...@googlegroups.com
One of the major uses for time.Parse is to convert user input (a string)
into a Time value. The user input will rarely include any time zone
information, and when it doesn't, time.Parse will use UTC by default.
This is rarely what the user intended, so the programmer must do
something like:

t := time.Parse(fmt, input)
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(),
t.Second(), t.Nanosecond(), time.Local)

There's actually more to this, as the user might be allowed to use one
of several different formats, so a series of calls to Parse with
different formats, checking for error, must be used.

It would be nice if the time pkg had a function that would take a slice
of format strings, the string to be parsed, and defaults for elided
values, and return a Time value based on the first format string that
fit the user input, using the programmer-provided defaults for values
that the user did not specify.

...Marvin

Kyle Lemons

unread,
Jan 18, 2012, 9:01:50 PM1/18/12
to Marvin Renich, golan...@googlegroups.com
You can use Time.In to change it to time.Local

ziutek

unread,
Jan 19, 2012, 10:02:16 AM1/19/12
to golang-nuts
On 19 Sty, 03:01, Kyle Lemons <kev...@google.com> wrote:
> You can use Time.In to change it to time.Localhttp://tip.golang.org/pkg/time/#Time.In

This doesn't work, because user input isn't in UTC but it is stored as
if it was in UTC.
Time.In only changes Time.loc field. It doesn't convert Time value at
all.

I agree that we need some function for change location and preserve
the presentation form of the time or add something like
defaultLocation parameter to Parse function.

I met with this problem when I need to convert time returned by MySQL
server after text query (it doesn't contain timezone information).

Marvin Renich

unread,
Jan 19, 2012, 10:07:27 AM1/19/12
to golan...@googlegroups.com
* Kyle Lemons <kev...@google.com> [120118 21:06]:

> You can use Time.In to change it to time.Local
> http://tip.golang.org/pkg/time/#Time.In

(I'm subscribed, no need to CC me.)

Thanks, Kyle, for the suggestion.

However, this doesn't do what is needed in the case I gave. Suppose the
user input is "2012-01-19 01:38". He did not bother to specify that the
time zone is EST. Using Parse, I get a Time value that represents the
instant in time "2012-01-19 01:38 UTC". If I apply .In(time.Local) to
this value (where Local is my time zone, EST), I get back a time value
that represents the same instant in time, but in the Eastern time zone:
"2012-01-18 20:38 EST". This is clearly not the time the user intended.

What I want is to be able to tell Parse that the user might input the
date in several possible formats, give Parse the defaults for values
that the user might not enter, and have Parse return the correct time
value. To be useful for handling user input, it must be easy to use
Parse when the specific format is not known at compile time, and the
user must be allowed to omit parts that might reasonably be omitted if
he were writing the date or time on a piece of paper; implied values
should be supplied for him (either by the programmer or by the time
package).

The Unix date command allows specifying a date for the -d option in many
different formats, and it does a good job of figuring out what date was
intended. Try date -d with the following arguments: "3:45",
"3:45pm", "3/21", "3/21 3:45pm", "2010-03-21 15:45", "Mar 21, 2010
15:45", "2010-03-21 15:45 UTC", and "2010-02-21 15:45 UTC". date gives
reasonable and predictable results for each of these.

...Marvin

Kyle Lemons

unread,
Jan 20, 2012, 1:28:31 AM1/20/12
to Marvin Renich, golan...@googlegroups.com
Ah, from looking at the code for In it looked like it simply modified the zone...  Which it does, but that doesn't have the same effect as modifying the zone from an old *time.Time. Sorry for the noise, I still haven't gotten used to the new format in which time is stored.  Something like this is probably what you're looking for:

package main

import (
        "fmt"
        "time"
)

const (
        now = "2012/01/19 10:45:12"
        format = "2006/01/02 15:04:05"
)

func main() {
        _, offset := time.Now().Zone()
        diff := time.Duration(offset) * time.Second

        t, _ := time.Parse(format, now)
        fmt.Println("Now (parsed):   ", t)
        fmt.Println("Now (modified): ", t.Add(-diff).In(time.Local))
}

You mentioned that some of your formats might actually have the time zone, so you could check if (a) the format contains "MST" or if the offset of the parsed time is nonzero before you make the above correction.  Or just store booleans of whether to adjust them along with the format.

ziutek

unread,
Jan 20, 2012, 5:01:22 AM1/20/12
to golang-nuts
On 20 Sty, 07:28, Kyle Lemons <kev...@google.com> wrote:
> const (
>         now = "2012/01/19 10:45:12"
>         format = "2006/01/02 15:04:05"
> )
>
> func main() {
>         _, offset := time.Now().Zone()
>         diff := time.Duration(offset) * time.Second
>
>         t, _ := time.Parse(format, now)
>         fmt.Println("Now (parsed):   ", t)
>         fmt.Println("Now (modified): ", t.Add(-diff).In(time.Local))
>
> }

This code contains bug. offset isn't constants. It is some function of
time. So you can't use _, offset := time.Now().Zone() to obtain offset
for "2006/01/02 15:04:05".

Kyle Lemons

unread,
Jan 20, 2012, 12:51:02 PM1/20/12
to ziutek, golang-nuts
It is intended for the assumption that a time containing no zone is to be interpreted as having been measured relative to the time.Local zone.  It behaves the same as the OP's code but without deconstructing and reconstructing the date fields.

ziutek

unread,
Jan 20, 2012, 2:19:40 PM1/20/12
to golang-nuts
On 20 Sty, 18:51, Kyle Lemons <kev...@google.com> wrote:
> It is intended for the assumption that a time containing no zone is to be
> interpreted as having been measured relative to the time.Local zone.  It
> behaves the same as the OP's code but without deconstructing and
> reconstructing the date fields.

Offset may be different in winter and in summer. Your algorithm may
work wrong if time.Now() is in winter but user enters summer date.

Kyle Lemons

unread,
Jan 20, 2012, 2:33:06 PM1/20/12
to ziutek, golang-nuts
Offset may be different in winter and in summer. Your algorithm may
work wrong if time.Now() is in winter but user enters summer date.

Without the zone in what the user enters (and the format), it is impossible to distinguish.

Kyle Lemons

unread,
Jan 20, 2012, 2:33:56 PM1/20/12
to ziutek, golang-nuts
Also, if it is in summer, time.Now returns the proper time zone and if it is winter, time.Now also returns the time zone.  The offset will be different and appropriate.

ziutek

unread,
Jan 20, 2012, 2:53:35 PM1/20/12
to golang-nuts
If user enters time without zone and you assume that this time is in
Local zone, the following code:

t = time.Date(Year, Month, Day, Hour, Minute, Second, Nanosecond,
time.Local)

works well. time.Date function is clever enough to obtain correct
offset for specified time and location (except for two hours in the
year).

Think about why we have time.Zone function and not Location.Zone.

ziutek

unread,
Jan 20, 2012, 3:15:53 PM1/20/12
to golang-nuts
On 20 Sty, 20:33, Kyle Lemons <kev...@google.com> wrote:
> Also, if it is in summer, time.Now returns the proper time zone and if it
> is winter, time.Now also returns the time zone.  The offset will be
> different and appropriate.

Suppose that today (2012-01-20) some user enters two times:

2012-03-24 12:00:00
2012-03-25 12:00:00

we assume that these times are in Local timezone. Following code:

t1 := time.Date(2012, 3, 24, 12, 0, 0, 0, time.Local)
t2 := time.Date(2012, 3, 25, 12, 0, 0, 0, time.Local)
fmt.Println(t1.Zone())
fmt.Println(t2.Zone())

on my computer prints:

CET 3600
CEST 7200

You see that you need to use different offset to correct UTC time
obtained from time.Parse but your code uses the same offset (3600 s).

roger peppe

unread,
Jan 20, 2012, 3:31:07 PM1/20/12
to ziutek, golang-nuts
here's another, maybe dirty, but kinda cool way to do it.

if you know that some time string, say t, parsed with a given format, say f,
then you can get that time in some arbitrary other time zone,
by appending the time zone to both t and f.

in fact you can use this technique to add any information
that's missing in one format to another.

here's an example that goes through a list of time formats
and returns the first one that matches, with any missing
information taken from the current time.

package main

import (
"fmt"
"time"
)

type formatInfo struct {
format string
needed string // all the missing information from the format
}

var formats = []formatInfo{
{time.ANSIC, " -0700"},
{time.UnixDate, ""},
{time.RubyDate, ""},
{time.RFC822, ""},
{time.RFC822Z, ""},
{time.RFC850, ""},
{time.RFC1123, ""},
{time.RFC1123Z, ""},
{time.RFC3339, ""},
{time.Kitchen, " 05 -0700 01/02/2006"},
{"15:04", " 05 -0700 01/02/2006"},
{"02/01/2006", " -0700 15:04:05"},
}

func parse(s string, deflt time.Time) (time.Time, error) {
for _, f := range formats {
format := f.format
t, err := time.Parse(format, s)
if err != nil {
continue
}
t, err = time.Parse(f.format+f.needed, s+deflt.Format(f.needed))
if err != nil {
panic("unexpectedly failed parse")
}
return t, nil
}
return time.Time{}, fmt.Errorf("failed to parse time")
}

func main() {
t, err := parse("23:45", time.Now())
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("%v\n", t)
}
}

ziutek

unread,
Jan 20, 2012, 4:05:36 PM1/20/12
to golang-nuts
On 20 Sty, 21:31, roger peppe <rogpe...@gmail.com> wrote:
> if you know that some time string, say t, parsed with a given format, say f,
> then you can get that time in some arbitrary other time zone,
> by appending the time zone to both t and f.

I tried this:

format := "2006-01-02 15:04:05 MST"
t1, err := time.Parse(format, "2012-03-24 12:00:00" + " CET")
t2, err := time.Parse(format, "2012-03-25 12:00:00" + " CET")
fmt.Println(t1)
fmt.Println(t2)

and I obtain:

Sat Mar 24 12:00:00 +0100 CET 2012
Sun Mar 25 12:00:00 +0000 CET 2012

Second date is wrong. So I still need to known that i need to append
"CEST" to second date to obtain proper result:

Sun Mar 25 12:00:00 +0200 CEST 2012

If i use numeric format for timezone and add "+0100" to both times I
obtain:

Sat Mar 24 12:00:00 +0100 CET 2012
Sun Mar 25 12:00:00 +0100 +0100 2012

It seems that there isn't better solution to assume some default
timezone for user input (or for MySQL text output in my case) than use
time.Parse followed by time.Date.


roger peppe

unread,
Jan 20, 2012, 4:15:23 PM1/20/12
to ziutek, golang-nuts
On 20 January 2012 21:05, ziutek <ziu...@lnet.pl> wrote:
> On 20 Sty, 21:31, roger peppe <rogpe...@gmail.com> wrote:
>> if you know that some time string, say t, parsed with a given format, say f,
>> then you can get that time in some arbitrary other time zone,
>> by appending the time zone to both t and f.
>
> I tried this:
>
> format := "2006-01-02 15:04:05 MST"
> t1, err := time.Parse(format, "2012-03-24 12:00:00" + " CET")
> t2, err := time.Parse(format, "2012-03-25 12:00:00" + " CET")
> fmt.Println(t1)
> fmt.Println(t2)
>
> and I obtain:
>
> Sat Mar 24 12:00:00 +0100 CET 2012
> Sun Mar 25 12:00:00 +0000 CET 2012
>
> Second date is wrong.

it looks right to me. how is it wrong?

ziutek

unread,
Jan 20, 2012, 4:24:13 PM1/20/12
to golang-nuts
> it looks right to me. how is it wrong?

This is because Warsaw is a little east of Greenwich :)

Correct output should be:

Sat Mar 24 12:00:00 +0100 CET 2012
Sun Mar 25 12:00:00 +0200 CEST 2012

roger peppe

unread,
Jan 20, 2012, 4:24:42 PM1/20/12
to ziutek, golang-nuts
On 20 January 2012 21:05, ziutek <ziu...@lnet.pl> wrote:
> On 20 Sty, 21:31, roger peppe <rogpe...@gmail.com> wrote:
>> if you know that some time string, say t, parsed with a given format, say f,
>> then you can get that time in some arbitrary other time zone,
>> by appending the time zone to both t and f.
>
> I tried this:
>
> format := "2006-01-02 15:04:05 MST"
> t1, err := time.Parse(format, "2012-03-24 12:00:00" + " CET")
> t2, err := time.Parse(format, "2012-03-25 12:00:00" + " CET")
> fmt.Println(t1)
> fmt.Println(t2)
>
> and I obtain:
>
> Sat Mar 24 12:00:00 +0100 CET 2012
> Sun Mar 25 12:00:00 +0000 CET 2012
>
> Second date is wrong. So I still need to known that i need to append
> "CEST" to second date to obtain proper result:

oh, i've just realised, you're saying that the time
zone name is wrong, not the date..

unfortunately the time package doesn't know
how to map from time instant to zone name (and in
general there's no correct such mapping), so unless you know the
default time zone name you can't do much better.

but if you ignore the time zone names and use
numeric offsets only, i think the technique works
ok.

> It seems that there isn't better solution to assume some default
> timezone for user input (or for MySQL text output in my case) than use
> time.Parse followed by time.Date.

how is using time.Date better (other than being somewhat
more efficient) ?

ziutek

unread,
Jan 20, 2012, 4:44:47 PM1/20/12
to golang-nuts
On 20 Sty, 22:24, roger peppe <rogpe...@gmail.com> wrote:
> how is using time.Date better (other than being somewhat
> more efficient) ?

Because this works:

format := "2006-01-02 15:04:05"
t1 := time.Parse(format, "2012-03-24 12:00:00")
t1 = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(),
t.Second(), t.Nanosecond(), time.Local)
t2 := time.Parse(format, "2012-03-25 12:00:00")
t2 = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(),
t.Second(), t.Nanosecond(), time.Local)

and you don't need to know any offset or timezone name. You need to
known only proper location (eg Poland or GB) or use time.Local:

loc, _ := time.LoadLocation("Poland")
fmt.Println(time.Date(2012, 3, 24, 12, 0, 0, 0, loc))
fmt.Println(time.Date(2012, 3, 25, 12, 0, 0, 0, loc))

prints:

Sat Mar 24 12:00:00 +0100 CET 2012
Sun Mar 25 12:00:00 +0200 CEST 2012

For loc, _ := time.LoadLocation("GB") it prints:

Sat Mar 24 12:00:00 +0000 GMT 2012
Sun Mar 25 12:00:00 +0100 BST 2012

ziutek

unread,
Jan 20, 2012, 4:53:37 PM1/20/12
to golang-nuts
On 20 Sty, 22:24, roger peppe <rogpe...@gmail.com> wrote:
> but if you ignore the time zone names and use
> numeric offsets only, i think the technique works
> ok.

Yes but you need to known the offset, which may be different during
summer and winter. time.Zone function performs the appropriate
calculations but you have string not time.Time.

Marvin Renich

unread,
Jan 21, 2012, 11:27:07 AM1/21/12
to golang-nuts
* ziutek <ziu...@Lnet.pl> [120120 16:57]:

Sorry, I was gone all day.

zuitek, thank you for explaining the subtleties of this so well; you
clearly understand why I believe the "Right" place for this is in the
time package and not in user code.

I actually thought of roger's solution and rejected it for the reason
you give. A variation of roger's code that does work is to use
time.Parse() with different strings until one works, then use
time.Date() to mix the parsed values with the correct defaults,
including Location rather than an offset. This is still cumbersome and
is code that should be written once in the system library rather than
multiple times, half of them wrong, in user code.

I've read the contribution guidelines, so I'm asking here what people
think about a new function in the time package that parses using a slice
of strings for the possible formats and a set of individual default
values. I don't have a good name for the method; suggestions are
encouraged. ParseMulti or ParseMultiFormat comes to mind. My suggested
signature is

func ParseMultiFormat(layouts []string, value string, year int, month Month,
day, hour, min, sec, nsec int, loc *Location) (Time, error)

I would include an exported variable that includes most or all of the
pre-defined formats. (Suggestions for the name? MultiFormats?)

Would such a function be welcome in the time package? Is it too late
for Go 1?

...Marvin

Kyle Lemons

unread,
Jan 21, 2012, 2:50:57 PM1/21/12
to Marvin Renich, golang-nuts
I think that API solves a very focused problem and that a better one could be designed which handles the broader issue.  A few possibilities:

// ParseInto parses the text according to the given format, using the original Time as a reference when
// part of the date or time cannot be determined from the format.
func ParseInto(original Time, format, text string) Time { ... }

// Parse parses the text according to the format, using t as a reference when part of the date or time
// cannot be determined due to the format.
func (t Time) Parse(format, text string) Time { ... }

In either case, it allows you to pretty easily solve your problem along with many others (given a wall clock time, interpret it with respect to a given date, etc).

Rémy Oudompheng

unread,
Jan 21, 2012, 3:13:12 PM1/21/12
to Kyle Lemons, Marvin Renich, golang-nuts
On 2012/1/21 Kyle Lemons <kev...@google.com> wrote:
> I think that API solves a very focused problem and that a better one could
> be designed which handles the broader issue.  A few possibilities:
>
> // ParseInto parses the text according to the given format, using the
> original Time as a reference when
> // part of the date or time cannot be determined from the format.
> func ParseInto(original Time, format, text string) Time { ... }
>
> // Parse parses the text according to the format, using t as a reference
> when part of the date or time
> // cannot be determined due to the format.
> func (t Time) Parse(format, text string) Time { ... }
>
> In either case, it allows you to pretty easily solve your problem along with
> many others (given a wall clock time, interpret it with respect to a given
> date, etc).

You can also write a setter in the following way:

func OverrideLocation(t time.Time, loc *time.Location) (time.Time) {
y, m, d := t.Date()
H, M, S := t.Clock()
return time.Date(y, m, d, H, M, S, t.Nanoseconds(), loc)
}

I'd say it's a bit easier to digest than:

return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(),
t.Second(), t.Nanoseconds(), loc)

that was proposed earlier. Maybe I'd prefer to add the reciprocal of Date():

func (t Time)Split() (year int, month Month, day, hour, min, sec, nsec
int, loc *Location)

Rémy.

ziutek

unread,
Jan 21, 2012, 3:50:54 PM1/21/12
to golang-nuts
On 21 Sty, 17:27, Marvin Renich <m...@renich.org> wrote:
> I've read the contribution guidelines, so I'm asking here what people
> think about a new function in the time package that parses using a slice
> of strings for the possible formats and a set of individual default
> values.  I don't have a good name for the method; suggestions are
> encouraged.  ParseMulti or ParseMultiFormat comes to mind.  My suggested
> signature is
>
> func ParseMultiFormat(layouts []string, value string, year int, month Month,
>         day, hour, min, sec, nsec int, loc *Location) (Time, error)
>
> I would include an exported variable that includes most or all of the
> pre-defined formats.  (Suggestions for the name?  MultiFormats?)
>
> Would such a function be welcome in the time package?  Is it too late
> for Go 1?

I think that the current implementation of time.Parse function isn't
very useful to generate time, because we rarely expect that user
enters date with timezone (except when we parse date generated by
another program). Cases where the user need to enter time in UTC are
rare to. In most cases current implementation of time.Parse is used
only to obtain Y, m, d, H, M, S and pass them to the time.Date.

To be more useful time.Parse should accept the default location
parameter or after all it should assume that the default location is
Local.

In my opinion we don't need to have more complex parsers in Go 1 but
we really need to fix time.Parse.

ziutek

unread,
Jan 21, 2012, 4:21:25 PM1/21/12
to golang-nuts
On 21 Sty, 21:13, Rémy Oudompheng <remyoudomph...@gmail.com> wrote:
> You can also write a setter in the following way:
>
> func OverrideLocation(t time.Time, loc *time.Location) (time.Time) {
>   y, m, d := t.Date()
>   H, M, S := t.Clock()
>   return time.Date(y, m, d, H, M, S, t.Nanoseconds(), loc)
>
> }
Your function is identical to my function in mymysql:

https://github.com/ziutek/mymysql/blob/master/mysql/row.go#L202

But we still call time.Date twice: first to obtain t from string (in
time.Parse) and second to convert t using specified location.
And we still need to call t.Date and t.Clock, but this time only once.

ziutek

unread,
Jan 21, 2012, 7:19:56 PM1/21/12
to golang-nuts
My proposal to add an additional parameter to time.Parse was
immediately rejected: http://codereview.appspot.com/5565043/

It seems that time.Parse is intended mainly for well defined
timestamps from network protocols or file formats. So we need to use
the Parse-Date trick or if we need more efficient solution we need to
write our own parser.
Reply all
Reply to author
Forward
0 new messages