You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
435 lines
14 KiB
Go
435 lines
14 KiB
Go
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
|
|
},
|
|
})
|
|
|
|
// TimeAddFunc is a function that adds a duration to a timestamp, returning a new timestamp.
|
|
var TimeAddFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "timestamp",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "duration",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.String),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
ts, err := parseTimestamp(args[0].AsString())
|
|
if err != nil {
|
|
return cty.UnknownVal(cty.String), err
|
|
}
|
|
duration, err := time.ParseDuration(args[1].AsString())
|
|
if err != nil {
|
|
return cty.UnknownVal(cty.String), err
|
|
}
|
|
|
|
return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), 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) {
|
|
if atEOF {
|
|
// We have a closing quote and are at the end of our input
|
|
return len(data), data, nil
|
|
} else {
|
|
// 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')
|
|
}
|
|
|
|
// TimeAdd adds a duration to a timestamp, returning a new timestamp.
|
|
//
|
|
// In the HCL language, timestamps are conventionally represented as
|
|
// strings using RFC 3339 "Date and Time format" syntax. Timeadd requires
|
|
// the timestamp argument to be a string conforming to this syntax.
|
|
//
|
|
// `duration` is a string representation of a time difference, consisting of
|
|
// sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted
|
|
// units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first
|
|
// number may be negative to indicate a negative duration, like `"-2h5m"`.
|
|
//
|
|
// The result is a string, also in RFC 3339 format, representing the result
|
|
// of adding the given direction to the given timestamp.
|
|
func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) {
|
|
return TimeAddFunc.Call([]cty.Value{timestamp, duration})
|
|
}
|