skylarktime: a sketch of a time package
Work in progress
Fixes #17
Change-Id: Ia4bd6b28b24168d996dc5f159a05d2b153e1fede
diff --git a/skylarktime/testdata/time.sky b/skylarktime/testdata/time.sky
new file mode 100644
index 0000000..05faf16
--- /dev/null
+++ b/skylarktime/testdata/time.sky
@@ -0,0 +1,31 @@
+
+load("assert.sky", "assert")
+load("time.sky", "time")
+
+assert.eq(type(time.now()), "time.time")
+
+year = 365.25 * 24 * time.hour
+# assert.eq((time.now() - time.zero) / year, 292.271) # years since zero, approx
+
+# The clock is set to Sat Mar 7 11:06:39 PST 2015.
+assert.eq(time.now().year, 2015)
+assert.eq(time.now().month, "March")
+assert.eq(time.now().day, 7)
+assert.eq(time.now().hour, 11)
+assert.eq(time.now(), time.time("Sat Mar 7 11:06:39 PST 2015")) # TODO: decide on formats
+
+assert.eq(str(300 * time.millisecond), "300ms")
+assert.eq(str(135 * time.second + 2 * time.minute), "4m15s")
+assert.eq(str(time.second * 135 + time.minute * 2), "4m15s")
+assert.eq(str(time.hour * 2), "2h0m0s")
+assert.eq(str(time.hour * 2.0), "2h0m0s")
+assert.eq((time.hour * 1.25 - 15 * time.minute) / time.second, 3600)
+assert.eq(str(time.hour / 2), "30m0s")
+assert.eq(str(time.hour / 2.0), "30m0s")
+assert.fails(lambda: time.hour // 2, "unknown binary op: time.duration // int")
+assert.fails(lambda: time.hour // 2.0, "unknown binary op: time.duration // float")
+
+assert.eq(time.duration("300.0ms"), 300 * time.millisecond)
+assert.eq(time.duration("300.0ms"), 300.0 * time.millisecond)
+assert.eq(type(time.duration("1s")), "time.duration")
+assert.eq(str(time.duration("1.00s")), "1s")
diff --git a/skylarktime/time.go b/skylarktime/time.go
new file mode 100644
index 0000000..abf0d1c
--- /dev/null
+++ b/skylarktime/time.go
@@ -0,0 +1,383 @@
+// 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
+}
diff --git a/skylarktime/time.sky b/skylarktime/time.sky
new file mode 100644
index 0000000..6c10c83
--- /dev/null
+++ b/skylarktime/time.sky
@@ -0,0 +1,5 @@
+# the time module
+
+time = struct(
+ now = now,
+)
diff --git a/skylarktime/time_test.go b/skylarktime/time_test.go
new file mode 100644
index 0000000..6ca382c
--- /dev/null
+++ b/skylarktime/time_test.go
@@ -0,0 +1,81 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package skylarktime_test
+
+import (
+ "log"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/google/skylark"
+ "github.com/google/skylark/internal/chunkedfile"
+ "github.com/google/skylark/resolve"
+ "github.com/google/skylark/skylarktest"
+ "github.com/google/skylark/skylarktime"
+)
+
+func init() {
+ // The tests make extensive use of these not-yet-standard features.
+ resolve.AllowLambda = true
+ resolve.AllowNestedDef = true
+ resolve.AllowFloat = true
+ resolve.AllowSet = true
+
+ // Fake the clock for test determinism.
+ now, err := time.Parse(time.UnixDate, "Sat Mar 7 11:06:39 PST 2015")
+ if err != nil {
+ log.Fatal(err)
+ }
+ skylarktime.Now = func() time.Time { return now }
+}
+
+func TestExecFile(t *testing.T) {
+ testdata := skylarktest.DataFile("skylark/skylarktime", ".")
+ thread := &skylark.Thread{Load: load}
+ skylarktest.SetReporter(thread, t)
+ for _, file := range []string{
+ "testdata/time.sky",
+ } {
+ filename := filepath.Join(testdata, file)
+ for _, chunk := range chunkedfile.Read(filename, t) {
+ _, err := skylark.ExecFile(thread, filename, chunk.Source, nil)
+ switch err := err.(type) {
+ case *skylark.EvalError:
+ found := false
+ for _, fr := range err.Stack() {
+ posn := fr.Position()
+ if posn.Filename() == filename {
+ chunk.GotError(int(posn.Line), err.Error())
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error(err.Backtrace())
+ }
+ case nil:
+ // success
+ default:
+ t.Error(err)
+ }
+ chunk.Done()
+ }
+ }
+}
+
+// load implements the 'load' operation as used in the evaluator tests.
+func load(thread *skylark.Thread, module string) (skylark.StringDict, error) {
+ if module == "assert.sky" {
+ return skylarktest.LoadAssertModule()
+ }
+ if module == "time.sky" {
+ return skylarktime.LoadTimeModule()
+ }
+
+ // TODO(adonovan): test load() using this execution path.
+ filename := filepath.Join(filepath.Dir(thread.Caller().Position().Filename()), module)
+ return skylark.ExecFile(thread, filename, nil, nil)
+}