1package stdlib
2
3import (
4	"bufio"
5	"bytes"
6	"fmt"
7	"strings"
8	"time"
9
10	"github.com/zclconf/go-cty/cty"
11	"github.com/zclconf/go-cty/cty/function"
12)
13
14var FormatDateFunc = function.New(&function.Spec{
15	Params: []function.Parameter{
16		{
17			Name: "format",
18			Type: cty.String,
19		},
20		{
21			Name: "time",
22			Type: cty.String,
23		},
24	},
25	Type: function.StaticReturnType(cty.String),
26	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
27		formatStr := args[0].AsString()
28		timeStr := args[1].AsString()
29		t, err := parseTimestamp(timeStr)
30		if err != nil {
31			return cty.DynamicVal, function.NewArgError(1, err)
32		}
33
34		var buf bytes.Buffer
35		sc := bufio.NewScanner(strings.NewReader(formatStr))
36		sc.Split(splitDateFormat)
37		const esc = '\''
38		for sc.Scan() {
39			tok := sc.Bytes()
40
41			// The leading byte signals the token type
42			switch {
43			case tok[0] == esc:
44				if tok[len(tok)-1] != esc || len(tok) == 1 {
45					return cty.DynamicVal, function.NewArgErrorf(0, "unterminated literal '")
46				}
47				if len(tok) == 2 {
48					// Must be a single escaped quote, ''
49					buf.WriteByte(esc)
50				} else {
51					// The content (until a closing esc) is printed out verbatim
52					// except that we must un-double any double-esc escapes in
53					// the middle of the string.
54					raw := tok[1 : len(tok)-1]
55					for i := 0; i < len(raw); i++ {
56						buf.WriteByte(raw[i])
57						if raw[i] == esc {
58							i++ // skip the escaped quote
59						}
60					}
61				}
62
63			case startsDateFormatVerb(tok[0]):
64				switch tok[0] {
65				case 'Y':
66					y := t.Year()
67					switch len(tok) {
68					case 2:
69						fmt.Fprintf(&buf, "%02d", y%100)
70					case 4:
71						fmt.Fprintf(&buf, "%04d", y)
72					default:
73						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: year must either be \"YY\" or \"YYYY\"", tok)
74					}
75				case 'M':
76					m := t.Month()
77					switch len(tok) {
78					case 1:
79						fmt.Fprintf(&buf, "%d", m)
80					case 2:
81						fmt.Fprintf(&buf, "%02d", m)
82					case 3:
83						buf.WriteString(m.String()[:3])
84					case 4:
85						buf.WriteString(m.String())
86					default:
87						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: month must be \"M\", \"MM\", \"MMM\", or \"MMMM\"", tok)
88					}
89				case 'D':
90					d := t.Day()
91					switch len(tok) {
92					case 1:
93						fmt.Fprintf(&buf, "%d", d)
94					case 2:
95						fmt.Fprintf(&buf, "%02d", d)
96					default:
97						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of month must either be \"D\" or \"DD\"", tok)
98					}
99				case 'E':
100					d := t.Weekday()
101					switch len(tok) {
102					case 3:
103						buf.WriteString(d.String()[:3])
104					case 4:
105						buf.WriteString(d.String())
106					default:
107						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of week must either be \"EEE\" or \"EEEE\"", tok)
108					}
109				case 'h':
110					h := t.Hour()
111					switch len(tok) {
112					case 1:
113						fmt.Fprintf(&buf, "%d", h)
114					case 2:
115						fmt.Fprintf(&buf, "%02d", h)
116					default:
117						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 24-hour must either be \"h\" or \"hh\"", tok)
118					}
119				case 'H':
120					h := t.Hour() % 12
121					if h == 0 {
122						h = 12
123					}
124					switch len(tok) {
125					case 1:
126						fmt.Fprintf(&buf, "%d", h)
127					case 2:
128						fmt.Fprintf(&buf, "%02d", h)
129					default:
130						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 12-hour must either be \"H\" or \"HH\"", tok)
131					}
132				case 'A', 'a':
133					if len(tok) != 2 {
134						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: must be \"%s%s\"", tok, tok[0:1], tok[0:1])
135					}
136					upper := tok[0] == 'A'
137					switch t.Hour() / 12 {
138					case 0:
139						if upper {
140							buf.WriteString("AM")
141						} else {
142							buf.WriteString("am")
143						}
144					case 1:
145						if upper {
146							buf.WriteString("PM")
147						} else {
148							buf.WriteString("pm")
149						}
150					}
151				case 'm':
152					m := t.Minute()
153					switch len(tok) {
154					case 1:
155						fmt.Fprintf(&buf, "%d", m)
156					case 2:
157						fmt.Fprintf(&buf, "%02d", m)
158					default:
159						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: minute must either be \"m\" or \"mm\"", tok)
160					}
161				case 's':
162					s := t.Second()
163					switch len(tok) {
164					case 1:
165						fmt.Fprintf(&buf, "%d", s)
166					case 2:
167						fmt.Fprintf(&buf, "%02d", s)
168					default:
169						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: second must either be \"s\" or \"ss\"", tok)
170					}
171				case 'Z':
172					// We'll just lean on Go's own formatter for this one, since
173					// the necessary information is unexported.
174					switch len(tok) {
175					case 1:
176						buf.WriteString(t.Format("Z07:00"))
177					case 3:
178						str := t.Format("-0700")
179						switch str {
180						case "+0000":
181							buf.WriteString("UTC")
182						default:
183							buf.WriteString(str)
184						}
185					case 4:
186						buf.WriteString(t.Format("-0700"))
187					case 5:
188						buf.WriteString(t.Format("-07:00"))
189					default:
190						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: timezone must be Z, ZZZZ, or ZZZZZ", tok)
191					}
192				default:
193					return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q", tok)
194				}
195
196			default:
197				// Any other starting character indicates a literal sequence
198				buf.Write(tok)
199			}
200		}
201
202		return cty.StringVal(buf.String()), nil
203	},
204})
205
206// TimeAddFunc is a function that adds a duration to a timestamp, returning a new timestamp.
207var TimeAddFunc = function.New(&function.Spec{
208	Params: []function.Parameter{
209		{
210			Name: "timestamp",
211			Type: cty.String,
212		},
213		{
214			Name: "duration",
215			Type: cty.String,
216		},
217	},
218	Type: function.StaticReturnType(cty.String),
219	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
220		ts, err := parseTimestamp(args[0].AsString())
221		if err != nil {
222			return cty.UnknownVal(cty.String), err
223		}
224		duration, err := time.ParseDuration(args[1].AsString())
225		if err != nil {
226			return cty.UnknownVal(cty.String), err
227		}
228
229		return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil
230	},
231})
232
233// FormatDate reformats a timestamp given in RFC3339 syntax into another time
234// syntax defined by a given format string.
235//
236// The format string uses letter mnemonics to represent portions of the
237// timestamp, with repetition signifying length variants of each portion.
238// Single quote characters ' can be used to quote sequences of literal letters
239// that should not be interpreted as formatting mnemonics.
240//
241// The full set of supported mnemonic sequences is listed below:
242//
243//     YY       Year modulo 100 zero-padded to two digits, like "06".
244//     YYYY     Four (or more) digit year, like "2006".
245//     M        Month number, like "1" for January.
246//     MM       Month number zero-padded to two digits, like "01".
247//     MMM      English month name abbreviated to three letters, like "Jan".
248//     MMMM     English month name unabbreviated, like "January".
249//     D        Day of month number, like "2".
250//     DD       Day of month number zero-padded to two digits, like "02".
251//     EEE      English day of week name abbreviated to three letters, like "Mon".
252//     EEEE     English day of week name unabbreviated, like "Monday".
253//     h        24-hour number, like "2".
254//     hh       24-hour number zero-padded to two digits, like "02".
255//     H        12-hour number, like "2".
256//     HH       12-hour number zero-padded to two digits, like "02".
257//     AA       Hour AM/PM marker in uppercase, like "AM".
258//     aa       Hour AM/PM marker in lowercase, like "am".
259//     m        Minute within hour, like "5".
260//     mm       Minute within hour zero-padded to two digits, like "05".
261//     s        Second within minute, like "9".
262//     ss       Second within minute zero-padded to two digits, like "09".
263//     ZZZZ     Timezone offset with just sign and digit, like "-0800".
264//     ZZZZZ    Timezone offset with colon separating hours and minutes, like "-08:00".
265//     Z        Like ZZZZZ but with a special case "Z" for UTC.
266//     ZZZ      Like ZZZZ but with a special case "UTC" for UTC.
267//
268// The format syntax is optimized mainly for generating machine-oriented
269// timestamps rather than human-oriented timestamps; the English language
270// portions of the output reflect the use of English names in a number of
271// machine-readable date formatting standards. For presentation to humans,
272// a locale-aware time formatter (not included in this package) is a better
273// choice.
274//
275// The format syntax is not compatible with that of any other language, but
276// is optimized so that patterns for common standard date formats can be
277// recognized quickly even by a reader unfamiliar with the format syntax.
278func FormatDate(format cty.Value, timestamp cty.Value) (cty.Value, error) {
279	return FormatDateFunc.Call([]cty.Value{format, timestamp})
280}
281
282func parseTimestamp(ts string) (time.Time, error) {
283	t, err := time.Parse(time.RFC3339, ts)
284	if err != nil {
285		switch err := err.(type) {
286		case *time.ParseError:
287			// If err is s time.ParseError then its string representation is not
288			// appropriate since it relies on details of Go's strange date format
289			// representation, which a caller of our functions is not expected
290			// to be familiar with.
291			//
292			// Therefore we do some light transformation to get a more suitable
293			// error that should make more sense to our callers. These are
294			// still not awesome error messages, but at least they refer to
295			// the timestamp portions by name rather than by Go's example
296			// values.
297			if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" {
298				// For some reason err.Message is populated with a ": " prefix
299				// by the time package.
300				return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message)
301			}
302			var what string
303			switch err.LayoutElem {
304			case "2006":
305				what = "year"
306			case "01":
307				what = "month"
308			case "02":
309				what = "day of month"
310			case "15":
311				what = "hour"
312			case "04":
313				what = "minute"
314			case "05":
315				what = "second"
316			case "Z07:00":
317				what = "UTC offset"
318			case "T":
319				return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'")
320			case ":", "-":
321				if err.ValueElem == "" {
322					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem)
323				} else {
324					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem)
325				}
326			default:
327				// Should never get here, because time.RFC3339 includes only the
328				// above portions, but since that might change in future we'll
329				// be robust here.
330				what = "timestamp segment"
331			}
332			if err.ValueElem == "" {
333				return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what)
334			} else {
335				return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what)
336			}
337		}
338		return time.Time{}, err
339	}
340	return t, nil
341}
342
343// splitDataFormat is a bufio.SplitFunc used to tokenize a date format.
344func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err error) {
345	if len(data) == 0 {
346		return 0, nil, nil
347	}
348
349	const esc = '\''
350
351	switch {
352
353	case data[0] == esc:
354		// If we have another quote immediately after then this is a single
355		// escaped escape.
356		if len(data) > 1 && data[1] == esc {
357			return 2, data[:2], nil
358		}
359
360		// Beginning of quoted sequence, so we will seek forward until we find
361		// the closing quote, ignoring escaped quotes along the way.
362		for i := 1; i < len(data); i++ {
363			if data[i] == esc {
364				if (i + 1) == len(data) {
365					if atEOF {
366						// We have a closing quote and are at the end of our input
367						return len(data), data, nil
368					} else {
369						// We need at least one more byte to decide if this is an
370						// escape or a terminator.
371						return 0, nil, nil
372					}
373				}
374				if data[i+1] == esc {
375					i++ // doubled-up quotes are an escape sequence
376					continue
377				}
378				// We've found the closing quote
379				return i + 1, data[:i+1], nil
380			}
381		}
382		// If we fall out here then we need more bytes to find the end,
383		// unless we're already at the end with an unclosed quote.
384		if atEOF {
385			return len(data), data, nil
386		}
387		return 0, nil, nil
388
389	case startsDateFormatVerb(data[0]):
390		rep := data[0]
391		for i := 1; i < len(data); i++ {
392			if data[i] != rep {
393				return i, data[:i], nil
394			}
395		}
396		if atEOF {
397			return len(data), data, nil
398		}
399		// We need more data to decide if we've found the end
400		return 0, nil, nil
401
402	default:
403		for i := 1; i < len(data); i++ {
404			if data[i] == esc || startsDateFormatVerb(data[i]) {
405				return i, data[:i], nil
406			}
407		}
408		// We might not actually be at the end of a literal sequence,
409		// but that doesn't matter since we'll concat them back together
410		// anyway.
411		return len(data), data, nil
412	}
413}
414
415func startsDateFormatVerb(b byte) bool {
416	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
417}
418
419// TimeAdd adds a duration to a timestamp, returning a new timestamp.
420//
421// In the HCL language, timestamps are conventionally represented as
422// strings using RFC 3339 "Date and Time format" syntax. Timeadd requires
423// the timestamp argument to be a string conforming to this syntax.
424//
425// `duration` is a string representation of a time difference, consisting of
426// sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted
427// units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first
428// number may be negative to indicate a negative duration, like `"-2h5m"`.
429//
430// The result is a string, also in RFC 3339 format, representing the result
431// of adding the given direction to the given timestamp.
432func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) {
433	return TimeAddFunc.Call([]cty.Value{timestamp, duration})
434}
435