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