From e05b381e79b1478ae138e08c5f541ecbb5d6955d Mon Sep 17 00:00:00 2001 From: Rob Pike Date: Thu, 7 Jan 2010 10:32:48 +1100 Subject: [PATCH] New time formatter, time.Format(formatString) The model is that formatString is a a representation of a standard time, and that Format converts the time to that representation. Standard representaitons are defined for ANSIC, RFC850, RFC1123, and ISO8601. There's also a humane Kitchen fomat: 3:04PM. R=rsc, benolive, cw CC=golang-dev https://golang.org/cl/181130 --- src/pkg/time/Makefile | 1 + src/pkg/time/format.go | 227 ++++++++++++++++++++++++++++++++++++++ src/pkg/time/time.go | 152 ------------------------- src/pkg/time/time_test.go | 34 +++++- 4 files changed, 259 insertions(+), 155 deletions(-) create mode 100644 src/pkg/time/format.go diff --git a/src/pkg/time/Makefile b/src/pkg/time/Makefile index ac441506972bf..f73fc8878c936 100644 --- a/src/pkg/time/Makefile +++ b/src/pkg/time/Makefile @@ -6,6 +6,7 @@ include ../../Make.$(GOARCH) TARG=time GOFILES=\ + format.go \ sleep.go\ tick.go\ time.go\ diff --git a/src/pkg/time/format.go b/src/pkg/time/format.go new file mode 100644 index 0000000000000..52d62a90862bf --- /dev/null +++ b/src/pkg/time/format.go @@ -0,0 +1,227 @@ +package time + +import ( + "strconv" +) + +const ( + numeric = iota + alphabetic + separator +) + +// These are predefined layouts for use in Time.Format. +// The standard time used in the layouts is: +// Mon Jan 2 15:04:05 PST 2006 (PST is GMT-0800) +// which is Unix time 1136243045. +const ( + ANSIC = "Mon Jan 2 15:04:05 2006" + UnixDate = "Mon Jan 2 15:04:05 PST 2006" + RFC850 = "Monday, 02-Jan-06 15:04:05 PST" + RFC1123 = "Mon, 02 Jan 2006 15:04:05 PST" + Kitchen = "3:04PM" + // Special case: use Z to get the time zone formatted according to ISO 8601, + // which is -0800 or Z for UTC + ISO8601 = "2006-01-02T15:04:05Z" +) + +const ( + stdLongMonth = "January" + stdMonth = "Jan" + stdNumMonth = "1" + stdZeroMonth = "01" + stdLongWeekDay = "Monday" + stdWeekDay = "Mon" + stdDay = "2" + stdZeroDay = "02" + stdHour = "15" + stdHour12 = "3" + stdZeroHour12 = "03" + stdMinute = "4" + stdZeroMinute = "04" + stdSecond = "5" + stdZeroSecond = "05" + stdLongYear = "2006" + stdYear = "06" + stdZulu = "1504" + stdPM = "PM" + stdpm = "pm" + stdTZ = "PST" + stdISO8601TZ = "Z" +) + +var longDayNames = []string{ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +} + +var shortDayNames = []string{ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", +} + +var shortMonthNames = []string{ + "---", + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +} + +var longMonthNames = []string{ + "---", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +} + +func charType(c uint8) int { + switch { + case '0' <= c && c <= '9': + return numeric + case 'a' <= c && c < 'z', 'A' <= c && c <= 'Z': + return alphabetic + } + return separator +} + +func pieces(s string) []string { + p := make([]string, 20) + i := 0 + // Each iteration generates one piece + for n := range p { + if i >= len(s) { + p = p[0:n] + break + } + start := i + c := s[i] + pieceType := charType(c) + for i < len(s) && charType(s[i]) == pieceType { + i++ + } + p[n] = s[start:i] + } + return p +} + +func zeroPad(i int) string { + s := strconv.Itoa(i) + if i < 10 { + s = "0" + s + } + return s +} + +// Format returns a textual representation of the time value formatted +// according to layout. The layout defines the format by showing the +// representation of a standard time, which is then used to describe +// the time to be formatted. Predefined layouts ANSIC, UnixDate, +// ISO8601 and others describe standard representations. +func (t *Time) Format(layout string) string { + pc := pieces(layout) + s := "" + for _, p := range pc { + switch p { + case stdYear: + p = strconv.Itoa64(t.Year % 100) + case stdLongYear: + p = strconv.Itoa64(t.Year) + case stdMonth: + p = shortMonthNames[t.Month] + case stdLongMonth: + p = longMonthNames[t.Month] + case stdNumMonth: + p = strconv.Itoa(t.Month) + case stdZeroMonth: + p = zeroPad(t.Month) + case stdWeekDay: + p = shortDayNames[t.Weekday] + case stdLongWeekDay: + p = longDayNames[t.Weekday] + case stdDay: + p = strconv.Itoa(t.Day) + case stdZeroDay: + p = zeroPad(t.Day) + case stdHour: + p = zeroPad(t.Hour) + case stdHour12: + p = strconv.Itoa(t.Hour % 12) + case stdZeroHour12: + p = zeroPad(t.Hour % 12) + case stdMinute: + p = strconv.Itoa(t.Minute) + case stdZeroMinute: + p = zeroPad(t.Minute) + case stdSecond: + p = strconv.Itoa(t.Second) + case stdZeroSecond: + p = zeroPad(t.Second) + case stdZulu: + p = zeroPad(t.Hour) + zeroPad(t.Minute) + case stdISO8601TZ: + // Rather ugly special case, required because the time zone is too broken down + // in this format to recognize easily. We cheat and take "Z" to mean "the time + // zone as formatted for ISO 8601". + if t.ZoneOffset == 0 { + p = "Z" + } else { + zone := t.ZoneOffset / 60 // minutes + if zone < 0 { + p = "-" + zone = -zone + } else { + p = "+" + } + p += zeroPad(zone / 60) + p += zeroPad(zone % 60) + } + case stdPM: + if t.Hour >= 12 { + p = "PM" + } else { + p = "AM" + } + case stdpm: + if t.Hour >= 12 { + p = "pm" + } else { + p = "am" + } + case stdTZ: + p = t.Zone + } + s += p + } + return s +} + +// String returns a Unix-style representation of the time value. +func (t *Time) String() string { return t.Format(UnixDate) } diff --git a/src/pkg/time/time.go b/src/pkg/time/time.go index ff696e0ac0a93..d84807e836e1f 100644 --- a/src/pkg/time/time.go +++ b/src/pkg/time/time.go @@ -227,155 +227,3 @@ func (t *Time) Seconds() int64 { sec -= int64(t.ZoneOffset) return sec } - -var longDayNames = []string{ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", -} - -var shortDayNames = []string{ - "Sun", - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", -} - -var shortMonthNames = []string{ - "---", - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", -} - -func copy(dst []byte, s string) { - for i := 0; i < len(s); i++ { - dst[i] = s[i] - } -} - -func decimal(dst []byte, n int) { - if n < 0 { - n = 0 - } - for i := len(dst) - 1; i >= 0; i-- { - dst[i] = byte(n%10 + '0') - n /= 10 - } -} - -func addString(buf []byte, bp int, s string) int { - n := len(s) - copy(buf[bp:bp+n], s) - return bp + n -} - -// Just enough of strftime to implement the date formats below. -// Not exported. -func format(t *Time, fmt string) string { - buf := make([]byte, 128) - bp := 0 - - for i := 0; i < len(fmt); i++ { - if fmt[i] == '%' { - i++ - switch fmt[i] { - case 'A': // %A full weekday name - bp = addString(buf, bp, longDayNames[t.Weekday]) - case 'a': // %a abbreviated weekday name - bp = addString(buf, bp, shortDayNames[t.Weekday]) - case 'b': // %b abbreviated month name - bp = addString(buf, bp, shortMonthNames[t.Month]) - case 'd': // %d day of month (01-31) - decimal(buf[bp:bp+2], t.Day) - bp += 2 - case 'e': // %e day of month ( 1-31) - if t.Day >= 10 { - decimal(buf[bp:bp+2], t.Day) - } else { - buf[bp] = ' ' - buf[bp+1] = byte(t.Day + '0') - } - bp += 2 - case 'H': // %H hour 00-23 - decimal(buf[bp:bp+2], t.Hour) - bp += 2 - case 'M': // %M minute 00-59 - decimal(buf[bp:bp+2], t.Minute) - bp += 2 - case 'm': // %m month 01-12 - decimal(buf[bp:bp+2], t.Month) - bp += 2 - case 'S': // %S second 00-59 - decimal(buf[bp:bp+2], t.Second) - bp += 2 - case 'Y': // %Y year 2008 - decimal(buf[bp:bp+4], int(t.Year)) - bp += 4 - case 'y': // %y year 08 - decimal(buf[bp:bp+2], int(t.Year%100)) - bp += 2 - case 'z': // %z tz in the form -0500 - if t.ZoneOffset == 0 { - bp = addString(buf, bp, "Z") - } else if t.ZoneOffset < 0 { - bp = addString(buf, bp, "-") - decimal(buf[bp:bp+2], -t.ZoneOffset/3600) - decimal(buf[bp+2:bp+4], (-t.ZoneOffset%3600)/60) - bp += 4 - } else { - bp = addString(buf, bp, "+") - decimal(buf[bp:bp+2], t.ZoneOffset/3600) - decimal(buf[bp+2:bp+4], (t.ZoneOffset%3600)/60) - bp += 4 - } - case 'Z': - bp = addString(buf, bp, t.Zone) - default: - buf[bp] = '%' - buf[bp+1] = fmt[i] - bp += 2 - } - } else { - buf[bp] = fmt[i] - bp++ - } - } - return string(buf[0:bp]) -} - -// Asctime formats the parsed time value in the style of -// ANSI C asctime: Sun Nov 6 08:49:37 1994 -func (t *Time) Asctime() string { return format(t, "%a %b %e %H:%M:%S %Y") } - -// RFC850 formats the parsed time value in the style of -// RFC 850: Sunday, 06-Nov-94 08:49:37 UTC -func (t *Time) RFC850() string { return format(t, "%A, %d-%b-%y %H:%M:%S %Z") } - -// RFC1123 formats the parsed time value in the style of -// RFC 1123: Sun, 06 Nov 1994 08:49:37 UTC -func (t *Time) RFC1123() string { return format(t, "%a, %d %b %Y %H:%M:%S %Z") } - -// ISO8601 formats the parsed time value in the style of -// ISO 8601: 1994-11-06T08:49:37Z -func (t *Time) ISO8601() string { return format(t, "%Y-%m-%dT%H:%M:%S%z") } - -// String formats the parsed time value in the style of -// date(1): Sun Nov 6 08:49:37 UTC 1994 -func (t *Time) String() string { return format(t, "%a %b %e %H:%M:%S %Z %Y") } diff --git a/src/pkg/time/time_test.go b/src/pkg/time/time_test.go index 97787f30bc2a0..4dfdea4456879 100644 --- a/src/pkg/time/time_test.go +++ b/src/pkg/time/time_test.go @@ -114,10 +114,38 @@ var iso8601Formats = []TimeFormatTest{ func TestISO8601Conversion(t *testing.T) { for _, f := range iso8601Formats { - if f.time.ISO8601() != f.formattedValue { - t.Error("ISO8601():") + if f.time.Format(ISO8601) != f.formattedValue { + t.Error("ISO8601:") t.Errorf(" want=%+v", f.formattedValue) - t.Errorf(" have=%+v", f.time.ISO8601()) + t.Errorf(" have=%+v", f.time.Format(ISO8601)) + } + } +} + +type FormatTest struct { + name string + format string + result string +} + +var formatTests = []FormatTest{ + FormatTest{"ANSIC", ANSIC, "Thu Feb 4 21:00:57 2010"}, + FormatTest{"UnixDate", UnixDate, "Thu Feb 4 21:00:57 PST 2010"}, + FormatTest{"RFC850", RFC850, "Thursday, 04-Feb-10 21:00:57 PST"}, + FormatTest{"RFC1123", RFC1123, "Thu, 04 Feb 2010 21:00:57 PST"}, + FormatTest{"ISO8601", ISO8601, "2010-02-04T21:00:57-0800"}, + FormatTest{"Kitchen", Kitchen, "9:00PM"}, + FormatTest{"am/pm", "3pm", "9pm"}, + FormatTest{"AM/PM", "3PM", "9PM"}, +} + +func TestFormat(t *testing.T) { + // The numeric time represents Thu Feb 4 21:00:57 EST 2010 + time := SecondsToLocalTime(1265346057) + for _, test := range formatTests { + result := time.Format(test.format) + if result != test.result { + t.Errorf("%s expected %q got %q", test.name, test.result, result) } } }