]> Cypherpunks repositories - gostls13.git/commitdiff
time: make time.Time print a valid Go string with %#v
authorKevin Burke <kevin@burke.dev>
Thu, 25 Mar 2021 19:33:35 +0000 (12:33 -0700)
committerKevin Burke <kev@inburke.com>
Sun, 2 May 2021 20:59:26 +0000 (20:59 +0000)
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 <emmanuel@orijtech.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Run-TryBot: Emmanuel Odeke <emmanuel@orijtech.com>

doc/go1.17.html
src/time/format.go
src/time/format_test.go

index b670d1b149d67fbc0d3fff2178c08686eca86aa8..0521f9fd919526e0fa221267158c60fa4f525bc1 100644 (file)
@@ -205,6 +205,15 @@ that can be used to pass values between C and Go safely. See
   </p>
 </dl><!-- net/http -->
 
+<dl id="time"><dt><a href="/pkg/time/">time</a></dt>
+  <dd>
+    <p><!-- CL 260858 -->
+    time.Time now has a <a href="/pkg/time/#Time.GoString">GoString</a>
+    method that will return a more useful value for times when printed with
+    the <code>"%#v"</code> format specifier in the fmt package.
+    </p>
+  </dd>
+</dl><!-- time -->
 <p>
   TODO: complete this section
 </p>
index 9624752fb48de42c68f281d62df1dd8597030df3..f6dc8ee621b13c5ba17f110dcf67269c52dda936 100644 (file)
@@ -477,6 +477,60 @@ func (t Time) String() string {
        return s
 }
 
+// GoString implements fmt.GoStringer and formats t to be printed in Go source
+// code.
+func (t Time) GoString() string {
+       buf := []byte("time.Date(")
+       buf = appendInt(buf, t.Year(), 0)
+       month := t.Month()
+       if January <= month && month <= December {
+               buf = append(buf, ", time."...)
+               buf = append(buf, t.Month().String()...)
+       } else {
+               // It's difficult to construct a time.Time with a date outside the
+               // standard range but we might as well try to handle the case.
+               buf = appendInt(buf, int(month), 0)
+       }
+       buf = append(buf, ", "...)
+       buf = appendInt(buf, t.Day(), 0)
+       buf = append(buf, ", "...)
+       buf = appendInt(buf, t.Hour(), 0)
+       buf = append(buf, ", "...)
+       buf = appendInt(buf, t.Minute(), 0)
+       buf = append(buf, ", "...)
+       buf = appendInt(buf, t.Second(), 0)
+       buf = append(buf, ", "...)
+       buf = appendInt(buf, t.Nanosecond(), 0)
+       buf = append(buf, ", "...)
+       switch loc := t.Location(); loc {
+       case UTC, nil:
+               buf = append(buf, "time.UTC"...)
+       case Local:
+               buf = append(buf, "time.Local"...)
+       default:
+               // there are several options for how we could display this, none of
+               // which are great:
+               //
+               // - use Location(loc.name), which is not technically valid syntax
+               // - use LoadLocation(loc.name), which will cause a syntax error when
+               // embedded and also would require us to escape the string without
+               // importing fmt or strconv
+               // - try to use FixedZone, which would also require escaping the name
+               // and would represent e.g. "America/Los_Angeles" daylight saving time
+               // shifts inaccurately
+               // - use the pointer format, which is no worse than you'd get with the
+               // old fmt.Sprintf("%#v", t) format.
+               //
+               // Of these, Location(loc.name) is the least disruptive. This is an edge
+               // case we hope not to hit too often.
+               buf = append(buf, `time.Location(`...)
+               buf = append(buf, []byte(quote(loc.name))...)
+               buf = append(buf, `)`...)
+       }
+       buf = append(buf, ')')
+       return string(buf)
+}
+
 // Format returns a textual representation of the time value formatted
 // according to layout, which defines the format by showing how the reference
 // time, defined to be
