]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package stdlib |
2 | ||
3 | import ( | |
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 | ||
14 | var 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 | // FormatDate reformats a timestamp given in RFC3339 syntax into another time | |
207 | // syntax defined by a given format string. | |
208 | // | |
209 | // The format string uses letter mnemonics to represent portions of the | |
210 | // timestamp, with repetition signifying length variants of each portion. | |
211 | // Single quote characters ' can be used to quote sequences of literal letters | |
212 | // that should not be interpreted as formatting mnemonics. | |
213 | // | |
214 | // The full set of supported mnemonic sequences is listed below: | |
215 | // | |
216 | // YY Year modulo 100 zero-padded to two digits, like "06". | |
217 | // YYYY Four (or more) digit year, like "2006". | |
218 | // M Month number, like "1" for January. | |
219 | // MM Month number zero-padded to two digits, like "01". | |
220 | // MMM English month name abbreviated to three letters, like "Jan". | |
221 | // MMMM English month name unabbreviated, like "January". | |
222 | // D Day of month number, like "2". | |
223 | // DD Day of month number zero-padded to two digits, like "02". | |
224 | // EEE English day of week name abbreviated to three letters, like "Mon". | |
225 | // EEEE English day of week name unabbreviated, like "Monday". | |
226 | // h 24-hour number, like "2". | |
227 | // hh 24-hour number zero-padded to two digits, like "02". | |
228 | // H 12-hour number, like "2". | |
229 | // HH 12-hour number zero-padded to two digits, like "02". | |
230 | // AA Hour AM/PM marker in uppercase, like "AM". | |
231 | // aa Hour AM/PM marker in lowercase, like "am". | |
232 | // m Minute within hour, like "5". | |
233 | // mm Minute within hour zero-padded to two digits, like "05". | |
234 | // s Second within minute, like "9". | |
235 | // ss Second within minute zero-padded to two digits, like "09". | |
236 | // ZZZZ Timezone offset with just sign and digit, like "-0800". | |
237 | // ZZZZZ Timezone offset with colon separating hours and minutes, like "-08:00". | |
238 | // Z Like ZZZZZ but with a special case "Z" for UTC. | |
239 | // ZZZ Like ZZZZ but with a special case "UTC" for UTC. | |
240 | // | |
241 | // The format syntax is optimized mainly for generating machine-oriented | |
242 | // timestamps rather than human-oriented timestamps; the English language | |
243 | // portions of the output reflect the use of English names in a number of | |
244 | // machine-readable date formatting standards. For presentation to humans, | |
245 | // a locale-aware time formatter (not included in this package) is a better | |
246 | // choice. | |
247 | // | |
248 | // The format syntax is not compatible with that of any other language, but | |
249 | // is optimized so that patterns for common standard date formats can be | |
250 | // recognized quickly even by a reader unfamiliar with the format syntax. | |
251 | func FormatDate(format cty.Value, timestamp cty.Value) (cty.Value, error) { | |
252 | return FormatDateFunc.Call([]cty.Value{format, timestamp}) | |
253 | } | |
254 | ||
255 | func parseTimestamp(ts string) (time.Time, error) { | |
256 | t, err := time.Parse(time.RFC3339, ts) | |
257 | if err != nil { | |
258 | switch err := err.(type) { | |
259 | case *time.ParseError: | |
260 | // If err is s time.ParseError then its string representation is not | |
261 | // appropriate since it relies on details of Go's strange date format | |
262 | // representation, which a caller of our functions is not expected | |
263 | // to be familiar with. | |
264 | // | |
265 | // Therefore we do some light transformation to get a more suitable | |
266 | // error that should make more sense to our callers. These are | |
267 | // still not awesome error messages, but at least they refer to | |
268 | // the timestamp portions by name rather than by Go's example | |
269 | // values. | |
270 | if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" { | |
271 | // For some reason err.Message is populated with a ": " prefix | |
272 | // by the time package. | |
273 | return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message) | |
274 | } | |
275 | var what string | |
276 | switch err.LayoutElem { | |
277 | case "2006": | |
278 | what = "year" | |
279 | case "01": | |
280 | what = "month" | |
281 | case "02": | |
282 | what = "day of month" | |
283 | case "15": | |
284 | what = "hour" | |
285 | case "04": | |
286 | what = "minute" | |
287 | case "05": | |
288 | what = "second" | |
289 | case "Z07:00": | |
290 | what = "UTC offset" | |
291 | case "T": | |
292 | return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'") | |
293 | case ":", "-": | |
294 | if err.ValueElem == "" { | |
295 | return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem) | |
296 | } else { | |
297 | return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem) | |
298 | } | |
299 | default: | |
300 | // Should never get here, because time.RFC3339 includes only the | |
301 | // above portions, but since that might change in future we'll | |
302 | // be robust here. | |
303 | what = "timestamp segment" | |
304 | } | |
305 | if err.ValueElem == "" { | |
306 | return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what) | |
307 | } else { | |
308 | return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what) | |
309 | } | |
310 | } | |
311 | return time.Time{}, err | |
312 | } | |
313 | return t, nil | |
314 | } | |
315 | ||
316 | // splitDataFormat is a bufio.SplitFunc used to tokenize a date format. | |
317 | func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err error) { | |
318 | if len(data) == 0 { | |
319 | return 0, nil, nil | |
320 | } | |
321 | ||
322 | const esc = '\'' | |
323 | ||
324 | switch { | |
325 | ||
326 | case data[0] == esc: | |
327 | // If we have another quote immediately after then this is a single | |
328 | // escaped escape. | |
329 | if len(data) > 1 && data[1] == esc { | |
330 | return 2, data[:2], nil | |
331 | } | |
332 | ||
333 | // Beginning of quoted sequence, so we will seek forward until we find | |
334 | // the closing quote, ignoring escaped quotes along the way. | |
335 | for i := 1; i < len(data); i++ { | |
336 | if data[i] == esc { | |
337 | if (i + 1) == len(data) { | |
338 | // We need at least one more byte to decide if this is an | |
339 | // escape or a terminator. | |
340 | return 0, nil, nil | |
341 | } | |
342 | if data[i+1] == esc { | |
343 | i++ // doubled-up quotes are an escape sequence | |
344 | continue | |
345 | } | |
346 | // We've found the closing quote | |
347 | return i + 1, data[:i+1], nil | |
348 | } | |
349 | } | |
350 | // If we fall out here then we need more bytes to find the end, | |
351 | // unless we're already at the end with an unclosed quote. | |
352 | if atEOF { | |
353 | return len(data), data, nil | |
354 | } | |
355 | return 0, nil, nil | |
356 | ||
357 | case startsDateFormatVerb(data[0]): | |
358 | rep := data[0] | |
359 | for i := 1; i < len(data); i++ { | |
360 | if data[i] != rep { | |
361 | return i, data[:i], nil | |
362 | } | |
363 | } | |
364 | if atEOF { | |
365 | return len(data), data, nil | |
366 | } | |
367 | // We need more data to decide if we've found the end | |
368 | return 0, nil, nil | |
369 | ||
370 | default: | |
371 | for i := 1; i < len(data); i++ { | |
372 | if data[i] == esc || startsDateFormatVerb(data[i]) { | |
373 | return i, data[:i], nil | |
374 | } | |
375 | } | |
376 | // We might not actually be at the end of a literal sequence, | |
377 | // but that doesn't matter since we'll concat them back together | |
378 | // anyway. | |
379 | return len(data), data, nil | |
380 | } | |
381 | } | |
382 | ||
383 | func startsDateFormatVerb(b byte) bool { | |
384 | return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') | |
385 | } |