go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/lib/time/time.go (about)

     1  // Copyright 2021 The Bazel Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package time provides time-related constants and functions.
     6  package time // import "go.starlark.net/lib/time"
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"sort"
    12  	"time"
    13  
    14  	"go.starlark.net/starlark"
    15  	"go.starlark.net/starlarkstruct"
    16  	"go.starlark.net/syntax"
    17  )
    18  
    19  // Module time is a Starlark module of time-related functions and constants.
    20  // The module defines the following functions:
    21  //
    22  //     from_timestamp(sec, nsec) - Converts the given Unix time corresponding to the number of seconds
    23  //                                 and (optionally) nanoseconds since January 1, 1970 UTC into an object
    24  //                                 of type Time. For more details, refer to https://pkg.go.dev/time#Unix.
    25  //
    26  //     is_valid_timezone(loc) - Reports whether loc is a valid time zone name.
    27  //
    28  //     now() - Returns the current local time. Applications may replace this function by a deterministic one.
    29  //
    30  //     parse_duration(d) - Parses the given duration string. For more details, refer to
    31  //                         https://pkg.go.dev/time#ParseDuration.
    32  //
    33  //     parse_time(x, format, location) - Parses the given time string using a specific time format and location.
    34  //                                      The expected arguments are a time string (mandatory), a time format
    35  //                                      (optional, set to RFC3339 by default, e.g. "2021-03-22T23:20:50.52Z")
    36  //                                      and a name of location (optional, set to UTC by default). For more details,
    37  //                                      refer to https://pkg.go.dev/time#Parse and https://pkg.go.dev/time#ParseInLocation.
    38  //
    39  //     time(year, month, day, hour, minute, second, nanosecond, location) - Returns the Time corresponding to
    40  //	                                                                        yyyy-mm-dd hh:mm:ss + nsec nanoseconds
    41  //                                                                          in the appropriate zone for that time
    42  //                                                                          in the given location. All the parameters
    43  //                                                                          are optional.
    44  // The module also defines the following constants:
    45  //
    46  //     nanosecond - A duration representing one nanosecond.
    47  //     microsecond - A duration representing one microsecond.
    48  //     millisecond - A duration representing one millisecond.
    49  //     second - A duration representing one second.
    50  //     minute - A duration representing one minute.
    51  //     hour - A duration representing one hour.
    52  //
    53  var Module = &starlarkstruct.Module{
    54  	Name: "time",
    55  	Members: starlark.StringDict{
    56  		"from_timestamp":    starlark.NewBuiltin("from_timestamp", fromTimestamp),
    57  		"is_valid_timezone": starlark.NewBuiltin("is_valid_timezone", isValidTimezone),
    58  		"now":               starlark.NewBuiltin("now", now),
    59  		"parse_duration":    starlark.NewBuiltin("parse_duration", parseDuration),
    60  		"parse_time":        starlark.NewBuiltin("parse_time", parseTime),
    61  		"time":              starlark.NewBuiltin("time", newTime),
    62  
    63  		"nanosecond":  Duration(time.Nanosecond),
    64  		"microsecond": Duration(time.Microsecond),
    65  		"millisecond": Duration(time.Millisecond),
    66  		"second":      Duration(time.Second),
    67  		"minute":      Duration(time.Minute),
    68  		"hour":        Duration(time.Hour),
    69  	},
    70  }
    71  
    72  // NowFunc is a function that reports the current time. Intentionally exported
    73  // so that it can be overridden, for example by applications that require their
    74  // Starlark scripts to be fully deterministic.
    75  //
    76  // Deprecated: avoid updating this global variable
    77  // and instead use SetNow on each thread to set its clock function.
    78  var NowFunc = time.Now
    79  
    80  const contextKey = "time.now"
    81  
    82  // SetNow sets the thread's optional clock function.
    83  // If non-nil, it will be used in preference to NowFunc when the
    84  // thread requests the current time by executing a call to time.now.
    85  func SetNow(thread *starlark.Thread, nowFunc func() (time.Time, error)) {
    86  	thread.SetLocal(contextKey, nowFunc)
    87  }
    88  
    89  // Now returns the clock function previously associated with this thread.
    90  func Now(thread *starlark.Thread) func() (time.Time, error) {
    91  	nowFunc, _ := thread.Local(contextKey).(func() (time.Time, error))
    92  	return nowFunc
    93  }
    94  
    95  func parseDuration(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    96  	var d Duration
    97  	err := starlark.UnpackPositionalArgs("parse_duration", args, kwargs, 1, &d)
    98  	return d, err
    99  }
   100  
   101  func isValidTimezone(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   102  	var s string
   103  	if err := starlark.UnpackPositionalArgs("is_valid_timezone", args, kwargs, 1, &s); err != nil {
   104  		return nil, err
   105  	}
   106  	_, err := time.LoadLocation(s)
   107  	return starlark.Bool(err == nil), nil
   108  }
   109  
   110  func parseTime(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   111  	var (
   112  		x        string
   113  		location = "UTC"
   114  		format   = time.RFC3339
   115  	)
   116  	if err := starlark.UnpackArgs("parse_time", args, kwargs, "x", &x, "format?", &format, "location?", &location); err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	if location == "UTC" {
   121  		t, err := time.Parse(format, x)
   122  		if err != nil {
   123  			return nil, err
   124  		}
   125  		return Time(t), nil
   126  	}
   127  
   128  	loc, err := time.LoadLocation(location)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	t, err := time.ParseInLocation(format, x, loc)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	return Time(t), nil
   137  }
   138  
   139  func fromTimestamp(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   140  	var (
   141  		sec  int64
   142  		nsec int64 = 0
   143  	)
   144  	if err := starlark.UnpackPositionalArgs("from_timestamp", args, kwargs, 1, &sec, &nsec); err != nil {
   145  		return nil, err
   146  	}
   147  	return Time(time.Unix(sec, nsec)), nil
   148  }
   149  
   150  func now(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   151  	nowErrFunc := Now(thread)
   152  	if nowErrFunc != nil {
   153  		t, err := nowErrFunc()
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  		return Time(t), nil
   158  	}
   159  	nowFunc := NowFunc
   160  	if nowFunc == nil {
   161  		return nil, errors.New("time.now() is not available")
   162  	}
   163  	return Time(nowFunc()), nil
   164  }
   165  
   166  // Duration is a Starlark representation of a duration.
   167  type Duration time.Duration
   168  
   169  // Assert at compile time that Duration implements Unpacker.
   170  var _ starlark.Unpacker = (*Duration)(nil)
   171  
   172  // Unpack is a custom argument unpacker
   173  func (d *Duration) Unpack(v starlark.Value) error {
   174  	switch x := v.(type) {
   175  	case Duration:
   176  		*d = x
   177  		return nil
   178  	case starlark.String:
   179  		dur, err := time.ParseDuration(string(x))
   180  		if err != nil {
   181  			return err
   182  		}
   183  
   184  		*d = Duration(dur)
   185  		return nil
   186  	}
   187  
   188  	return fmt.Errorf("got %s, want a duration, string, or int", v.Type())
   189  }
   190  
   191  // String implements the Stringer interface.
   192  func (d Duration) String() string { return time.Duration(d).String() }
   193  
   194  // Type returns a short string describing the value's type.
   195  func (d Duration) Type() string { return "time.duration" }
   196  
   197  // Freeze renders Duration immutable. required by starlark.Value interface
   198  // because duration is already immutable this is a no-op.
   199  func (d Duration) Freeze() {}
   200  
   201  // Hash returns a function of x such that Equals(x, y) => Hash(x) == Hash(y)
   202  // required by starlark.Value interface.
   203  func (d Duration) Hash() (uint32, error) {
   204  	return uint32(d) ^ uint32(int64(d)>>32), nil
   205  }
   206  
   207  // Truth reports whether the duration is non-zero.
   208  func (d Duration) Truth() starlark.Bool { return d != 0 }
   209  
   210  // Attr gets a value for a string attribute, implementing dot expression support
   211  // in starklark. required by starlark.HasAttrs interface.
   212  func (d Duration) Attr(name string) (starlark.Value, error) {
   213  	switch name {
   214  	case "hours":
   215  		return starlark.Float(time.Duration(d).Hours()), nil
   216  	case "minutes":
   217  		return starlark.Float(time.Duration(d).Minutes()), nil
   218  	case "seconds":
   219  		return starlark.Float(time.Duration(d).Seconds()), nil
   220  	case "milliseconds":
   221  		return starlark.MakeInt64(time.Duration(d).Milliseconds()), nil
   222  	case "microseconds":
   223  		return starlark.MakeInt64(time.Duration(d).Microseconds()), nil
   224  	case "nanoseconds":
   225  		return starlark.MakeInt64(time.Duration(d).Nanoseconds()), nil
   226  	}
   227  	return nil, fmt.Errorf("unrecognized %s attribute %q", d.Type(), name)
   228  }
   229  
   230  // AttrNames lists available dot expression strings. required by
   231  // starlark.HasAttrs interface.
   232  func (d Duration) AttrNames() []string {
   233  	return []string{
   234  		"hours",
   235  		"minutes",
   236  		"seconds",
   237  		"milliseconds",
   238  		"microseconds",
   239  		"nanoseconds",
   240  	}
   241  }
   242  
   243  // Cmp implements comparison of two Duration values. required by
   244  // starlark.TotallyOrdered interface.
   245  func (d Duration) Cmp(v starlark.Value, depth int) (int, error) {
   246  	if x, y := d, v.(Duration); x < y {
   247  		return -1, nil
   248  	} else if x > y {
   249  		return 1, nil
   250  	}
   251  	return 0, nil
   252  }
   253  
   254  // Binary implements binary operators, which satisfies the starlark.HasBinary
   255  // interface. operators:
   256  //    duration + duration = duration
   257  //    duration + time = time
   258  //    duration - duration = duration
   259  //    duration / duration = float
   260  //    duration / int = duration
   261  //    duration / float = duration
   262  //    duration // duration = int
   263  //    duration * int = duration
   264  func (d Duration) Binary(op syntax.Token, y starlark.Value, side starlark.Side) (starlark.Value, error) {
   265  	x := time.Duration(d)
   266  
   267  	switch op {
   268  	case syntax.PLUS:
   269  		switch y := y.(type) {
   270  		case Duration:
   271  			return Duration(x + time.Duration(y)), nil
   272  		case Time:
   273  			return Time(time.Time(y).Add(x)), nil
   274  		}
   275  
   276  	case syntax.MINUS:
   277  		switch y := y.(type) {
   278  		case Duration:
   279  			return Duration(x - time.Duration(y)), nil
   280  		}
   281  
   282  	case syntax.SLASH:
   283  		switch y := y.(type) {
   284  		case Duration:
   285  			if y == 0 {
   286  				return nil, fmt.Errorf("%s division by zero", d.Type())
   287  			}
   288  			return starlark.Float(x.Nanoseconds()) / starlark.Float(time.Duration(y).Nanoseconds()), nil
   289  		case starlark.Int:
   290  			if side == starlark.Right {
   291  				return nil, fmt.Errorf("unsupported operation")
   292  			}
   293  			i, ok := y.Int64()
   294  			if !ok {
   295  				return nil, fmt.Errorf("int value out of range (want signed 64-bit value)")
   296  			}
   297  			if i == 0 {
   298  				return nil, fmt.Errorf("%s division by zero", d.Type())
   299  			}
   300  			return d / Duration(i), nil
   301  		case starlark.Float:
   302  			f := float64(y)
   303  			if f == 0 {
   304  				return nil, fmt.Errorf("%s division by zero", d.Type())
   305  			}
   306  			return Duration(float64(x.Nanoseconds()) / f), nil
   307  		}
   308  
   309  	case syntax.SLASHSLASH:
   310  		switch y := y.(type) {
   311  		case Duration:
   312  			if y == 0 {
   313  				return nil, fmt.Errorf("%s division by zero", d.Type())
   314  			}
   315  			return starlark.MakeInt64(x.Nanoseconds() / time.Duration(y).Nanoseconds()), nil
   316  		}
   317  
   318  	case syntax.STAR:
   319  		switch y := y.(type) {
   320  		case starlark.Int:
   321  			i, ok := y.Int64()
   322  			if !ok {
   323  				return nil, fmt.Errorf("int value out of range (want signed 64-bit value)")
   324  			}
   325  			return d * Duration(i), nil
   326  		}
   327  	}
   328  
   329  	return nil, nil
   330  }
   331  
   332  // Time is a Starlark representation of a moment in time.
   333  type Time time.Time
   334  
   335  func newTime(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   336  	var (
   337  		year, month, day, hour, min, sec, nsec int
   338  		loc                                    string
   339  	)
   340  	if err := starlark.UnpackArgs("time", args, kwargs,
   341  		"year?", &year,
   342  		"month?", &month,
   343  		"day?", &day,
   344  		"hour?", &hour,
   345  		"minute?", &min,
   346  		"second?", &sec,
   347  		"nanosecond?", &nsec,
   348  		"location?", &loc,
   349  	); err != nil {
   350  		return nil, err
   351  	}
   352  	if len(args) > 0 {
   353  		return nil, fmt.Errorf("time: unexpected positional arguments")
   354  	}
   355  	location, err := time.LoadLocation(loc)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  	return Time(time.Date(year, time.Month(month), day, hour, min, sec, nsec, location)), nil
   360  }
   361  
   362  // String returns the time formatted using the format string
   363  //	"2006-01-02 15:04:05.999999999 -0700 MST".
   364  func (t Time) String() string { return time.Time(t).String() }
   365  
   366  // Type returns "time.time".
   367  func (t Time) Type() string { return "time.time" }
   368  
   369  // Freeze renders time immutable. required by starlark.Value interface
   370  // because Time is already immutable this is a no-op.
   371  func (t Time) Freeze() {}
   372  
   373  // Hash returns a function of x such that Equals(x, y) => Hash(x) == Hash(y)
   374  // required by starlark.Value interface.
   375  func (t Time) Hash() (uint32, error) {
   376  	return uint32(time.Time(t).UnixNano()) ^ uint32(int64(time.Time(t).UnixNano())>>32), nil
   377  }
   378  
   379  // Truth returns the truth value of an object required by starlark.Value
   380  // interface.
   381  func (t Time) Truth() starlark.Bool { return !starlark.Bool(time.Time(t).IsZero()) }
   382  
   383  // Attr gets a value for a string attribute, implementing dot expression support
   384  // in starklark. required by starlark.HasAttrs interface.
   385  func (t Time) Attr(name string) (starlark.Value, error) {
   386  	switch name {
   387  	case "year":
   388  		return starlark.MakeInt(time.Time(t).Year()), nil
   389  	case "month":
   390  		return starlark.MakeInt(int(time.Time(t).Month())), nil
   391  	case "day":
   392  		return starlark.MakeInt(time.Time(t).Day()), nil
   393  	case "hour":
   394  		return starlark.MakeInt(time.Time(t).Hour()), nil
   395  	case "minute":
   396  		return starlark.MakeInt(time.Time(t).Minute()), nil
   397  	case "second":
   398  		return starlark.MakeInt(time.Time(t).Second()), nil
   399  	case "nanosecond":
   400  		return starlark.MakeInt(time.Time(t).Nanosecond()), nil
   401  	case "unix":
   402  		return starlark.MakeInt64(time.Time(t).Unix()), nil
   403  	case "unix_nano":
   404  		return starlark.MakeInt64(time.Time(t).UnixNano()), nil
   405  	}
   406  	return builtinAttr(t, name, timeMethods)
   407  }
   408  
   409  // AttrNames lists available dot expression strings for time. required by
   410  // starlark.HasAttrs interface.
   411  func (t Time) AttrNames() []string {
   412  	return append(builtinAttrNames(timeMethods),
   413  		"year",
   414  		"month",
   415  		"day",
   416  		"hour",
   417  		"minute",
   418  		"second",
   419  		"nanosecond",
   420  		"unix",
   421  		"unix_nano",
   422  	)
   423  }
   424  
   425  // Cmp implements comparison of two Time values. Required by
   426  // starlark.TotallyOrdered interface.
   427  func (t Time) Cmp(yV starlark.Value, depth int) (int, error) {
   428  	x := time.Time(t)
   429  	y := time.Time(yV.(Time))
   430  	if x.Before(y) {
   431  		return -1, nil
   432  	} else if x.After(y) {
   433  		return 1, nil
   434  	}
   435  	return 0, nil
   436  }
   437  
   438  // Binary implements binary operators, which satisfies the starlark.HasBinary
   439  // interface
   440  //    time + duration = time
   441  //    time - duration = time
   442  //    time - time = duration
   443  func (t Time) Binary(op syntax.Token, y starlark.Value, side starlark.Side) (starlark.Value, error) {
   444  	x := time.Time(t)
   445  
   446  	switch op {
   447  	case syntax.PLUS:
   448  		switch y := y.(type) {
   449  		case Duration:
   450  			return Time(x.Add(time.Duration(y))), nil
   451  		}
   452  	case syntax.MINUS:
   453  		switch y := y.(type) {
   454  		case Duration:
   455  			return Time(x.Add(time.Duration(-y))), nil
   456  		case Time:
   457  			// time - time = duration
   458  			return Duration(x.Sub(time.Time(y))), nil
   459  		}
   460  	}
   461  
   462  	return nil, nil
   463  }
   464  
   465  var timeMethods = map[string]builtinMethod{
   466  	"in_location": timeIn,
   467  	"format":      timeFormat,
   468  }
   469  
   470  func timeFormat(fnname string, recV starlark.Value, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   471  	var x string
   472  	if err := starlark.UnpackPositionalArgs("format", args, kwargs, 1, &x); err != nil {
   473  		return nil, err
   474  	}
   475  
   476  	recv := time.Time(recV.(Time))
   477  	return starlark.String(recv.Format(x)), nil
   478  }
   479  
   480  func timeIn(fnname string, recV starlark.Value, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   481  	var x string
   482  	if err := starlark.UnpackPositionalArgs("in_location", args, kwargs, 1, &x); err != nil {
   483  		return nil, err
   484  	}
   485  	loc, err := time.LoadLocation(x)
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  
   490  	recv := time.Time(recV.(Time))
   491  	return Time(recv.In(loc)), nil
   492  }
   493  
   494  type builtinMethod func(fnname string, recv starlark.Value, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error)
   495  
   496  func builtinAttr(recv starlark.Value, name string, methods map[string]builtinMethod) (starlark.Value, error) {
   497  	method := methods[name]
   498  	if method == nil {
   499  		return nil, nil // no such method
   500  	}
   501  
   502  	// Allocate a closure over 'method'.
   503  	impl := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   504  		return method(b.Name(), b.Receiver(), args, kwargs)
   505  	}
   506  	return starlark.NewBuiltin(name, impl).BindReceiver(recv), nil
   507  }
   508  
   509  func builtinAttrNames(methods map[string]builtinMethod) []string {
   510  	names := make([]string, 0, len(methods))
   511  	for name := range methods {
   512  		names = append(names, name)
   513  	}
   514  	sort.Strings(names)
   515  	return names
   516  }