Skip to content

Commit bb09f8a

Browse files
committed
time: make time.Time print a valid Go string with %#v
Previously calling fmt.Sprintf("%#v", t) on a time.Time value would yield a result like: time.Time{wall:0x0, ext:63724924180, loc:(*time.Location)(nil)} which does not compile when embedded in a Go program, and does not tell you what value is represented at a glance. This change adds a GoString method that returns much more legible output: "time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.UTC)" which gives you more information about the time.Time and also can be usefully embedded in a Go program without additional work. Update Quote() to hex escape non-ASCII characters (copying logic from strconv), which makes it safer to embed them in the output of GoString(). Fixes #39034. Change-Id: Ic985bafe4e556f64e82223c643f65143c9a45c3b Reviewed-on: https://go-review.googlesource.com/c/go/+/267017 Reviewed-by: Emmanuel Odeke <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Run-TryBot: Emmanuel Odeke <[email protected]>
1 parent fadad85 commit bb09f8a

File tree

3 files changed

+117
-7
lines changed

3 files changed

+117
-7
lines changed

doc/go1.17.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ <h3 id="minor_library_changes">Minor changes to the library</h3>
205205
</p>
206206
</dl><!-- net/http -->
207207

208+
<dl id="time"><dt><a href="/pkg/time/">time</a></dt>
209+
<dd>
210+
<p><!-- CL 260858 -->
211+
time.Time now has a <a href="/pkg/time/#Time.GoString">GoString</a>
212+
method that will return a more useful value for times when printed with
213+
the <code>"%#v"</code> format specifier in the fmt package.
214+
</p>
215+
</dd>
216+
</dl><!-- time -->
208217
<p>
209218
TODO: complete this section
210219
</p>

src/time/format.go

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,60 @@ func (t Time) String() string {
477477
return s
478478
}
479479

480+
// GoString implements fmt.GoStringer and formats t to be printed in Go source
481+
// code.
482+
func (t Time) GoString() string {
483+
buf := []byte("time.Date(")
484+
buf = appendInt(buf, t.Year(), 0)
485+
month := t.Month()
486+
if January <= month && month <= December {
487+
buf = append(buf, ", time."...)
488+
buf = append(buf, t.Month().String()...)
489+
} else {
490+
// It's difficult to construct a time.Time with a date outside the
491+
// standard range but we might as well try to handle the case.
492+
buf = appendInt(buf, int(month), 0)
493+
}
494+
buf = append(buf, ", "...)
495+
buf = appendInt(buf, t.Day(), 0)
496+
buf = append(buf, ", "...)
497+
buf = appendInt(buf, t.Hour(), 0)
498+
buf = append(buf, ", "...)
499+
buf = appendInt(buf, t.Minute(), 0)
500+
buf = append(buf, ", "...)
501+
buf = appendInt(buf, t.Second(), 0)
502+
buf = append(buf, ", "...)
503+
buf = appendInt(buf, t.Nanosecond(), 0)
504+
buf = append(buf, ", "...)
505+
switch loc := t.Location(); loc {
506+
case UTC, nil:
507+
buf = append(buf, "time.UTC"...)
508+
case Local:
509+
buf = append(buf, "time.Local"...)
510+
default:
511+
// there are several options for how we could display this, none of
512+
// which are great:
513+
//
514+
// - use Location(loc.name), which is not technically valid syntax
515+
// - use LoadLocation(loc.name), which will cause a syntax error when
516+
// embedded and also would require us to escape the string without
517+
// importing fmt or strconv
518+
// - try to use FixedZone, which would also require escaping the name
519+
// and would represent e.g. "America/Los_Angeles" daylight saving time
520+
// shifts inaccurately
521+
// - use the pointer format, which is no worse than you'd get with the
522+
// old fmt.Sprintf("%#v", t) format.
523+
//
524+
// Of these, Location(loc.name) is the least disruptive. This is an edge
525+
// case we hope not to hit too often.
526+
buf = append(buf, `time.Location(`...)
527+
buf = append(buf, []byte(quote(loc.name))...)
528+
buf = append(buf, `)`...)
529+
}
530+
buf = append(buf, ')')
531+
return string(buf)
532+
}
533+
480534
// Format returns a textual representation of the time value formatted
481535
// according to layout, which defines the format by showing how the reference
482536
// time, defined to be
@@ -688,14 +742,33 @@ type ParseError struct {
688742
Message string
689743
}
690744

