A Go Puzzler: time.IsZero, the year zero and negative times.

1,839 views
Skip to first unread message

decitrig

unread,
Jan 12, 2013, 11:19:10 PM1/12/13
to golan...@googlegroups.com
I ran into some confusing behavior today when trying to add time.Time values together: time.Time{} has the year 0001, but t, _ := time.Parse("15:04", "15:00") has the year 0000. This is how it's documented, but it's very surprising (and appears to break the appengine datastore viewer).

In other words, the output of http://play.golang.org/p/wRGCyR0lxl is really surprising, until you realize that giving time.Parse hours & minutes but no date results in a negative time. That code is based on something I was writing which was trying to add a time-only value to a date-only value (much more awkward than I thought it would be), and I discovered you need to add a year to your time-only value before subtracting the zero time from it.

As I said, everything's behaving as documented, it just seems inconsistent, and like it might be worth calling out more emphatically in the docs. In my opinion, for what it's worth, time.Parse("", "") should return time.Time{}, but it may be that we're stuck with the existing behavior.

peterGo

unread,
Jan 13, 2013, 1:45:47 AM1/13/13
to golan...@googlegroups.com
decitrig,


"trying to add a time-only value to a date-only value"

package main

import (
    "fmt"
    "time"
)

// DateTime returns the time using the date from date d and the time from time t.
func DateTime(d, t time.Time) time.Time {
    return time.Date(
        d.Year(), d.Month(), d.Day(),
        t.Hour(), t.Minute(), t.Second(), t.Nanosecond(),
        t.Location(),
    )
}

func main() {
    t, err := time.Parse("2006-01-02", "2012-01-01")
    if err != nil {
        fmt.Println(err)
        return
    }
    u, err := time.Parse("15:04", "15:00")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(DateTime(t, u))
}

Output:

2012-01-01 15:00:00 +0000 UTC

Peter

decitrig

unread,
Jan 13, 2013, 9:45:08 AM1/13/13
to golan...@googlegroups.com
On Sunday, January 13, 2013 1:45:47 AM UTC-5, peterGo wrote:
decitrig,

"trying to add a time-only value to a date-only value"

I certainly agree that my original algorithm isn't the best, but my point is not how to add times to dates. It's that the semantics of parsing "15:00" vs creating a time.Time{} value are probably more confusing than they aught.

Jan Mercl

unread,
Jan 13, 2013, 9:50:29 AM1/13/13
to decitrig, golang-nuts
On Sun, Jan 13, 2013 at 3:45 PM, decitrig <rws...@gmail.com> wrote:
> I certainly agree that my original algorithm isn't the best, but my point is
> not how to add times to dates. It's that the semantics of parsing "15:00" vs
> creating a time.Time{} value are probably more confusing than they aught.

I think that when you want to do date arithmetic you should rather use
a `time.Duration` to express the deltas.

-j

Ryan Sims

unread,
Jan 13, 2013, 10:04:02 AM1/13/13
to Jan Mercl, golang-nuts
Yes, but I don't want to store "9:30am" as a Duration.

Jan Mercl

unread,
Jan 13, 2013, 10:05:50 AM1/13/13
to Ryan Sims, golang-nuts
On Sun, Jan 13, 2013 at 4:04 PM, Ryan Sims <rws...@gmail.com> wrote:
> Yes, but I don't want to store "9:30am" as a Duration.

Why not? It's duration "9h30m".

-j

Ryan Sims

unread,
Jan 13, 2013, 10:16:21 AM1/13/13
to Jan Mercl, golang-nuts
Then why have a Time type at all, since all times can be expressed as durations? When doing time.Parse("15:04", "15:00"), I imagine people rarely have the explicit intent of expression 3:00pm on 1/1/0 UTC, i.e. a specific point in time. I certainly wouldn't prompt a user for a time of day expressed as a duration. If we want to represent a specific time of day, without reference to a specific date, I would think 99% of us would reach for a time.Time value, not a Duration.

Jan Mercl

unread,
Jan 13, 2013, 10:23:53 AM1/13/13
to Ryan Sims, golang-nuts
On Sun, Jan 13, 2013 at 4:16 PM, Ryan Sims <rws...@gmail.com> wrote:
> Then why have a Time type at all, since all times can be expressed as
> durations?

Actually no `time.Time` can be fully expressed by `time.Duration`.
They are fundamentally different. Durations have no locations.

> When doing time.Parse("15:04", "15:00"), I imagine people rarely
> have the explicit intent of expression 3:00pm on 1/1/0 UTC, i.e. a specific
> point in time. I certainly wouldn't prompt a user for a time of day
> expressed as a duration.

Yes as that's not a duration. If you are adding 15 hours to time,
you're adding a duration, not a time.