@@ -688,14 +742,33 @@ type ParseError struct {
        Message    string
 }
 
+// These are borrowed from unicode/utf8 and strconv and replicate behavior in
+// that package, since we can't take a dependency on either.
+const runeSelf = 0x80
+const lowerhex = "0123456789abcdef"
+
 func quote(s string) string {
-       buf := make([]byte, 0, len(s)+2) // +2 for surrounding quotes
-       buf = append(buf, '"')
-       for _, c := range s {
-               if c == '"' || c == '\\' {
-                       buf = append(buf, '\\')
+       buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes
+       buf[0] = '"'
+       for i, c := range s {
+               if c >= runeSelf || c < ' ' {
+                       // This means you are asking us to parse a time.Duration or
+                       // time.Location with unprintable or non-ASCII characters in it.
+                       // We don't expect to hit this case very often. We could try to
+                       // reproduce strconv.Quote's behavior with full fidelity but
+                       // given how rarely we expect to hit these edge cases, speed and
+                       // conciseness are better.
+                       for j := 0; j < len(string(c)) && j < len(s); j++ {
+                               buf = append(buf, `\x`...)
+                               buf = append(buf, lowerhex[s[i+j]>>4])
+                               buf = append(buf, lowerhex[s[i+j]&0xF])
+                       }
+               } else {
+                       if c == '"' || c == '\\' {
+                               buf = append(buf, '\\')
+                       }
+                       buf = append(buf, string(c)...)
                }
-               buf = append(buf, string(c)...)
        }
        buf = append(buf, '"')
        return string(buf)
index 09d3f842e3b56deb77dcdcff0767ec1f1391c0d2..1af41e2dfb315a65e6d154779d3665a4d7fdc268 100644 (file)
@@ -129,6 +129,31 @@ func TestFormat(t *testing.T) {
        }
 }
 
+var goStringTests = []struct {
+       in   Time
+       want string
+}{
+       {Date(2009, February, 5, 5, 0, 57, 12345600, UTC),
+               "time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.UTC)"},
+       {Date(2009, February, 5, 5, 0, 57, 12345600, Local),
+               "time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.Local)"},
+       {Date(2009, February, 5, 5, 0, 57, 12345600, FixedZone("Europe/Berlin", 3*60*60)),
+               `time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.Location("Europe/Berlin"))`,
+       },
+       {Date(2009, February, 5, 5, 0, 57, 12345600, FixedZone("Non-ASCII character ⏰", 3*60*60)),
+               `time.Date(2009, time.February, 5, 5, 0, 57, 12345600, time.Location("Non-ASCII character \xe2\x8f\xb0"))`,
+       },
+}
+
+func TestGoString(t *testing.T) {
+       // The numeric time represents Thu Feb  4 21:00:57.012345600 PST 2009
+       for _, tt := range goStringTests {
+               if tt.in.GoString() != tt.want {
+                       t.Errorf("GoString (%q): got %q want %q", tt.in, tt.in.GoString(), tt.want)
+               }
+       }
+}
+
 // issue 12440.
 func TestFormatSingleDigits(t *testing.T) {
        time := Date(2001, 2, 3, 4, 5, 6, 700000000, UTC)
@@ -796,10 +821,13 @@ func TestQuote(t *testing.T) {
                {`abc"xyz"`, `"abc\"xyz\""`},
                {"", `""`},
                {"abc", `"abc"`},
+               {`☺`, `"\xe2\x98\xba"`},
+               {`☺ hello ☺ hello`, `"\xe2\x98\xba hello \xe2\x98\xba hello"`},
+               {"\x04", `"\x04"`},
        }
        for _, tt := range tests {
                if q := Quote(tt.s); q != tt.want {
-                       t.Errorf("Quote(%q) = %q, want %q", tt.s, q, tt.want)
+                       t.Errorf("Quote(%q) = got %q, want %q", tt.s, q, tt.want)
                }
        }