745+
// These are borrowed from unicode/utf8 and strconv and replicate behavior in
746+
// that package, since we can't take a dependency on either.
747+
const runeSelf = 0x80
748+
const lowerhex = "0123456789abcdef"
749+
691750
func quote(s string) string {
692-
buf := make([]byte, 0, len(s)+2) // +2 for surrounding quotes
693-
buf = append(buf, '"')
694-
for _, c := range s {
695-
if c == '"' || c == '\\' {
696-
buf = append(buf, '\\')
751+
buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes
752+
buf[0] = '"'
753+
for i, c := range s {
754+
if c >= runeSelf || c < ' ' {
755+
// This means you are asking us to parse a time.Duration or
756+
// time.Location with unprintable or non-ASCII characters in it.
757+
// We don't expect to hit this case very often. We could try to
758+
// reproduce strconv.Quote's behavior with full fidelity but
759+
// given how rarely we expect to hit these edge cases, speed and
760+
// conciseness are better.
761+
for j := 0; j < len(string(c)) && j < len(s); j++ {
762+
buf = append(buf, `\x`...)
763+
buf = append(buf, lowerhex[s[i+j]>>4])
764+
buf = append(buf, lowerhex[s[i+j]&0xF])
765+
}
766+
} else {
767+
if c == '"' || c == '\\' {
768+
buf = append(buf, '\\')
769+
}
770+
buf = append(buf, string(c)...)
697771
}
698-
buf = append(buf, string(c)...)
699772
}
700773
buf = append(buf, '"')
701774
return string(buf)

src/time/format_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,31 @@ func TestFormat(t *testing.T) {
129129
}
130130
}
131131

132+
var goStringTests = []struct {
133+
in Time
134+
want string
135+
}{
136+
{Date(2009, February, 5, 5, 0, 57, 12345600, UTC),
137+
"time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.UTC)"},
138+
{Date(2009, February, 5, 5, 0, 57, 12345600, Local),
139+
"time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.Local)"},
140+
{Date(2009, February, 5, 5, 0, 57, 12345600, FixedZone("Europe/Berlin", 3*60*60)),
141+
`time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.Location("Europe/Berlin"))`,
142+
},
143+
{Date(2009, February, 5, 5, 0, 57, 12345600, FixedZone("Non-ASCII character ⏰", 3*60*60)),
144+
`time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.Location("Non-ASCII character \xe2\x8f\xb0"))`,
145+
},
146+
}
147+
148+
func TestGoString(t *testing.T) {
149+
// The numeric time represents Thu Feb 4 21:00:57.012345600 PST 2009
150+
for _, tt := range goStringTests {
151+
if tt.in.GoString() != tt.want {
152+
t.Errorf("GoString (%q): got %q want %q", tt.in, tt.in.GoString(), tt.want)
153+
}
154+
}
155+
}
156+
132157
// issue 12440.
133158
func TestFormatSingleDigits(t *testing.T) {
134159
time := Date(2001, 2, 3, 4, 5, 6, 700000000, UTC)
@@ -796,10 +821,13 @@ func TestQuote(t *testing.T) {
796821
{`abc"xyz"`, `"abc\"xyz\""`},
797822
{"", `""`},
798823
{"abc", `"abc"`},
824+
{`☺`, `"\xe2\x98\xba"`},
825+
{`☺ hello ☺ hello`, `"\xe2\x98\xba hello \xe2\x98\xba hello"`},
826+
{"\x04", `"\x04"`},
799827
}
800828
for _, tt := range tests {
801829
if q := Quote(tt.s); q != tt.want {
802-
t.Errorf("Quote(%q) = %q, want %q", tt.s, q, tt.want)
830+
t.Errorf("Quote(%q) = got %q, want %q", tt.s, q, tt.want)
803831
}
804832
}
805833

0 commit comments

Comments
 (0)