| package stdlib |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "strings" |
| "time" |
| |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/function" |
| ) |
| |
| var FormatDateFunc = function.New(&function.Spec{ |
| Params: []function.Parameter{ |
| { |
| Name: "format", |
| Type: cty.String, |
| }, |
| { |
| Name: "time", |
| Type: cty.String, |
| }, |
| }, |
| Type: function.StaticReturnType(cty.String), |
| Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { |
| formatStr := args[0].AsString() |
| timeStr := args[1].AsString() |
| t, err := parseTimestamp(timeStr) |
| if err != nil { |
| return cty.DynamicVal, function.NewArgError(1, err) |
| } |
| |
| var buf bytes.Buffer |
| sc := bufio.NewScanner(strings.NewReader(formatStr)) |
| sc.Split(splitDateFormat) |
| const esc = '\'' |
| for sc.Scan() { |
| tok := sc.Bytes() |
| |
| // The leading byte signals the token type |
| switch { |
| case tok[0] == esc: |
| if tok[len(tok)-1] != esc || len(tok) == 1 { |
| return cty.DynamicVal, function.NewArgErrorf(0, "unterminated literal '") |
| } |
| if len(tok) == 2 { |
| // Must be a single escaped quote, '' |
| buf.WriteByte(esc) |
| } else { |
| // The content (until a closing esc) is printed out verbatim |
| // except that we must un-double any double-esc escapes in |
| // the middle of the string. |
| raw := tok[1 : len(tok)-1] |
| for i := 0; i < len(raw); i++ { |
| buf.WriteByte(raw[i]) |
| if raw[i] == esc { |
| i++ // skip the escaped quote |
| } |
| } |
| } |
| |
| case startsDateFormatVerb(tok[0]): |
| switch tok[0] { |
| case 'Y': |
| y := t.Year() |
| switch len(tok) { |
| case 2: |
| fmt.Fprintf(&buf, "%02d", y%100) |
| case 4: |
| fmt.Fprintf(&buf, "%04d", y) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: year must either be \"YY\" or \"YYYY\"", tok) |
| } |
| case 'M': |
| m := t.Month() |
| switch len(tok) { |
| case 1: |
| fmt.Fprintf(&buf, "%d", m) |
| case 2: |
| fmt.Fprintf(&buf, "%02d", m) |
| case 3: |
| buf.WriteString(m.String()[:3]) |
| case 4: |
| buf.WriteString(m.String()) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: month must be \"M\", \"MM\", \"MMM\", or \"MMMM\"", tok) |
| } |
| case 'D': |
| d := t.Day() |
| switch len(tok) { |
| case 1: |
| fmt.Fprintf(&buf, "%d", d) |
| case 2: |
| fmt.Fprintf(&buf, "%02d", d) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of month must either be \"D\" or \"DD\"", tok) |
| } |
| case 'E': |
| d := t.Weekday() |
| switch len(tok) { |
| case 3: |
| buf.WriteString(d.String()[:3]) |
| case 4: |
| buf.WriteString(d.String()) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of week must either be \"EEE\" or \"EEEE\"", tok) |
| } |
| case 'h': |
| h := t.Hour() |
| switch len(tok) { |
| case 1: |
| fmt.Fprintf(&buf, "%d", h) |
| case 2: |
| fmt.Fprintf(&buf, "%02d", h) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 24-hour must either be \"h\" or \"hh\"", tok) |
| } |
| case 'H': |
| h := t.Hour() % 12 |
| if h == 0 { |
| h = 12 |
| } |
| switch len(tok) { |
| case 1: |
| fmt.Fprintf(&buf, "%d", h) |
| case 2: |
| fmt.Fprintf(&buf, "%02d", h) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 12-hour must either be \"H\" or \"HH\"", tok) |
| } |
| case 'A', 'a': |
| if len(tok) != 2 { |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: must be \"%s%s\"", tok, tok[0:1], tok[0:1]) |
| } |
| upper := tok[0] == 'A' |
| switch t.Hour() / 12 { |
| case 0: |
| if upper { |
| buf.WriteString("AM") |
| } else { |
| buf.WriteString("am") |
| } |
| case 1: |
| if upper { |
| buf.WriteString("PM") |
| } else { |
| buf.WriteString("pm") |
| } |
| } |
| case 'm': |
| m := t.Minute() |
| switch len(tok) { |
| case 1: |
| fmt.Fprintf(&buf, "%d", m) |
| case 2: |
| fmt.Fprintf(&buf, "%02d", m) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: minute must either be \"m\" or \"mm\"", tok) |
| } |
| case 's': |
| s := t.Second() |
| switch len(tok) { |
| case 1: |
| fmt.Fprintf(&buf, "%d", s) |
| case 2: |
| fmt.Fprintf(&buf, "%02d", s) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: second must either be \"s\" or \"ss\"", tok) |
| } |
| case 'Z': |
| // We'll just lean on Go's own formatter for this one, since |
| // the necessary information is unexported. |
| switch len(tok) { |
| case 1: |
| buf.WriteString(t.Format("Z07:00")) |
| case 3: |
| str := t.Format("-0700") |
| switch str { |
| case "+0000": |
| buf.WriteString("UTC") |
| default: |
| buf.WriteString(str) |
| } |
| case 4: |
| buf.WriteString(t.Format("-0700")) |
| case 5: |
| buf.WriteString(t.Format("-07:00")) |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: timezone must be Z, ZZZZ, or ZZZZZ", tok) |
| } |
| default: |
| return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q", tok) |
| } |
| |
| default: |
| // Any other starting character indicates a literal sequence |
| buf.Write(tok) |
| } |
| } |
| |
| return cty.StringVal(buf.String()), nil |
| }, |
| }) |
| |
| // FormatDate reformats a timestamp given in RFC3339 syntax into another time |
| // syntax defined by a given format string. |
| // |
| // The format string uses letter mnemonics to represent portions of the |
| // timestamp, with repetition signifying length variants of each portion. |
| // Single quote characters ' can be used to quote sequences of literal letters |
| // that should not be interpreted as formatting mnemonics. |
| // |
| // The full set of supported mnemonic sequences is listed below: |
| // |
| // YY Year modulo 100 zero-padded to two digits, like "06". |
| // YYYY Four (or more) digit year, like "2006". |
| // M Month number, like "1" for January. |
| // MM Month number zero-padded to two digits, like "01". |
| // MMM English month name abbreviated to three letters, like "Jan". |
| // MMMM English month name unabbreviated, like "January". |
| // D Day of month number, like "2". |
| // DD Day of month number zero-padded to two digits, like "02". |
| // EEE English day of week name abbreviated to three letters, like "Mon". |
| // EEEE English day of week name unabbreviated, like "Monday". |
| // h 24-hour number, like "2". |
| // hh 24-hour number zero-padded to two digits, like "02". |
| // H 12-hour number, like "2". |
| // HH 12-hour number zero-padded to two digits, like "02". |
| // AA Hour AM/PM marker in uppercase, like "AM". |
| // aa Hour AM/PM marker in lowercase, like "am". |
| // m Minute within hour, like "5". |
| // mm Minute within hour zero-padded to two digits, like "05". |
| // s Second within minute, like "9". |
| // ss Second within minute zero-padded to two digits, like "09". |
| // ZZZZ Timezone offset with just sign and digit, like "-0800". |
| // ZZZZZ Timezone offset with colon separating hours and minutes, like "-08:00". |
| // Z Like ZZZZZ but with a special case "Z" for UTC. |
| // ZZZ Like ZZZZ but with a special case "UTC" for UTC. |
| // |
| // The format syntax is optimized mainly for generating machine-oriented |
| // timestamps rather than human-oriented timestamps; the English language |
| // portions of the output reflect the use of English names in a number of |
| // machine-readable date formatting standards. For presentation to humans, |
| // a locale-aware time formatter (not included in this package) is a better |
| // choice. |
| // |
| // The format syntax is not compatible with that of any other language, but |
| // is optimized so that patterns for common standard date formats can be |
| // recognized quickly even by a reader unfamiliar with the format syntax. |
| func FormatDate(format cty.Value, timestamp cty.Value) (cty.Value, error) { |
| return FormatDateFunc.Call([]cty.Value{format, timestamp}) |
| } |
| |
| func parseTimestamp(ts string) (time.Time, error) { |
| t, err := time.Parse(time.RFC3339, ts) |
| if err != nil { |
| switch err := err.(type) { |
| case *time.ParseError: |
| // If err is s time.ParseError then its string representation is not |
| // appropriate since it relies on details of Go's strange date format |
| // representation, which a caller of our functions is not expected |
| // to be familiar with. |
| // |
| // Therefore we do some light transformation to get a more suitable |
| // error that should make more sense to our callers. These are |
| // still not awesome error messages, but at least they refer to |
| // the timestamp portions by name rather than by Go's example |
| // values. |
| if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" { |
| // For some reason err.Message is populated with a ": " prefix |
| // by the time package. |
| return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message) |
| } |
| var what string |
| switch err.LayoutElem { |
| case "2006": |
| what = "year" |
| case "01": |
| what = "month" |
| case "02": |
| what = "day of month" |
| case "15": |
| what = "hour" |
| case "04": |
| what = "minute" |
| case "05": |
| what = "second" |
| case "Z07:00": |
| what = "UTC offset" |
| case "T": |
| return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'") |
| case ":", "-": |
| if err.ValueElem == "" { |
| return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem) |
| } else { |
| return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem) |
| } |
| default: |
| // Should never get here, because time.RFC3339 includes only the |
| // above portions, but since that might change in future we'll |
| // be robust here. |
| what = "timestamp segment" |
| } |
| if err.ValueElem == "" { |
| return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what) |
| } else { |
| return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what) |
| } |
| } |
| return time.Time{}, err |
| } |
| return t, nil |
| } |
| |
| // splitDataFormat is a bufio.SplitFunc used to tokenize a date format. |
| func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err error) { |
| if len(data) == 0 { |
| return 0, nil, nil |
| } |
| |
| const esc = '\'' |
| |
| switch { |
| |
| case data[0] == esc: |
| // If we have another quote immediately after then this is a single |
| // escaped escape. |
| if len(data) > 1 && data[1] == esc { |
| return 2, data[:2], nil |
| } |
| |
| // Beginning of quoted sequence, so we will seek forward until we find |
| // the closing quote, ignoring escaped quotes along the way. |
| for i := 1; i < len(data); i++ { |
| if data[i] == esc { |
| if (i + 1) == len(data) { |
| // We need at least one more byte to decide if this is an |
| // escape or a terminator. |
| return 0, nil, nil |
| } |
| if data[i+1] == esc { |
| i++ // doubled-up quotes are an escape sequence |
| continue |
| } |
| // We've found the closing quote |
| return i + 1, data[:i+1], nil |
| } |
| } |
| // If we fall out here then we need more bytes to find the end, |
| // unless we're already at the end with an unclosed quote. |
| if atEOF { |
| return len(data), data, nil |
| } |
| return 0, nil, nil |
| |
| case startsDateFormatVerb(data[0]): |
| rep := data[0] |
| for i := 1; i < len(data); i++ { |
| if data[i] != rep { |
| return i, data[:i], nil |
| } |
| } |
| if atEOF { |
| return len(data), data, nil |
| } |
| // We need more data to decide if we've found the end |
| return 0, nil, nil |
| |
| default: |
| for i := 1; i < len(data); i++ { |
| if data[i] == esc || startsDateFormatVerb(data[i]) { |
| return i, data[:i], nil |
| } |
| } |
| // We might not actually be at the end of a literal sequence, |
| // but that doesn't matter since we'll concat them back together |
| // anyway. |
| return len(data), data, nil |
| } |
| } |
| |
| func startsDateFormatVerb(b byte) bool { |
| return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') |
| } |