package time import ( "bytes" "once" "os" "strconv" ) const ( numeric = iota alphabetic separator plus minus ) // These are predefined layouts for use in Time.Format. // The standard time used in the layouts is: // Mon Jan 2 15:04:05 MST 2006 (MST is GMT-0700) // which is Unix time 1136243045. // (Think of it as 01/02 03:04:05PM '06 -0700.) // An underscore _ represents a space that // may be replaced by a digit if the following number // (a day) has two digits; for compatibility with // fixed-width Unix time formats. const ( ANSIC = "Mon Jan _2 15:04:05 2006" UnixDate = "Mon Jan _2 15:04:05 MST 2006" RubyDate = "Mon Jan 02 15:04:05 -0700 2006" RFC850 = "Monday, 02-Jan-06 15:04:05 MST" RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" Kitchen = "3:04PM" // Special case: use Z to get the time zone formatted according to ISO 8601, // which is -0700 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" stdUnderDay = "_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 = "MST" stdISO8601TZ = "Z" // prints Z for UTC stdNumTZ = "0700" // always numeric ) 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 lookup(tab []string, val string) (int, os.Error) { for i, v := range tab { if v == val { return i, nil } } return -1, errBad } func charType(c uint8) int { switch { case '0' <= c && c <= '9': return numeric case c == '_': // underscore; treated like a number when printing return numeric case 'a' <= c && c <= 'z', 'A' <= c && c <= 'Z': return alphabetic case c == '+': return plus case c == '-': return minus } return separator } func pad(i int, padding string) string { s := strconv.Itoa(i) if i < 10 { s = padding + s } return s } func zeroPad(i int) string { return pad(i, "0") } // 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 { b := new(bytes.Buffer) // Each iteration generates one piece for len(layout) > 0 { c := layout[0] pieceType := charType(c) i := 0 for i < len(layout) && charType(layout[i]) == pieceType { i++ } p := layout[0:i] layout = layout[i:] 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 stdUnderDay: p = pad(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, stdNumTZ: // Ugly special case. We cheat and take "Z" to mean "the time // zone as formatted for ISO 8601". zone := t.ZoneOffset / 60 // conver to minutes if p == stdISO8601TZ && t.ZoneOffset == 0 { p = "Z" } else { // If the reference time is stdNumTZ (0700), the sign has already been // emitted but may be wrong. For stdISO8601TZ we must print it. if p == stdNumTZ && b.Len() > 0 { soFar := b.Bytes() if soFar[len(soFar)-1] == '-' && zone >= 0 { // fix the sign soFar[len(soFar)-1] = '+' } else { zone = -zone } p = "" } else { 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 } b.WriteString(p) } return b.String() } // String returns a Unix-style representation of the time value. func (t *Time) String() string { return t.Format(UnixDate) } var errBad = os.ErrorString("bad") // just a marker; not returned to user // ParseError describes a problem parsing a time string. type ParseError struct { Layout string Value string LayoutElem string ValueElem string Message string } // String is the string representation of a ParseError. func (e *ParseError) String() string { if e.Message == "" { return "parsing time " + strconv.Quote(e.Value) + " as " + strconv.Quote(e.Layout) + ": cannot parse " + strconv.Quote(e.ValueElem) + " as " + strconv.Quote(e.LayoutElem) } return "parsing time " + strconv.Quote(e.Value) + e.Message } // To simplify comparison, collapse an initial run of spaces into a single space. func collapseSpaces(s string) string { if len(s) <= 1 || s[0] != ' ' { return s } var i int for i = 1; i < len(s); i++ { if s[i] != ' ' { return s[i-1:] } } return " " } // Parse parses a formatted string and returns the time value it represents. // The layout defines the format by showing the representation of a standard // time, which is then used to describe the string to be parsed. Predefined // layouts ANSIC, UnixDate, ISO8601 and others describe standard // representations. // // Only those elements present in the value will be set in the returned time // structure. Also, if the input string represents an inconsistent time // (such as having the wrong day of the week), the returned value will also // be inconsistent. In any case, the elements of the returned time will be // sane: hours in 0..23, minutes in 0..59, day of month in 0..31, etc. func Parse(alayout, avalue string) (*Time, os.Error) { var t Time const formatErr = ": different format from " rangeErrString := "" // set if a value is out of range pmSet := false // do we need to add 12 to the hour? // Each iteration steps along one piece layout, value := alayout, avalue sign := "" // pending + or - from previous iteration for len(layout) > 0 && len(value) > 0 { c := layout[0] pieceType := charType(c) var i int for i = 0; i < len(layout) && charType(layout[i]) == pieceType; i++ { } reference := layout[0:i] prevLayout := layout layout = layout[i:] // Ugly time zone handling. if reference == "Z" { // Special case for ISO8601 time zone: "Z" or "-0800" if value[0] == 'Z' { i = 1 } else if len(value) >= 5 { i = 5 } else { return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout} } } else { c = value[0] if charType(c) != pieceType { // Ugly management of signs. Reference and data might differ. // 1. Could be a minus sign introducing a negative year. if c == '-' && pieceType != minus { value = value[1:] layout = prevLayout // don't consume reference item sign = "-" continue } // 2. Could be a plus sign for a +0100 time zone, represented by -0700 in the standard. if c == '+' && pieceType == minus { value = value[1:] layout = prevLayout[1:] // absorb sign in both value and layout sign = "+" continue } return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout} } for i = 0; i < len(value) && charType(value[i]) == pieceType; i++ { } } p := value[0:i] value = value[i:] switch pieceType { case separator: // Separators must match but initial run of spaces is treated as a single space. if collapseSpaces(p) != collapseSpaces(reference) { return nil, &ParseError{Layout: alayout, Value: avalue, Message: formatErr + alayout} } continue case plus, minus: if len(p) == 1 { // ++ or -- don't count as signs. sign = p continue } } var err os.Error switch reference { case stdYear: t.Year, err = strconv.Atoi64(p) if t.Year >= 69 { // Unix time starts Dec 31 1969 in some time zones t.Year += 1900 } else { t.Year += 2000 } case stdLongYear: t.Year, err = strconv.Atoi64(p) if sign == "-" { t.Year = -t.Year } case stdMonth: t.Month, err = lookup(shortMonthNames, p) case stdLongMonth: t.Month, err = lookup(longMonthNames, p) case stdNumMonth, stdZeroMonth: t.Month, err = strconv.Atoi(p) if t.Month <= 0 || 12 < t.Month { rangeErrString = "month" } case stdWeekDay: t.Weekday, err = lookup(shortDayNames, p) case stdLongWeekDay: t.Weekday, err = lookup(longDayNames, p) case stdDay, stdUnderDay, stdZeroDay: t.Day, err = strconv.Atoi(p) if t.Day < 0 || 31 < t.Day { // TODO: be more thorough in date check? rangeErrString = "day" } case stdHour: t.Hour, err = strconv.Atoi(p) if t.Hour < 0 || 24 <= t.Hour { rangeErrString = "hour" } case stdHour12, stdZeroHour12: t.Hour, err = strconv.Atoi(p) if t.Hour < 0 || 12 < t.Hour { rangeErrString = "hour" } case stdMinute, stdZeroMinute: t.Minute, err = strconv.Atoi(p) if t.Minute < 0 || 60 <= t.Minute { rangeErrString = "minute" } case stdSecond, stdZeroSecond: t.Second, err = strconv.Atoi(p) if t.Second < 0 || 60 <= t.Second { rangeErrString = "second" } case stdZulu: if len(p) != 4 { err = os.ErrorString("HHMM value must be 4 digits") break } t.Hour, err = strconv.Atoi(p[0:2]) if err != nil { t.Minute, err = strconv.Atoi(p[2:4]) } case stdISO8601TZ, stdNumTZ: if reference == stdISO8601TZ { if p == "Z" { t.Zone = "UTC" break } // len(p) known to be 5: "-0800" sign = p[0:1] p = p[1:] } else { // len(p) known to be 4: "0800" and sign is set } var hr, min int hr, err = strconv.Atoi(p[0:2]) if err != nil { min, err = strconv.Atoi(p[2:4]) } t.ZoneOffset = (hr*60 + min) * 60 // offset is in seconds switch sign[0] { case '+': case '-': t.ZoneOffset = -t.ZoneOffset default: err = errBad } case stdPM: if p == "PM" { pmSet = true } else if p != "AM" { err = errBad } case stdpm: if p == "pm" { pmSet = true } else if p != "am" { err = errBad } case stdTZ: // Does it look like a time zone? if p == "UTC" { t.Zone = p break } // All other time zones look like XXT or XXXT. if len(p) != 3 && len(p) != 4 || p[len(p)-1] != 'T' { err = errBad } for i := 0; i < len(p); i++ { if p[i] < 'A' || 'Z' < p[i] { err = errBad } } if err != nil { break } // It's a valid format. t.Zone = p // Can we find it in the table? once.Do(setupZone) for _, z := range zones { if p == z.zone.name { t.ZoneOffset = z.zone.utcoff break } } } if rangeErrString != "" { return nil, &ParseError{alayout, avalue, reference, p, ": " + rangeErrString + " out of range"} } if err != nil { return nil, &ParseError{alayout, avalue, reference, p, ""} } sign = "" } if pmSet && t.Hour < 12 { t.Hour += 12 } return &t, nil }