2022-04-03 04:07:16 +00:00
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 {
2022-12-24 16:57:19 +00:00
Description : ` Formats a timestamp given in RFC 3339 syntax into another timestamp in some other machine-oriented time syntax, as described in the format string. ` ,
2022-04-03 04:07:16 +00:00
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 {
2022-12-24 16:57:19 +00:00
Description : ` Adds the duration represented by the given duration string to the given RFC 3339 timestamp string, returning another RFC 3339 timestamp. ` ,
2022-04-03 04:07:16 +00:00
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 } )
}