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