Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

time: documentation shows incorrect formatting of RFC3339 that does not parse #22135

Closed
joelpresence opened this issue Oct 4, 2017 · 11 comments

Comments

@joelpresence
Copy link

joelpresence commented Oct 4, 2017

Please answer these questions before submitting your issue. Thanks!

What version of Go are you using (go version)?

go version go1.8.3 darwin/amd64

Does this issue reproduce with the latest release?

Yes, as far as I know.

What operating system and processor architecture are you using (go env)?

GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/joel/go"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/mz/tzh0vwxd08jgh0lx24v39gx40000gn/T/go-build013964967=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"

What did you do?

Look at the documentation for the go time package at https://golang.org/pkg/time/#pkg-constants where you'll see:

const (
...
RFC3339     = "2006-01-02T15:04:05Z07:00"
...
)

Now, run the following program:

package main

import (
	"fmt"
	"time"
)

func main() {
	// From https://golang.org/pkg/time/#pkg-constants
	//
	//     RFC3339     = "2006-01-02T15:04:05Z07:00"
	timeStr := "2006-01-02T15:04:05Z07:00"

	// time.Parse fails on the sentinel example given in godoc
	parsed, errParsed := time.Parse(timeStr, time.RFC3339)
	fmt.Printf("parsed %v errParsed %v\n", parsed, errParsed)

	loc, errLoc := time.LoadLocation("MST")
	fmt.Printf("loc %v errLoc %v\n", loc, errLoc)

	t := time.Date(2006, 1, 2, 15, 4, 5, 0, loc)
	tStr := t.Format(time.RFC3339)

	fmt.Printf("tStr %v timeStr %v tStr == timeStr %v\n", tStr, timeStr, tStr == timeStr)

	// time.Parse fails on its own output that was just created
	parsed, errParsed = time.Parse(tStr, time.RFC3339)
	fmt.Printf("parsed %v errParsed %v\n", parsed, errParsed)
}

You will get the following output:

parsed 0001-01-01 00:00:00 +0000 UTC errParsed parsing time "2006-01-02T15:04:05Z07:00": extra text: 07:00
loc MST errLoc <nil>
tStr 2006-01-02T15:04:05-07:00 timeStr 2006-01-02T15:04:05Z07:00 tStr == timeStr false
parsed 0001-01-01 00:00:00 +0000 UTC errParsed parsing time "2006-01-02T15:04:05Z07:00" as "2006-01-02T15:04:05-07:00": cannot parse "" as "-07:00"

This is a clear and strong violation of the principle of least surprise. It is a strong expectation of go coders that the following loop will work:

var t time.Time = ...

// Format your time to a string
tString := t.Format(time.RFC3339)

// Your formatted time should ALWAYS parse successfully
// using the format you just formatted it with.
parsed, errParsed := time.Parse(time.RFC3339)

// errParsed MUST be nil here
// parsed should NOT be nil and SHOULD be equivalent to t

If I format a time into a string according to some format F, then I attempt to parse that untouched string using the format F I should ALWAYS be able to parse it back into the original time (or at least something equivalent where Parse(t.Format(F), F) does NOT error.

If possible, provide a recipe for reproducing the error.
A complete runnable program is good.
A link on play.golang.org is best.

What did you expect to see?

Parsing a time string you just formatted should successfully parse.

What did you see instead?

Parsing a time string you just formatted errors. Moreover, the godoc documentation for RFC3339 shows a sentinel format example that does not match actual output.

@ianlancetaylor
Copy link
Contributor

Dup of #9346?

@joelpresence
Copy link
Author

joelpresence commented Oct 4, 2017

@ianlancetaylor #9346 was "frozen due to age" so PLEASE do not close this issue in favor of that other issue. I would like to keep this issue open since it doesn't seem like the other issue was resolved and we can't comment on it now since it was frozen due to age.

So I see two issues here:

  • The example of RFC3339 output cannot be parsed and seems inaccurate.
  • Parse(t.Format(F), F) should be valid and succeed without error. You should be able to successfully parse back into a time a string that you just formatted from a time. Parse/Format should be inverses of each other and both need to be reversible.

If the second bullet point doesn't hold, then time.Format is too risky to be used for serialization, because imagine if I format a bunch of times into strings to record them into a data file as in:

# File.csv
timeStr,Format
2006-01-02T15:04:05-07:00,RFC3339

I should be able to read this file back in and Parse back my time strings into time objects so I can use them. If I cannot, then time.Parse is risky and not useful for serialization. Even if this just applies to RFC3339, I really feel that we should fix this edge case/bug in RFC3339.

Thoughts?

I'm happy to help with a PR for a doc or code change if somebody can point me in the right direction as to how to get started.

Thanks! :-)

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Oct 4, 2017

Your second bullet point does not hold. In general Parse and Format are not inverses of each other.

The best way to serialize and deserialize time.Time values is to use the MarshalText and UnmarshalText methods (or MarshalBinary and UnmarshalBinary). See https://golang.org/pkg/encoding; time.Time implements all of those interfaces.

Edit: Fixed typo in URL.

@joelpresence
Copy link
Author

joelpresence commented Oct 4, 2017

@ianlancetaylor thanks for responding. When you say "in general, Parse and Format are not inverses of each other", that alarms me. If that's the case, then what's the point of those methods? What is Parse parsing? Why is the output format of Format structured?

If you're suggesting that I use MarshalText etc., then how would I exchange times with a non-go program that doesn't support MarshalText formats? Not everbody uses JSON, so how would I interoperate with a non-go program to which I send RFC3339-formatted times and from which I would receive RFC3339-formatted times? If go's Format and Parse can't handle this format reliably then maybe RFC3339 should be removed from go?