> If we want to represent a specific time of day,
> without reference to a specific date, I would think 99% of us would reach
> for a time.Time value, not a Duration.

A specific time of day can be expressed by e.g. struct { hour, minute
int }, but a date-less time is a quite poor fit for time.Time. IOW,
time.Time is in no way not designed for that. Look at the internals of
it (http://golang.org/src/pkg/time/time.go?s=1541:2055#L24), there is
nothing there what can properly express a time w/o date.

-j

Ryan Sims

unread,
Jan 13, 2013, 10:41:12 AM1/13/13
to Jan Mercl, golang-nuts
On Sun, Jan 13, 2013 at 10:23 AM, Jan Mercl <0xj...@gmail.com> wrote:
On Sun, Jan 13, 2013 at 4:16 PM, Ryan Sims <rws...@gmail.com> wrote:
> Then why have a Time type at all, since all times can be expressed as
> durations?

Actually no `time.Time` can be fully expressed by `time.Duration`.
They are fundamentally different. Durations have no locations.

> When doing time.Parse("15:04", "15:00"), I imagine people rarely
> have the explicit intent of expression 3:00pm on 1/1/0 UTC, i.e. a specific
> point in time. I certainly wouldn't prompt a user for a time of day
> expressed as a duration.

Yes as that's not a duration. If you are adding 15 hours to time,
you're adding a duration, not a time.

Right. So in my original algorithm, I tried to convert a time to a duration by subtracting the zero time. This didn't work as expected, because parsing "15:04" results in a negative time: this is the odd behavior I wanted to highlight. Whether I'm misusing time.Time in this specific case, I think it's reasonable to assume that time.Parse has the same zero time as time.Time{}, which it doesn't.

Jan Mercl

unread,
Jan 13, 2013, 10:51:29 AM1/13/13
to Ryan Sims, golang-nuts
On Sun, Jan 13, 2013 at 4:41 PM, Ryan Sims <rws...@gmail.com> wrote:
> Right. So in my original algorithm, I tried to convert a time to a duration
> by subtracting the zero time. This didn't work as expected, because parsing
> "15:04" results in a negative time: this is the odd behavior I wanted to
> highlight. Whether I'm misusing time.Time in this specific case, I think
> it's reasonable to assume that time.Parse has the same zero time as
> time.Time{}, which it doesn't.

It still makes no sense for me to parse "15:04" as time.Time as it is
not a time, only a time of day. Maybe let's start from a different
origin. What are your inputs and what should be the output of this
part of your code? Please avoid referring to any specific
implementation and/or types. That could help someone to propose a
solution hopefully.

-j

Ryan Sims

unread,
Jan 13, 2013, 11:01:01 AM1/13/13
to Jan Mercl, golang-nuts
On Sun, Jan 13, 2013 at 10:51 AM, Jan Mercl <0xj...@gmail.com> wrote:
On Sun, Jan 13, 2013 at 4:41 PM, Ryan Sims <rws...@gmail.com> wrote:
> Right. So in my original algorithm, I tried to convert a time to a duration
> by subtracting the zero time. This didn't work as expected, because parsing
> "15:04" results in a negative time: this is the odd behavior I wanted to
> highlight. Whether I'm misusing time.Time in this specific case, I think
> it's reasonable to assume that time.Parse has the same zero time as
> time.Time{}, which it doesn't.

It still makes no sense for me to parse "15:04" as time.Time as it is
not a time, only a time of day.

But it's perfectly valid input for time.Parse; that specific example is even in the documentation for time.Parse: "parsing "3:04pm" returns the time corresponding to Jan 1, year 0, 15:04:00 UTC."
 
Maybe let's start from a different
origin. What are your inputs and what should be the output of this
part of your code? Please avoid referring to any specific
implementation and/or types. That could help someone to propose a
solution hopefully.

-j

I'm really not looking for a solution: I think the time package is inconsistent with itself w.r.t. zero times. Specifically, time.Parse considers the zero time to be the year 0, while time.IsZero() considers it to be the year 1. I agree with you entirely that I've misinterpreted what time.Time can represent, but a) I don't think it's an unreasonable mistake to make and b) the inconsistency between time.Parse & time.Time is what I'm trying to highlight. I have live data with the incorrect usage of time.Time, so I just have to live with it & code around it - I can't redesign my data model now.

minux

unread,
Jan 13, 2013, 11:05:00 AM1/13/13
to Ryan Sims, Jan Mercl, golang-nuts
On Sun, Jan 13, 2013 at 11:41 PM, Ryan Sims <rws...@gmail.com> wrote:
Right. So in my original algorithm, I tried to convert a time to a duration by subtracting the zero time. This didn't work as expected, because parsing "15:04" results in a negative time: this is the odd behavior I wanted to highlight. Whether I'm misusing time.Time in this specific case, I think it's reasonable to assume that time.Parse has the same zero time as time.Time{}, which it doesn't.
You're making assumptions about the internals of time package but it is not
documented.

