Repository: d5/tengo
Pull request #397 (2022-11-01 08:06:54)

Better support for TZ locations in the times module
I've added a `location` parameter to times.date to allow for creating a timestamp in a specific location. The call now works the same as the equivalent Go function. This parameter is optional so as not to break existing code.

Along with this I've also added `times.in_location(t time, l string)`, which works similar to [func (t Time) In(loc *Location) Time](https://pkg.go.dev/time#Time.In)


Patch:
diff --git a/docs/objects.md b/docs/objects.md
index 4b15060..99b4eb6 100644
--- a/docs/objects.md
+++ b/docs/objects.md
@@ -297,9 +297,11 @@ to add `StringArray` to the script:
 ```golang
 // script that uses 'my_list'
 s := tengo.NewScript([]byte(`
-    print(my_list + "three")
+    fmt := import("fmt")
+    fmt.println(my_list, ", three")
 `))
 
+s.SetImports(stdlib.GetModuleMap("fmt"))
 myList := &StringArray{Value: []string{"one", "two"}}
 s.Add("my_list", myList)  // add StringArray value 'my_list'
 s.Run()                   // prints "one, two, three"
diff --git a/docs/stdlib-math.md b/docs/stdlib-math.md
index d68eb77..ce8d2cd 100644
--- a/docs/stdlib-math.md
+++ b/docs/stdlib-math.md
@@ -18,6 +18,28 @@ math := import("math")
 - `ln10`
 - `ln10E`
 
+Mathematical constants.
+
+- `maxFloat32`
+- `smallestNonzeroFloat32`
+- `maxFloat64`
+- `smallestNonzeroFloat64`
+
+Floating-point limit values. Max is the largest finite value representable by the type. SmallestNonzero is the smallest positive, non-zero value representable by the type.
+
+- `maxInt`
+- `minInt`
+- `maxInt8`
+- `minInt8`
+- `maxInt16`
+- `minInt16`
+- `maxInt32`
+- `minInt32`
+- `maxInt64`
+- `minInt64`
+
+Integer limit values.
+
 ## Functions
 
 - `abs(x float) => float`: returns the absolute value of x.
diff --git a/docs/stdlib-times.md b/docs/stdlib-times.md
index ce4b997..ba58ab7 100644
--- a/docs/stdlib-times.md
+++ b/docs/stdlib-times.md
@@ -62,9 +62,10 @@ times := import("times")
   duration.
 - `month_string(month int) => string`:  returns the English name of the month
   ("January", "February", ...).
-- `date(year int, month int, day int, hour int, min int, sec int, nsec int) => time`:
-  returns the Time corresponding to "yyyy-mm-dd hh:mm:ss + nsec nanoseconds".
-  Current location is used.
+- `date(year int, month int, day int, hour int, min int, sec int, nsec int, loc string) => time`:
+  returns the Time corresponding to "yyyy-mm-dd hh:mm:ss + nsec nanoseconds" in
+  the appropriate zone for that Time in the given (optional) location.
+  The Local time zone will be used if executed without specifying a location.
 - `now() => time`: returns the current local time.
 - `parse(format string, s string) => time`: parses a formatted string and
   returns the time value it represents. The layout defines the format by
@@ -116,5 +117,8 @@ times := import("times")
   string "2006-01-02 15:04:05.999999999 -0700 MST".
 - `is_zero(t time) => bool`: reports whether t represents the zero time
   instant, January 1, year 1, 00:00:00 UTC.
+- `in_location(t time, l string) => time`:  returns a copy of t representing
+  the same time instant, but with the copy's location information set to l for 
+  display purposes.
 - `to_local(t time) => time`: returns t with the location set to local time.
 - `to_utc(t time) => time`: returns t with the location set to UTC.
diff --git a/script.go b/script.go
index 4cca4d0..16f2944 100644
--- a/script.go
+++ b/script.go
@@ -247,8 +247,8 @@ func (c *Compiled) RunContext(ctx context.Context) (err error) {
 // Clone creates a new copy of Compiled. Cloned copies are safe for concurrent
 // use by multiple goroutines.
 func (c *Compiled) Clone() *Compiled {
-	c.lock.Lock()
-	defer c.lock.Unlock()
+	c.lock.RLock()
+	defer c.lock.RUnlock()
 
 	clone := &Compiled{
 		globalIndexes: c.globalIndexes,
diff --git a/stdlib/json/decode.go b/stdlib/json/decode.go
index 6d468ef..37432d4 100644
--- a/stdlib/json/decode.go
+++ b/stdlib/json/decode.go
@@ -62,9 +62,12 @@ func (d *decodeState) scanNext() {
 
 // scanWhile processes bytes in d.data[d.off:] until it
 // receives a scan code not equal to op.
-func (d *decodeState) scanWhile(op int) {
+func (d *decodeState) scanWhile(op int) (isFloat bool) {
 	s, data, i := &d.scan, d.data, d.off
 	for i < len(data) {
+		if data[i] == '.' {
+			isFloat = true
+		}
 		newOp := s.step(s, data[i])
 		i++
 		if newOp != op {
@@ -76,6 +79,7 @@ func (d *decodeState) scanWhile(op int) {
 
 	d.off = len(data) + 1 // mark processed EOF with len+1
 	d.opcode = d.scan.eof()
+	return
 }
 
 func (d *decodeState) value() (tengo.Object, error) {
@@ -185,7 +189,7 @@ func (d *decodeState) object() (tengo.Object, error) {
 func (d *decodeState) literal() (tengo.Object, error) {
 	// All bytes inside literal return scanContinue op code.
 	start := d.readIndex()
-	d.scanWhile(scanContinue)
+	isFloat := d.scanWhile(scanContinue)
 
 	item := d.data[start:d.readIndex()]
 
@@ -210,8 +214,12 @@ func (d *decodeState) literal() (tengo.Object, error) {
 		if c != '-' && (c < '0' || c > '9') {
 			panic(phasePanicMsg)
 		}
-		n, _ := strconv.ParseFloat(string(item), 10)
-		return &tengo.Float{Value: n}, nil
+		if isFloat {
+			n, _ := strconv.ParseFloat(string(item), 10)
+			return &tengo.Float{Value: n}, nil
+		}
+		n, _ := strconv.ParseInt(string(item), 10, 64)
+		return &tengo.Int{Value: n}, nil
 	}
 }
 
diff --git a/stdlib/math.go b/stdlib/math.go
index 633ea09..1bd9ba6 100644
--- a/stdlib/math.go
+++ b/stdlib/math.go
@@ -7,17 +7,31 @@ import (
 )
 
 var mathModule = map[string]tengo.Object{
-	"e":       &tengo.Float{Value: math.E},
-	"pi":      &tengo.Float{Value: math.Pi},
-	"phi":     &tengo.Float{Value: math.Phi},
-	"sqrt2":   &tengo.Float{Value: math.Sqrt2},
-	"sqrtE":   &tengo.Float{Value: math.SqrtE},
-	"sqrtPi":  &tengo.Float{Value: math.SqrtPi},
-	"sqrtPhi": &tengo.Float{Value: math.SqrtPhi},
-	"ln2":     &tengo.Float{Value: math.Ln2},
-	"log2E":   &tengo.Float{Value: math.Log2E},
-	"ln10":    &tengo.Float{Value: math.Ln10},
-	"log10E":  &tengo.Float{Value: math.Log10E},
+	"e":                      &tengo.Float{Value: math.E},
+	"pi":                     &tengo.Float{Value: math.Pi},
+	"phi":                    &tengo.Float{Value: math.Phi},
+	"sqrt2":                  &tengo.Float{Value: math.Sqrt2},
+	"sqrtE":                  &tengo.Float{Value: math.SqrtE},
+	"sqrtPi":                 &tengo.Float{Value: math.SqrtPi},
+	"sqrtPhi":                &tengo.Float{Value: math.SqrtPhi},
+	"ln2":                    &tengo.Float{Value: math.Ln2},
+	"log2E":                  &tengo.Float{Value: math.Log2E},
+	"ln10":                   &tengo.Float{Value: math.Ln10},
+	"log10E":                 &tengo.Float{Value: math.Log10E},
+	"maxFloat32":             &tengo.Float{Value: math.MaxFloat32},
+	"smallestNonzeroFloat32": &tengo.Float{Value: math.SmallestNonzeroFloat32},
+	"maxFloat64":             &tengo.Float{Value: math.MaxFloat64},
+	"smallestNonzeroFloat64": &tengo.Float{Value: math.SmallestNonzeroFloat64},
+	"maxInt":                 &tengo.Int{Value: math.MaxInt},
+	"minInt":                 &tengo.Int{Value: math.MinInt},
+	"maxInt8":                &tengo.Int{Value: math.MaxInt8},
+	"minInt8":                &tengo.Int{Value: math.MinInt8},
+	"maxInt16":               &tengo.Int{Value: math.MaxInt16},
+	"minInt16":               &tengo.Int{Value: math.MinInt16},
+	"maxInt32":               &tengo.Int{Value: math.MaxInt32},
+	"minInt32":               &tengo.Int{Value: math.MinInt32},
+	"maxInt64":               &tengo.Int{Value: math.MaxInt64},
+	"minInt64":               &tengo.Int{Value: math.MinInt64},
 	"abs": &tengo.UserFunction{
 		Name:  "abs",
 		Value: FuncAFRF(math.Abs),
diff --git a/stdlib/times.go b/stdlib/times.go
index 0b6f7bd..dc9a1a5 100644
--- a/stdlib/times.go
+++ b/stdlib/times.go
@@ -180,6 +180,10 @@ var timesModule = map[string]tengo.Object{
 		Name:  "to_utc",
 		Value: timesToUTC,
 	}, // to_utc(time) => time
+	"in_location": &tengo.UserFunction{
+		Name:  "in_location",
+		Value: timesInLocation,
+	}, // in_location(time, location) => time
 }
 
 func timesSleep(args ...tengo.Object) (ret tengo.Object, err error) {
@@ -430,7 +434,7 @@ func timesDate(args ...tengo.Object) (
 	ret tengo.Object,
 	err error,
 ) {
-	if len(args) != 7 {
+	if len(args) < 7 || len(args) > 8 {
 		err = tengo.ErrWrongNumArguments
 		return
 	}
@@ -499,9 +503,29 @@ func timesDate(args ...tengo.Object) (
 		return
 	}
 
+	var loc *time.Location
+	if len(args) == 8 {
+		i8, ok := tengo.ToString(args[7])
+		if !ok {
+			err = tengo.ErrInvalidArgumentType{
+				Name:     "eighth",
+				Expected: "string(compatible)",
+				Found:    args[7].TypeName(),
+			}
+			return
+		}
+		loc, err = time.LoadLocation(i8)
+		if err != nil {
+			ret = wrapError(err)
+			return
+		}
+	} else {
+		loc = time.Now().Location()
+	}
+
 	ret = &tengo.Time{
 		Value: time.Date(i1,
-			time.Month(i2), i3, i4, i5, i6, i7, time.Now().Location()),
+			time.Month(i2), i3, i4, i5, i6, i7, loc),
 	}
 
 	return
@@ -1113,6 +1137,46 @@ func timesTimeLocation(args ...tengo.Object) (
 	return
 }
 
+func timesInLocation(args ...tengo.Object) (
+	ret tengo.Object,
+	err error,
+) {
+	if len(args) != 2 {
+		err = tengo.ErrWrongNumArguments
+		return
+	}
+
+	t1, ok := tengo.ToTime(args[0])
+	if !ok {
+		err = tengo.ErrInvalidArgumentType{
+			Name:     "first",
+			Expected: "time(compatible)",
+			Found:    args[0].TypeName(),
+		}
+		return
+	}
+
+	s2, ok := tengo.ToString(args[1])
+	if !ok {
+		err = tengo.ErrInvalidArgumentType{
+			Name:     "second",
+			Expected: "string(compatible)",
+			Found:    args[1].TypeName(),
+		}
+		return
+	}
+
+	location, err := time.LoadLocation(s2)
+	if err != nil {
+		ret = wrapError(err)
+		return
+	}
+
+	ret = &tengo.Time{Value: t1.In(location)}
+
+	return
+}
+
 func timesTimeString(args ...tengo.Object) (ret tengo.Object, err error) {
 	if len(args) != 1 {
 		err = tengo.ErrWrongNumArguments


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 72d20d8..f2137f6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,7 +12,7 @@ jobs:
     - name: set up Go
       uses: actions/setup-go@v1
       with:
-        go-version: 1.14
+        go-version: 1.17
       id: go
     - name: set up Go module cache
       uses: actions/cache@v1
@@ -29,6 +29,6 @@ jobs:
     - name: check out code
       uses: actions/checkout@v2
     - name: install golint
-      run: go get -u golang.org/x/lint/golint
+      run: go install golang.org/x/lint/golint@latest
     - name: run tests
       run: make test
diff --git a/stdlib/json/json_test.go b/stdlib/json/json_test.go
index 5abfdec..b12bd64 100644
--- a/stdlib/json/json_test.go
+++ b/stdlib/json/json_test.go
@@ -57,6 +57,8 @@ func TestJSON(t *testing.T) {
 		"arr": ARR{1, 2, 3, "four"}})
 	testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee",
 		"arr": ARR{1, 2, 3, MAP{"a": false, "b": 109.4}}})
+
+	testJSONEncodeDecode(t, MAP{"id1": 7075984636689534001, "id2": 7075984636689534002})
 }
 
 func TestDecode(t *testing.T) {
diff --git a/stdlib/json_test.go b/stdlib/json_test.go
index adf7037..ac8dcf1 100644
--- a/stdlib/json_test.go
+++ b/stdlib/json_test.go
@@ -48,13 +48,13 @@ func TestJSON(t *testing.T) {
 		expect([]byte("[[\"bar\",1],[\"bar\",1]]"))
 
 	module(t, "json").call("decode", `5`).
-		expect(5.0)
+		expect(5)
 	module(t, "json").call("decode", `"foo"`).
 		expect("foo")
 	module(t, "json").call("decode", `[1,2,3,"bar"]`).
-		expect(ARR{1.0, 2.0, 3.0, "bar"})
+		expect(ARR{1, 2, 3, "bar"})
 	module(t, "json").call("decode", `{"foo":5}`).
-		expect(MAP{"foo": 5.0})
+		expect(MAP{"foo": 5})
 	module(t, "json").call("decode", `{"foo":2.5}`).
 		expect(MAP{"foo": 2.5})
 	module(t, "json").call("decode", `{"foo":true}`).
@@ -62,7 +62,7 @@ func TestJSON(t *testing.T) {
 	module(t, "json").call("decode", `{"foo":"bar"}`).
 		expect(MAP{"foo": "bar"})
 	module(t, "json").call("decode", `{"foo":[1,2,3,"bar"]}`).
-		expect(MAP{"foo": ARR{1.0, 2.0, 3.0, "bar"}})
+		expect(MAP{"foo": ARR{1, 2, 3, "bar"}})
 
 	module(t, "json").
 		call("indent", []byte("{\"foo\":[\"bar\",1,1.8,56,true]}"), "", "  ").
diff --git a/stdlib/times_test.go b/stdlib/times_test.go
index 6977ba4..afe34bd 100644
--- a/stdlib/times_test.go
+++ b/stdlib/times_test.go
@@ -11,6 +11,8 @@ import (
 func TestTimes(t *testing.T) {
 	time1 := time.Date(1982, 9, 28, 19, 21, 44, 999, time.Now().Location())
 	time2 := time.Now()
+	location, _ := time.LoadLocation("Pacific/Auckland")
+	time3 := time.Date(1982, 9, 28, 19, 21, 44, 999, location)
 
 	module(t, "times").call("sleep", 1).expect(tengo.UndefinedValue)
 
@@ -35,6 +37,9 @@ func TestTimes(t *testing.T) {
 
 	module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999).
 		expect(time1)
+	module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999, "Pacific/Auckland").
+		expect(time3)
+
 	nowD := time.Until(module(t, "times").call("now").
 		o.(*tengo.Time).Value).Nanoseconds()
 	require.True(t, 0 > nowD && nowD > -100000000) // within 100ms
@@ -80,4 +85,5 @@ func TestTimes(t *testing.T) {
 	module(t, "times").call("time_location", time1).
 		expect(time1.Location().String())
 	module(t, "times").call("time_string", time1).expect(time1.String())
+	module(t, "times").call("in_location", time1, location.String()).expect(time1.In(location))
 }