diff --git a/builtin_date.go b/builtin_date.go index 84a80ac0..d676ddd9 100644 --- a/builtin_date.go +++ b/builtin_date.go @@ -198,11 +198,25 @@ func (r *Runtime) dateproto_toTimeString(call FunctionCall) Value { panic(r.NewTypeError("Method Date.prototype.toTimeString is called on incompatible receiver")) } +func (d *dateObject) withTimeZoneOpts(r *Runtime, call FunctionCall) time.Time { + t := d.time() + if arg1 := call.Argument(1); arg1 != _undefined { + opts := r.toObject(arg1) + if tz := opts.Get("timeZone"); tz != nil && tz != _undefined { + if loc, err := time.LoadLocation(tz.String()); err == nil { + return t.In(loc) + } + } + } + return t +} + func (r *Runtime) dateproto_toLocaleString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { if d.isSet() { - return asciiString(d.time().Format(datetimeLayout_en_GB)) + t := d.withTimeZoneOpts(r, call) + return asciiString(t.Format(datetimeLayout_en_GB)) } else { return stringInvalidDate } @@ -214,7 +228,8 @@ func (r *Runtime) dateproto_toLocaleDateString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { if d.isSet() { - return asciiString(d.time().Format(dateLayout_en_GB)) + t := d.withTimeZoneOpts(r, call) + return asciiString(t.Format(dateLayout_en_GB)) } else { return stringInvalidDate } @@ -226,7 +241,8 @@ func (r *Runtime) dateproto_toLocaleTimeString(call FunctionCall) Value { obj := r.toObject(call.This) if d, ok := obj.self.(*dateObject); ok { if d.isSet() { - return asciiString(d.time().Format(timeLayout_en_GB)) + t := d.withTimeZoneOpts(r, call) + return asciiString(t.Format(timeLayout_en_GB)) } else { return stringInvalidDate } @@ -989,9 +1005,9 @@ func createDateProtoTemplate() *objectTemplate { t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toString, "toString", 0) }) t.putStr("toDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toDateString, "toDateString", 0) }) t.putStr("toTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toTimeString, "toTimeString", 0) }) - t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleString, "toLocaleString", 0) }) - t.putStr("toLocaleDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleDateString, "toLocaleDateString", 0) }) - t.putStr("toLocaleTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleTimeString, "toLocaleTimeString", 0) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleString, "toLocaleString", 2) }) + t.putStr("toLocaleDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleDateString, "toLocaleDateString", 2) }) + t.putStr("toLocaleTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleTimeString, "toLocaleTimeString", 2) }) t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.dateproto_valueOf, "valueOf", 0) }) t.putStr("getTime", func(r *Runtime) Value { return r.methodProp(r.dateproto_getTime, "getTime", 0) }) t.putStr("getFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_getFullYear, "getFullYear", 0) }) diff --git a/date_test.go b/date_test.go index a3ef7bab..ed000dfc 100644 --- a/date_test.go +++ b/date_test.go @@ -130,6 +130,63 @@ func TestTimezoneOffset(t *testing.T) { testScript(SCRIPT, intToValue(-60), t) } +func TestDateLocaleTime(t *testing.T) { + const SCRIPT = ` + var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); + d.toLocaleString("en-GB"); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScript(SCRIPT, asciiString("12/20/2012, 03:00:00"), t) +} + +func TestDateLocaleTimeZones(t *testing.T) { + const SCRIPT = ` + var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); + d.toLocaleString("en-GB", { timeZone: "Europe/Berlin" }); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScript(SCRIPT, asciiString("12/20/2012, 04:00:00"), t) +} + +func TestDateUnsupportedOptions(t *testing.T) { + const SCRIPT = ` + var d = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); + d.toLocaleString("en-GB", { foo: "bar" }); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScript(SCRIPT, asciiString("12/20/2012, 03:00:00"), t) +} + func TestDateValueOf(t *testing.T) { const SCRIPT = ` var d9 = new Date(1.23e15); diff --git a/object.go b/object.go index 79bd67df..2abcda32 100644 --- a/object.go +++ b/object.go @@ -967,6 +967,9 @@ func (o *baseObject) export(ctx *objectExportCtx) interface{} { itemNameStr := itemName.String() v := o.val.self.getStr(itemName.string(), nil) if v != nil { + if IsUndefined(v) && o.val.runtime.exportOptions.dropUndefinedKeys { + continue + } m[itemNameStr] = exportValue(v, ctx) } else { m[itemNameStr] = nil @@ -1822,3 +1825,16 @@ func (i *privateId) String() string { func (i *privateId) string() unistring.String { return privateIdString(i.name) } + +type exportOptions struct { + dropUndefinedKeys bool +} + +type ExportOptions func(*exportOptions) + +// WithDropUndefinedKeys configures object exports to drop undefined keys instead of converting to nil. +func WithDropUndefinedKeys() ExportOptions { + return func(opts *exportOptions) { + opts.dropUndefinedKeys = true + } +} diff --git a/object_test.go b/object_test.go index 45b95bc5..5a46ad90 100644 --- a/object_test.go +++ b/object_test.go @@ -314,6 +314,26 @@ func TestExportToSliceNonIterable(t *testing.T) { } } +func TestExportDropUndefinedKeys(t *testing.T) { + vm := New() + vm.SetExportOptions(WithDropUndefinedKeys()) + + o := vm.NewObject() + o.Set("foo", vm.ToValue("bar")) + o.Set("baz", _undefined) + var a map[string]any + err := vm.ExportTo(o, &a) + if err != nil { + t.Fatal(err) + } + if len(a) != 1 { + t.Fatalf("a: %v", a) + } + if a["foo"] != "bar" { + t.Fatalf("Unexpected a[foo]: %v", a["foo"]) + } +} + func ExampleRuntime_ExportTo_iterableToSlice() { vm := New() v, err := vm.RunString(` diff --git a/runtime.go b/runtime.go index 9cccb469..e4fb94a3 100644 --- a/runtime.go +++ b/runtime.go @@ -193,6 +193,8 @@ type Runtime struct { fieldNameMapper FieldNameMapper + exportOptions exportOptions + vm *vm hash *maphash.Hash idSeq uint64 @@ -2441,6 +2443,13 @@ func (r *Runtime) SetParserOptions(opts ...parser.Option) { r.parserOptions = opts } +// SetExportOptions sets the export options for this Runtime. +func (r *Runtime) SetExportOptions(opts ...ExportOptions) { + for _, o := range opts { + o(&r.exportOptions) + } +} + // SetMaxCallStackSize sets the maximum function call depth. When exceeded, a *StackOverflowError is thrown and // returned by RunProgram or by a Callable call. This is useful to prevent memory exhaustion caused by an // infinite recursion. The default value is math.MaxInt32.