in fact, a zero time != time.Time{}, and that's why time.IsZero() exists.
if you want to extract the duration correctly, you should subtract it with a zero time (time.Parse("15:04", "0:00")),
then you don't need to make the (invalid) assumption.

Ryan Sims

unread,
Jan 13, 2013, 11:10:35 AM1/13/13
to minux, Jan Mercl, golang-nuts
On Sun, Jan 13, 2013 at 11:05 AM, minux <minu...@gmail.com> wrote:

On Sun, Jan 13, 2013 at 11:41 PM, Ryan Sims <rws...@gmail.com> wrote:
Right. So in my original algorithm, I tried to convert a time to a duration by subtracting the zero time. This didn't work as expected, because parsing "15:04" results in a negative time: this is the odd behavior I wanted to highlight. Whether I'm misusing time.Time in this specific case, I think it's reasonable to assume that time.Parse has the same zero time as time.Time{}, which it doesn't.
You're making assumptions about the internals of time package but it is not
documented.

in fact, a zero time != time.Time{}, and that's why time.IsZero() exists.

"The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC"
I don't see why time.Parse should play by different rules.
 
if you want to extract the duration correctly, you should subtract it with a zero time (time.Parse("15:04", "0:00")),
then you don't need to make the (invalid) assumption.

I agree it's invalid. But the fact that it's invalid seems inconsistent with the rest of the package.

minux

unread,
Jan 13, 2013, 11:27:58 AM1/13/13
to Ryan Sims, Jan Mercl, golang-nuts
On Mon, Jan 14, 2013 at 12:10 AM, Ryan Sims <rws...@gmail.com> wrote:
"The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC"
yes, but it's not the zero time adopted by time.Parse, see below.
I don't see why time.Parse should play by different rules.
Quoting docs for time.Parse: 
// Elements omitted from the value are assumed to be zero or, when
// zero is impossible, one, so parsing "3:04pm" returns the time
// corresponding to Jan 1, year 0, 15:04:00 UTC.

Ryan Sims

unread,
Jan 13, 2013, 11:33:40 AM1/13/13
to minux, Jan Mercl, golang-nuts
Yes, I *know* Parse has a different zero time, *that's why* I started this thread. I don't think it *should* have a different zero time, and I think it's confusing that it does.

Let me put it this way: why *should* time.Parse("", "") be different than time.Time{}? Principle of least surprise seems to suggest they should be the same. 

minux

unread,
Jan 13, 2013, 1:00:08 PM1/13/13
to Ryan Sims, Jan Mercl, golang-nuts
On Mon, Jan 14, 2013 at 12:33 AM, Ryan Sims <rws...@gmail.com> wrote:
Yes, I *know* Parse has a different zero time, *that's why* I started this thread. I don't think it *should* have a different zero time, and I think it's confusing that it does.

Let me put it this way: why *should* time.Parse("", "") be different than time.Time{}? Principle of least surprise seems to suggest they should be the same. 
As the docs of Parse explains: every missing parameter will be treated as 0, or 1 if 0 is impossible,
this is very reasonable choice for this API.

as for what time is represented by time.Time{}, i think this is an implementation detail, and the user
shouldn't care about it.

Kyle Lemons

unread,
Jan 13, 2013, 1:04:32 PM1/13/13
to Ryan Sims, minux, Jan Mercl, golang-nuts
Even if it is (and I'm not saying I agree with you) it's too late to change it now.

Time Is Hard. Making any assumptions when performing date math is just asking for heartache, especially when that assumption is about epochs (the "zero time").  It would be equally valid for time.Time{} to be Jan 1 1970, in which case I think the question of time.Parse("", "") returning 1970 would be obviously wrong.  The two are not (and need not be) correlated in the way you expect.  As was said earlier, if you want a particular time on a particular day in a particular location, create it using time.Date(...), don't try to calculate it yourself unless you want to reimplement all of the intricacies of time.

Dan Kortschak

unread,
Jan 13, 2013, 2:36:00 PM1/13/13
to Kyle Lemons, Ryan Sims, minux, Jan Mercl, golang-nuts
Parsing a time to year 0000 just seems wrong. In our calendar, there is no such year.

minux

unread,
Jan 13, 2013, 2:43:31 PM1/13/13
to Dan Kortschak, Kyle Lemons, Ryan Sims, Jan Mercl, golang-nuts
On Mon, Jan 14, 2013 at 3:36 AM, Dan Kortschak <dan.ko...@adelaide.edu.au> wrote:
Parsing a time to year 0000 just seems wrong. In our calendar, there is no such year.
perhaps the authors were thinking about ISO 8601 when choosing the default year.

Dan Kortschak

unread,
Jan 13, 2013, 2:57:36 PM1/13/13
to minux, Kyle Lemons, Ryan Sims, Jan Mercl, golang-nuts
All I can say is that the ISO make some very strange and wrong decisions; representing -1 as 0?

Ryan Sims

unread,
Jan 13, 2013, 4:49:07 PM1/13/13
to Kyle Lemons, golang-nuts, Jan Mercl, minux


On Jan 13, 2013 1:04 PM, "Kyle Lemons" <kev...@google.com> wrote:
>
> On Sun, Jan 13, 2013 at 8:33 AM, Ryan Sims <rws...@gmail.com> wrote:
>>
>> On Sun, Jan 13, 2013 at 11:27 AM, minux <minu...@gmail.com> wrote:
>>>
>>>
>>> On Mon, Jan 14, 2013 at 12:10 AM, Ryan Sims <rws...@gmail.com> wrote:
>>>>
>>>> "The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC"
>>>
>>> yes, but it's not the zero time adopted by time.Parse, see below.
>>>>
>>>> http://play.golang.org/p/fVvMI3qV-u
>>>> I don't see why time.Parse should play by different rules.
>>>
>>> Quoting docs for time.Parse: 
>>> // Elements omitted from the value are assumed to be zero or, when
>>> // zero is impossible, one, so parsing "3:04pm" returns the time
>>> // corresponding to Jan 1, year 0, 15:04:00 UTC.
>>
>>
>> Yes, I *know* Parse has a different zero time, *that's why* I started this thread. I don't think it *should* have a different zero time, and I think it's confusing that it does.
>>
>> Let me put it this way: why *should* time.Parse("", "") be different than time.Time{}? Principle of least surprise seems to suggest they should be the same.
>
>
> Even if it is (and I'm not saying I agree with you) it's too late to change it now.

Of course it is. I'm really not trying to be obtuse here. I think this is legitimately confusing and worth singling out. I referred to it as a "puzzler" with a nod to the Java equivalent: its not a bug to fix, its a gotcha to be aware of. My point is not that its broken, its that its inconsistent and subtle. I made some incorrect assumptions, but in my defense I think the API, while perfectly reasonable and elegant, makes it easy to make those wrong assumptions.

minux

unread,
Jan 13, 2013, 4:58:01 PM1/13/13
to Ryan Sims, Kyle Lemons, golang-nuts, Jan Mercl
On Mon, Jan 14, 2013 at 5:49 AM, Ryan Sims <rws...@gmail.com> wrote:

Of course it is. I'm really not trying to be obtuse here. I think this is legitimately confusing and worth singling out. I referred to it as a "puzzler" with a nod to the Java equivalent: its not a bug to fix, its a gotcha to be aware of. My point is not that its broken, its that its inconsistent and subtle. I made some incorrect assumptions, but in my defense I think the API, while perfectly reasonable and elegant, makes it easy to make those wrong assumptions.

Ok, I mailed a CL to explicitly point this out in the docs.

Let's see what others say.

Kevin Gillette

unread,
Jan 13, 2013, 11:52:52 PM1/13/13
to golan...@googlegroups.com, minux, Jan Mercl
The time.Parse behavior of using year zero for inputs without a specified year does make some sense depending on one's perspective, despite the 'zero time' starting at the beginning of year one. I say it makes sense, because year zero does not exist in the Gregorian calendar system (http://en.wikipedia.org/wiki/Year_Zero) -- thus a time without a year could be assigned zero to refer to a time that does not fit on the calendar system in use.

The implementation is still problematic regarding this interpretation, since the time package treats all dates within its range continuously -- what Go calls year zero would otherwise be called 1 BC(E). If, on the other hand, year -1 mapped to 1 BC(E), then 0 could viably be "out of calendar".

minux

unread,
Jan 18, 2013, 4:00:55 PM1/18/13
to Ryan Sims, Kyle Lemons, golang-nuts, Jan Mercl

On Mon, Jan 14, 2013 at 5:58 AM, minux <minu...@gmail.com> wrote:
> Ok, I mailed a CL to explicitly point this out in the docs.
> https://codereview.appspot.com/7101046/
Good news!
The CL accepted and just committed. Thank you for your persistence.

now the docs for time.Parse reads:
func Parse(layout, value string) (Time, error)
    // snip
    Elements omitted from the value are assumed to be zero or, when zero is
    impossible, one, so parsing "3:04pm" returns the time corresponding to
    Jan 1, year 0, 15:04:00 UTC **(note that because the year is 0, this time
    is before the zero Time)
**. Years must be in the range 0000..9999. The day
    of the week is checked for syntax but it is otherwise ignored.
Reply all
Reply to author
Forward
0 new messages