github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/lang/funcs/datetime.go (about) 1 package funcs 2 3 import ( 4 "fmt" 5 "time" 6 7 "github.com/zclconf/go-cty/cty" 8 "github.com/zclconf/go-cty/cty/function" 9 ) 10 11 // TimestampFunc constructs a function that returns a string representation of the current date and time. 12 var TimestampFunc = function.New(&function.Spec{ 13 Params: []function.Parameter{}, 14 Type: function.StaticReturnType(cty.String), 15 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 16 return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil 17 }, 18 }) 19 20 // TimeAddFunc constructs a function that adds a duration to a timestamp, returning a new timestamp. 21 var TimeAddFunc = function.New(&function.Spec{ 22 Params: []function.Parameter{ 23 { 24 Name: "timestamp", 25 Type: cty.String, 26 }, 27 { 28 Name: "duration", 29 Type: cty.String, 30 }, 31 }, 32 Type: function.StaticReturnType(cty.String), 33 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 34 ts, err := parseTimestamp(args[0].AsString()) 35 if err != nil { 36 return cty.UnknownVal(cty.String), err 37 } 38 duration, err := time.ParseDuration(args[1].AsString()) 39 if err != nil { 40 return cty.UnknownVal(cty.String), err 41 } 42 43 return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil 44 }, 45 }) 46 47 // TimeCmpFunc is a function that compares two timestamps. 48 var TimeCmpFunc = function.New(&function.Spec{ 49 Params: []function.Parameter{ 50 { 51 Name: "timestamp_a", 52 Type: cty.String, 53 }, 54 { 55 Name: "timestamp_b", 56 Type: cty.String, 57 }, 58 }, 59 Type: function.StaticReturnType(cty.Number), 60 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 61 tsA, err := parseTimestamp(args[0].AsString()) 62 if err != nil { 63 return cty.UnknownVal(cty.String), function.NewArgError(0, err) 64 } 65 tsB, err := parseTimestamp(args[1].AsString()) 66 if err != nil { 67 return cty.UnknownVal(cty.String), function.NewArgError(1, err) 68 } 69 70 switch { 71 case tsA.Equal(tsB): 72 return cty.NumberIntVal(0), nil 73 case tsA.Before(tsB): 74 return cty.NumberIntVal(-1), nil 75 default: 76 // By elimintation, tsA must be after tsB. 77 return cty.NumberIntVal(1), nil 78 } 79 }, 80 }) 81 82 // Timestamp returns a string representation of the current date and time. 83 // 84 // In the Terraform language, timestamps are conventionally represented as 85 // strings using RFC 3339 "Date and Time format" syntax, and so timestamp 86 // returns a string in this format. 87 func Timestamp() (cty.Value, error) { 88 return TimestampFunc.Call([]cty.Value{}) 89 } 90 91 // TimeAdd adds a duration to a timestamp, returning a new timestamp. 92 // 93 // In the Terraform language, timestamps are conventionally represented as 94 // strings using RFC 3339 "Date and Time format" syntax. Timeadd requires 95 // the timestamp argument to be a string conforming to this syntax. 96 // 97 // `duration` is a string representation of a time difference, consisting of 98 // sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted 99 // units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first 100 // number may be negative to indicate a negative duration, like `"-2h5m"`. 101 // 102 // The result is a string, also in RFC 3339 format, representing the result 103 // of adding the given direction to the given timestamp. 104 func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) { 105 return TimeAddFunc.Call([]cty.Value{timestamp, duration}) 106 } 107 108 // TimeCmp compares two timestamps, indicating whether they are equal or 109 // if one is before the other. 110 // 111 // TimeCmp considers the UTC offset of each given timestamp when making its 112 // decision, so for example 6:00 +0200 and 4:00 UTC are equal. 113 // 114 // In the Terraform language, timestamps are conventionally represented as 115 // strings using RFC 3339 "Date and Time format" syntax. TimeCmp requires 116 // the timestamp argument to be a string conforming to this syntax. 117 // 118 // The result is always a number between -1 and 1. -1 indicates that 119 // timestampA is earlier than timestampB. 1 indicates that timestampA is 120 // later. 0 indicates that the two timestamps represent the same instant. 121 func TimeCmp(timestampA, timestampB cty.Value) (cty.Value, error) { 122 return TimeCmpFunc.Call([]cty.Value{timestampA, timestampB}) 123 } 124 125 func parseTimestamp(ts string) (time.Time, error) { 126 t, err := time.Parse(time.RFC3339, ts) 127 if err != nil { 128 switch err := err.(type) { 129 case *time.ParseError: 130 // If err is a time.ParseError then its string representation is not 131 // appropriate since it relies on details of Go's strange date format 132 // representation, which a caller of our functions is not expected 133 // to be familiar with. 134 // 135 // Therefore we do some light transformation to get a more suitable 136 // error that should make more sense to our callers. These are 137 // still not awesome error messages, but at least they refer to 138 // the timestamp portions by name rather than by Go's example 139 // values. 140 if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" { 141 // For some reason err.Message is populated with a ": " prefix 142 // by the time package. 143 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message) 144 } 145 var what string 146 switch err.LayoutElem { 147 case "2006": 148 what = "year" 149 case "01": 150 what = "month" 151 case "02": 152 what = "day of month" 153 case "15": 154 what = "hour" 155 case "04": 156 what = "minute" 157 case "05": 158 what = "second" 159 case "Z07:00": 160 what = "UTC offset" 161 case "T": 162 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'") 163 case ":", "-": 164 if err.ValueElem == "" { 165 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem) 166 } else { 167 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem) 168 } 169 default: 170 // Should never get here, because time.RFC3339 includes only the 171 // above portions, but since that might change in future we'll 172 // be robust here. 173 what = "timestamp segment" 174 } 175 if err.ValueElem == "" { 176 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what) 177 } else { 178 return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what) 179 } 180 } 181 return time.Time{}, err 182 } 183 return t, nil 184 }