Sorry, I'm not trying to be harsh here, but I'm really surprised to hear that Format/Parse are not inverses of each other ... What is their purpose and why are they paired in the same go pkg to use the same space of format strings then?

Thanks again, and I really appreciate your feedback and the work of the go team!

Best,

Joel

@ianlancetaylor
Copy link
Contributor

The point of the Format method and Parse function is for producing and parsing human-readable time strings. I understand why you expect them to be inverses, but that is not how they are documented. They are pretty close to being inverses. I forget where it fails.

If you want to define your own format to interoperate with a non-Go program, then define that format. You should be able to design a format that can use Format to produce that format and Parse to parse it.

By the way, I took a closer look at your program, and you are trying to use time.Parse to parse time.RFC3339 as though it were a formatted date. It is not. You will not be able to produce the same value as the constant time.RFC3339 using the Format method. And in your second call to Parse you seem to have reversed the format and the string you want to parse.

@joelpresence
Copy link
Author

@ianlancetaylor thanks for replying. You're right, I did mixup the order of the layout and the value to be parsed (I'd argue that since Format expects the layout as the first arg, then so too should Parse, but my fault for not reading the arg sig in this case). I corrected my program and appended it below:

package main

import (
	"fmt"
	"time"
)

func main() {
	// From https://golang.org/pkg/time/#pkg-constants
	//
	//     RFC3339     = "2006-01-02T15:04:05Z07:00"
	timeStr := "2006-01-02T15:04:05Z07:00"

	// time.Parse fails on the sentinel example given in godoc
	parsed, errParsed := time.Parse(time.RFC3339, timeStr)
	fmt.Printf("parsed %v errParsed %v\n", parsed, errParsed)

	loc, errLoc := time.LoadLocation("MST")
	fmt.Printf("loc %v errLoc %v\n", loc, errLoc)

	t := time.Date(2006, 1, 2, 15, 4, 5, 0, loc)
	tStr := t.Format(time.RFC3339)

	fmt.Printf("tStr %v timeStr %v tStr == timeStr %v\n", tStr, timeStr, tStr == timeStr)

	// time.Parse fails on the sentinel example given in godoc
	parsed, errParsed = time.Parse(time.RFC3339, tStr)
	fmt.Printf("parsed %v errParsed %v\n", parsed, errParsed)
}

Now the output is as follows:

parsed 0001-01-01 00:00:00 +0000 UTC errParsed parsing time "2006-01-02T15:04:05Z07:00": extra text: 07:00
loc MST errLoc <nil>
tStr 2006-01-02T15:04:05-07:00 timeStr 2006-01-02T15:04:05Z07:00 tStr == timeStr false
parsed 2006-01-02 15:04:05 -0700 -0700 errParsed <nil>

So now the roundtrip within go DOES succeed (yay!) but the sentinel example given in the godoc "2006-01-02T15:04:05Z07:00" still does NOT parse.

Where does the godoc say that "the point of the Format method and Parse function is for producing and parsing human-readable time strings"?

I disagree that "this is not how they are documented". The documentation of the Parse method states Parse parses a formatted string and returns the time value it represents. "formatted" here refers to what? The logical conclusion reading this, IMHO, would be "formatted == formatted by the Format method". Is there another interpretation that was intended? The documentation of Parse explicitly mentions RFC3339 and also mentions time.Format in terms of layout, so it's reasonable to conclude from that mention that Format and Parse are interoperable in their formats. The example given in the godoc mentions that // The layout string used by the Parse function and Format method // shows by example how the reference time should be represented..

So maybe the confusion was the the value given in

const (
...
RFC3339     = "2006-01-02T15:04:05Z07:00"
...
)

is NOT actually representative of the values produced by Format for RFC3339 nor is it representative of the values that can be parsed by Parse for RFC3339? If that's the case, then I'd argue that it's misleading to represent the const as something that looks like a formatted time when in fact it's not a format that can be either produced by Format or parsed by Parse. In that case, since it's probably risky to change a const, maybe we could at least add a comment to the go src or at least the godoc to warn others about this?

I'm really not trying to be a PITA here, but I did find this confusing and was very surprise by go's behavior here and I just want to prevent others from having the same confusion.

Thanks again for your help! :-)

@ianlancetaylor
Copy link
Contributor

Yes, it is confusing, and there have been many dups of #9346.

RFC3339 is not a formatted time. It's a time format. Time formats do look like formatted times, by design, but they are not identical.

Fixes to the docs are always welcome, but make sure they are correct.

@joelpresence
Copy link
Author

OK, I'll take a look next week when I have time ... Is there a procedure for updating the official godocs? Thanks!

@ianlancetaylor
Copy link
Contributor

Thanks. It's just like contributing source code: https://golang.org/doc/contribute.html

@odeke-em odeke-em changed the title package time documentation shows incorrect formatting of RFC3339 that does not parse time: documentation shows incorrect formatting of RFC3339 that does not parse Oct 5, 2017
@gopherbot
Copy link

Change https://golang.org/cl/74231 mentions this issue: time: document valid layouts are not valid Parse values

@gopherbot
Copy link

Change https://golang.org/cl/74890 mentions this issue: time: improve comments about valid layouts being invalid Parse values

gopherbot pushed a commit that referenced this issue Nov 1, 2017
Updates #9346
Updates #22135

Change-Id: I7039c9f7d49600e877e35b7255c341fea35890e2
Reviewed-on: https://go-review.googlesource.com/74890
Reviewed-by: Rob Pike <r@golang.org>
@golang golang locked and limited conversation to collaborators Oct 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants