github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/funcs/datetime.go (about)

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