blob: abf0d1cd0c62f8893deda1874d47cc335ed1ca5c [file] [log] [blame]
// Package time provides time.Time wrappers for the skylark embedded language.
// See https://github.com/google/skylark/issues/17.
package skylarktime
import (
"errors"
"sync"
"time"
"github.com/google/skylark"
"github.com/google/skylark/skylarkstruct"
"github.com/google/skylark/syntax"
)
/*
TODO: provide Skylark module documentation.
module time
functions
duration(string) duration # parse a duration
location(string) location # parse a location
time(string, format=..., location=...) time # parse a time
now() time # implementations would be able to make this a constant
zero time # a constant
type duration
operators
duration - time = duration
duration + time = time
duration == duration
duration < duration
fields
hour int
minute int
nanosecond int
second int
type time
operators
time == time
time < time
time + duration = time
time - duration = time
time - time = duration
fields
year int
month int
day int
hour int
minute int
second int
nanosecond int
TODO:
- unix time_t conversions
- timezone stuff
- strftime formatting
- constructor from 6 components + location
*/
// TODO: the Skylark module system is poor.
var (
once sync.Once
timeModule skylark.StringDict
timeErr error
)
// LoadTimeModule loads the time module.
// It is concurrency-safe and idempotent.
func LoadTimeModule() (skylark.StringDict, error) {
once.Do(func() {
timeModule = skylark.StringDict{
"time": skylarkstruct.FromStringDict(
skylarkstruct.Default,
skylark.StringDict{
"now": skylark.NewBuiltin("error", now),
"zero": Time(time.Time{}),
"duration": skylark.NewBuiltin("duration", parseDuration),
"location": skylark.NewBuiltin("location", parseLocation),
"time": skylark.NewBuiltin("time", parseTime),
"nanosecond": Duration(time.Nanosecond),
"microsecond": Duration(time.Microsecond),
"millisecond": Duration(time.Millisecond),
"second": Duration(time.Second),
"minute": Duration(time.Minute),
"hour": Duration(time.Hour),
},
),
}
})
return timeModule, nil
}
// Now is the function called by Skylark's time.now function.
// By default it is Go's time function, but a client
// may install an alternative function that, for example,
// returns a constant to ensure deterministic execution.
//
// TODO(adonovan): make this a thread-local variable.
var Now = time.Now
// now returns the current time.
func now(_ *skylark.Thread, _ *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
if len(args) > 0 || len(kwargs) != 0 {
return nil, errors.New("now: unexpected arguments")
}
return Time(Now()), nil
}
// Delta returns a duration created from kwargs. Expected "hours", "minutes",
// "seconds", "milliseconds", or "nanoseconds", and assigned an int.
func Delta(_ *skylark.Thread, _ *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
if len(args) != 0 {
return nil, errors.New("too many args")
}
var d time.Duration
for _, t := range kwargs {
if len(t) != 2 {
panic("invalid kwarg")
}
s, ok := t[0].(skylark.String)
if !ok {
panic("invalid kwarg name")
}
i, ok := t[1].(skylark.Int)
if !ok {
return nil, errors.New("invalid value for timedelta arg, must be int")
}
v, ok := i.Int64()
if !ok {
return nil, errors.New("numeric value overflows int64")
}
switch s {
case "hours":
d += time.Hour * time.Duration(v)
case "minutes":
d += time.Minute * time.Duration(v)
case "seconds":
d += time.Second * time.Duration(v)
case "milliseconds":
d += time.Millisecond * time.Duration(v)
case "nanoseconds":
d += time.Nanosecond * time.Duration(v)
default:
return nil, errors.New("invalid duration unit: " + string(s))
}
}
return Duration(d), nil
}
// Time is the type of a Skylark time.Time.
type Time time.Time
var (
_ skylark.Value = Time{}
_ skylark.Comparable = Time{}
_ skylark.HasAttrs = Time{}
_ skylark.HasBinary = Time{}
)
func (t Time) String() string { return time.Time(t).String() }
func (t Time) Type() string { return "time.time" }
func (t Time) Freeze() {} // immutable
func (t Time) Truth() skylark.Bool { return skylark.Bool(!time.Time(t).IsZero()) }
func (t Time) Hash() (uint32, error) { return uint32(time.Time(t).Unix()), nil } // TODO not robust
func (t Time) CompareSameType(op syntax.Token, y_ skylark.Value, depth int) (bool, error) {
x := time.Time(t)
y := time.Time(y_.(Time))
switch op {
case syntax.EQL:
return x.Equal(y), nil
case syntax.NEQ:
return !x.Equal(y), nil
case syntax.LE:
return !y.Before(x), nil
case syntax.LT:
return x.Before(y), nil
case syntax.GE:
return !y.After(x), nil
case syntax.GT:
return x.After(y), nil
}
panic(op)
}
func (t Time) AttrNames() []string {
return []string{"year", "month", "day", "hour", "minute", "second", "nanosecond"}
}
func (t Time) Attr(name string) (skylark.Value, error) {
x := time.Time(t)
switch name {
case "year":
return skylark.MakeInt(x.Year()), nil
case "month":
return skylark.String(x.Month().String()), nil
case "day":
return skylark.MakeInt(x.Day()), nil
case "hour":
return skylark.MakeInt(x.Hour()), nil
case "minute":
return skylark.MakeInt(x.Minute()), nil
case "second":
return skylark.MakeInt(x.Second()), nil
case "nanosecond":
return skylark.MakeInt(x.Nanosecond()), nil
}
return nil, nil // no such attribute
}
func parseTime(_ *skylark.Thread, b *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
var s string
if err := skylark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &s); err != nil {
return nil, err
}
t, err := time.Parse(time.UnixDate, s) // TODO
if err != nil {
return nil, err
}
return Time(t), nil
}
func (t Time) Binary(op syntax.Token, y skylark.Value, side skylark.Side) (skylark.Value, error) {
if side == skylark.Right {
return nil, nil
}
if time2, ok := y.(Time); ok {
return t.binaryTime(op, time2)
}
if d, ok := y.(Duration); ok {
return t.binaryDuration(op, d)
}
return nil, nil
}
func (t Time) binaryTime(op syntax.Token, y Time) (skylark.Value, error) {
switch op {
case syntax.MINUS:
return Duration(time.Time(t).Sub(time.Time(y))), nil
}
return nil, nil
}
func (t Time) binaryDuration(op syntax.Token, y Duration) (skylark.Value, error) {
switch op {
case syntax.MINUS:
return Time(time.Time(t).Add(-time.Duration(y))), nil
case syntax.PLUS:
return Time(time.Time(t).Add(time.Duration(y))), nil
}
return nil, nil
}
type Duration time.Duration
var (
_ skylark.Value = Duration(0)
_ skylark.Comparable = Duration(0)
_ skylark.HasBinary = Duration(0)
)
func (d Duration) String() string { return time.Duration(d).String() }
func (d Duration) Type() string { return "time.duration" }
func (d Duration) Freeze() {} // immutable
func (d Duration) Truth() skylark.Bool { return d != 0 }
func (d Duration) Hash() (uint32, error) { return skylark.MakeInt64(int64(d)).Hash() }
func (x Duration) CompareSameType(op syntax.Token, y_ skylark.Value, depth int) (bool, error) {
y := y_.(Duration)
switch op {
case syntax.EQL:
return x == y, nil
case syntax.NEQ:
return x != y, nil
case syntax.LE:
return x <= y, nil
case syntax.LT:
return x < y, nil
case syntax.GE:
return x >= y, nil
case syntax.GT:
return x > y, nil
}
panic(op)
}
// duration + duration = duration
// duration - duration = duration
// duration / duration = float
// duration + time = time
// duration * number = duration
// number * duration = duration
// duration / number = duration
//
// time - time = duration
// time + duration = time
// time - duration = time
func (x Duration) Binary(op syntax.Token, y_ skylark.Value, side skylark.Side) (skylark.Value, error) {
if side == skylark.Left {
// duration op y
switch y := y_.(type) {
case Duration:
// duration + duration
// duration - duration
// duration / duration
switch op {
case syntax.PLUS:
return Duration(x + y), nil
case syntax.MINUS:
return Duration(x - y), nil
case syntax.SLASH:
return skylark.Float(x) / skylark.Float(y), nil
}
case Time:
// duration + time = time
if op == syntax.PLUS {
return Time(time.Time(y).Add(time.Duration(x))), nil
}
case skylark.Int, skylark.Float:
// (double x//y not supported)
if op == syntax.STAR || op == syntax.SLASH {
return scaleDuration(x, y, op == syntax.SLASH)
}
}
} else {
// y op duration
// We need handle only cases not covered by side==Left.
switch y := y_.(type) {
case skylark.Int, skylark.Float:
if op == syntax.STAR {
// duration * number = duration
return scaleDuration(x, y, false)
}
}
}
return nil, nil
}
// duration * k = duration
// k * duration = duration
func scaleDuration(x Duration, y skylark.Value, divide bool) (skylark.Value, error) {
switch y := y.(type) {
case skylark.Int:
// TODO: check for overflow
// TODO: handle Uint64, bigint.
if y, ok := y.Int64(); ok {
if divide {
return x / Duration(y), nil
} else {
return x * Duration(y), nil
}
}
case skylark.Float:
if divide {
return Duration(float64(x) / float64(y)), nil
} else {
return Duration(float64(x) * float64(y)), nil
}
}
return nil, nil
}
func parseDuration(_ *skylark.Thread, b *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
var s string
if err := skylark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &s); err != nil {
return nil, err
}
d, err := time.ParseDuration(s)
if err != nil {
return nil, err
}
return Duration(d), nil
}
func parseLocation(_ *skylark.Thread, _ *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
return nil, nil
}