Browse Source

Date.parse() now returns a number. Switched to own date parser for better compatibility (extended years, etc.). Fixes #79

Dmitry Panov 6 years ago
parent
commit
5e65f9206b
5 changed files with 1051 additions and 52 deletions
  1. 34 19
      builtin_date.go
  2. 27 33
      date.go
  3. 860 0
      date_parser.go
  4. 31 0
      date_parser_test.go
  5. 99 0
      date_test.go

+ 34 - 19
builtin_date.go

@@ -1,6 +1,7 @@
 package goja
 
 import (
+	"fmt"
 	"math"
 	"time"
 )
@@ -15,6 +16,10 @@ func timeFromMsec(msec int64) time.Time {
 	return time.Unix(sec, nsec)
 }
 
+func timeToMsec(t time.Time) int64 {
+	return t.Unix()*1000 + int64(t.Nanosecond())/1e6
+}
+
 func makeDate(args []Value, loc *time.Location) (t time.Time, valid bool) {
 	pick := func(index int, default_ int64) (int64, bool) {
 		if index >= len(args) {
@@ -111,7 +116,11 @@ func (r *Runtime) builtin_date(call FunctionCall) Value {
 }
 
 func (r *Runtime) date_parse(call FunctionCall) Value {
-	return r.newDateObject(dateParse(call.Argument(0).String()))
+	t, set := dateParse(call.Argument(0).String())
+	if set {
+		return intToValue(timeToMsec(t))
+	}
+	return _NaN
 }
 
 func (r *Runtime) date_UTC(call FunctionCall) Value {
@@ -119,11 +128,11 @@ func (r *Runtime) date_UTC(call FunctionCall) Value {
 	if !valid {
 		return _NaN
 	}
-	return intToValue(int64(t.UnixNano() / 1e6))
+	return intToValue(timeToMsec(t))
 }
 
 func (r *Runtime) date_now(call FunctionCall) Value {
-	return intToValue(time.Now().UnixNano() / 1e6)
+	return intToValue(timeToMsec(time.Now()))
 }
 
 func (r *Runtime) dateproto_toString(call FunctionCall) Value {
@@ -156,7 +165,13 @@ func (r *Runtime) dateproto_toISOString(call FunctionCall) Value {
 	obj := r.toObject(call.This)
 	if d, ok := obj.self.(*dateObject); ok {
 		if d.isSet {
-			return asciiString(d.time.In(time.UTC).Format(isoDateTimeLayout))
+			utc := d.time.In(time.UTC)
+			year := utc.Year()
+			if year >= -9999 && year <= 9999 {
+				return asciiString(utc.Format(isoDateTimeLayout))
+			}
+			// extended year
+			return asciiString(fmt.Sprintf("%+06d-", year) + utc.Format(isoDateTimeLayout[5:]))
 		} else {
 			panic(r.newError(r.global.RangeError, "Invalid time value"))
 		}
@@ -270,7 +285,7 @@ func (r *Runtime) dateproto_getTime(call FunctionCall) Value {
 	obj := r.toObject(call.This)
 	if d, ok := obj.self.(*dateObject); ok {
 		if d.isSet {
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -518,7 +533,7 @@ func (r *Runtime) dateproto_setMilliseconds(call FunctionCall) Value {
 		if d.isSet {
 			msec := int(call.Argument(0).ToInteger())
 			d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), d.time.Hour(), d.time.Minute(), d.time.Second(), msec*1e6, time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -534,7 +549,7 @@ func (r *Runtime) dateproto_setUTCMilliseconds(call FunctionCall) Value {
 			msec := int(call.Argument(0).ToInteger())
 			t := d.time.In(time.UTC)
 			d.time = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), msec*1e6, time.UTC).In(time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -555,7 +570,7 @@ func (r *Runtime) dateproto_setSeconds(call FunctionCall) Value {
 				nsec = d.time.Nanosecond()
 			}
 			d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), d.time.Hour(), d.time.Minute(), sec, nsec, time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -577,7 +592,7 @@ func (r *Runtime) dateproto_setUTCSeconds(call FunctionCall) Value {
 				nsec = t.Nanosecond()
 			}
 			d.time = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), sec, nsec, time.UTC).In(time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -603,7 +618,7 @@ func (r *Runtime) dateproto_setMinutes(call FunctionCall) Value {
 				nsec = d.time.Nanosecond()
 			}
 			d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), d.time.Hour(), min, sec, nsec, time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -630,7 +645,7 @@ func (r *Runtime) dateproto_setUTCMinutes(call FunctionCall) Value {
 				nsec = t.Nanosecond()
 			}
 			d.time = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), min, sec, nsec, time.UTC).In(time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -661,7 +676,7 @@ func (r *Runtime) dateproto_setHours(call FunctionCall) Value {
 				nsec = d.time.Nanosecond()
 			}
 			d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), hour, min, sec, nsec, time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -693,7 +708,7 @@ func (r *Runtime) dateproto_setUTCHours(call FunctionCall) Value {
 				nsec = t.Nanosecond()
 			}
 			d.time = time.Date(d.time.Year(), d.time.Month(), d.time.Day(), hour, min, sec, nsec, time.UTC).In(time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -707,7 +722,7 @@ func (r *Runtime) dateproto_setDate(call FunctionCall) Value {
 	if d, ok := obj.self.(*dateObject); ok {
 		if d.isSet {
 			d.time = time.Date(d.time.Year(), d.time.Month(), int(call.Argument(0).ToInteger()), d.time.Hour(), d.time.Minute(), d.time.Second(), d.time.Nanosecond(), time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -722,7 +737,7 @@ func (r *Runtime) dateproto_setUTCDate(call FunctionCall) Value {
 		if d.isSet {
 			t := d.time.In(time.UTC)
 			d.time = time.Date(t.Year(), t.Month(), int(call.Argument(0).ToInteger()), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC).In(time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -743,7 +758,7 @@ func (r *Runtime) dateproto_setMonth(call FunctionCall) Value {
 				day = d.time.Day()
 			}
 			d.time = time.Date(d.time.Year(), month, day, d.time.Hour(), d.time.Minute(), d.time.Second(), d.time.Nanosecond(), time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -765,7 +780,7 @@ func (r *Runtime) dateproto_setUTCMonth(call FunctionCall) Value {
 				day = t.Day()
 			}
 			d.time = time.Date(t.Year(), month, day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC).In(time.Local)
-			return intToValue(d.time.UnixNano() / 1e6)
+			return intToValue(timeToMsec(d.time))
 		} else {
 			return _NaN
 		}
@@ -794,7 +809,7 @@ func (r *Runtime) dateproto_setFullYear(call FunctionCall) Value {
 			day = d.time.Day()
 		}
 		d.time = time.Date(year, month, day, d.time.Hour(), d.time.Minute(), d.time.Second(), d.time.Nanosecond(), time.Local)
-		return intToValue(d.time.UnixNano() / 1e6)
+		return intToValue(timeToMsec(d.time))
 	}
 	r.typeErrorResult(true, "Method Date.prototype.setFullYear is called on incompatible receiver")
 	panic("Unreachable")
@@ -821,7 +836,7 @@ func (r *Runtime) dateproto_setUTCFullYear(call FunctionCall) Value {
 			day = t.Day()
 		}
 		d.time = time.Date(year, month, day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC).In(time.Local)
-		return intToValue(d.time.UnixNano() / 1e6)
+		return intToValue(timeToMsec(d.time))
 	}
 	r.typeErrorResult(true, "Method Date.prototype.setUTCFullYear is called on incompatible receiver")
 	panic("Unreachable")

+ 27 - 33
date.go

@@ -1,7 +1,6 @@
 package goja
 
 import (
-	"regexp"
 	"time"
 )
 
@@ -23,9 +22,22 @@ type dateObject struct {
 
 var (
 	dateLayoutList = []string{
+		"2006-01-02T15:04:05.000Z0700",
+		"2006-01-02T15:04:05.000",
+		"2006-01-02T15:04:05Z0700",
+		"2006-01-02T15:04:05",
+		"2006-01-02",
+		time.RFC1123,
+		time.RFC1123Z,
+		dateTimeLayout,
+		time.UnixDate,
+		time.ANSIC,
+		time.RubyDate,
+		"Mon, 02 Jan 2006 15:04:05 GMT-0700 (MST)",
+		"Mon, 02 Jan 2006 15:04:05 -0700 (MST)",
+
 		"2006",
 		"2006-01",
-		"2006-01-02",
 
 		"2006T15:04",
 		"2006-01T15:04",
@@ -33,51 +45,33 @@ var (
 
 		"2006T15:04:05",
 		"2006-01T15:04:05",
-		"2006-01-02T15:04:05",
 
 		"2006T15:04:05.000",
 		"2006-01T15:04:05.000",
-		"2006-01-02T15:04:05.000",
-
-		"2006T15:04-0700",
-		"2006-01T15:04-0700",
-		"2006-01-02T15:04-0700",
 
-		"2006T15:04:05-0700",
-		"2006-01T15:04:05-0700",
-		"2006-01-02T15:04:05-0700",
+		"2006T15:04Z0700",
+		"2006-01T15:04Z0700",
+		"2006-01-02T15:04Z0700",
 
-		"2006T15:04:05.000-0700",
-		"2006-01T15:04:05.000-0700",
-		"2006-01-02T15:04:05.000-0700",
+		"2006T15:04:05Z0700",
+		"2006-01T15:04:05Z0700",
 
-		time.RFC1123,
-		dateTimeLayout,
+		"2006T15:04:05.000Z0700",
+		"2006-01T15:04:05.000Z0700",
 	}
-	matchDateTimeZone = regexp.MustCompile(`^(.*)(?:(Z)|([\+\-]\d{2}):(\d{2}))$`)
 )
 
 func dateParse(date string) (time.Time, bool) {
-	// YYYY-MM-DDTHH:mm:ss.sssZ
 	var t time.Time
 	var err error
-	{
-		date := date
-		if match := matchDateTimeZone.FindStringSubmatch(date); match != nil {
-			if match[2] == "Z" {
-				date = match[1] + "+0000"
-			} else {
-				date = match[1] + match[3] + match[4]
-			}
-		}
-		for _, layout := range dateLayoutList {
-			t, err = time.Parse(layout, date)
-			if err == nil {
-				break
-			}
+	for _, layout := range dateLayoutList {
+		t, err = parseDate(layout, date, time.UTC)
+		if err == nil {
+			break
 		}
 	}
-	return t, err == nil
+	unix := timeToMsec(t)
+	return t, err == nil && unix >= -8640000000000000 && unix <= 8640000000000000
 }
 
 func (r *Runtime) newDateObject(t time.Time, isSet bool) *Object {

+ 860 - 0
date_parser.go

@@ -0,0 +1,860 @@
+package goja
+
+// This is a slightly modified version of the standard Go parser to make it more compatible with ECMAScript 5.1
+// Changes:
+// - 6-digit extended years are supported in place of long year (2006) in the form of +123456
+// - Timezone formats tolerate colons, e.g. -0700 will parse -07:00
+// - Short week day will also parse long week day
+// - Timezone in brackets, "(MST)", will match any string in brackets (e.g. "(GMT Standard Time)")
+// - If offset is not set and timezone name is unknown, an error is returned
+// - If offset and timezone name are both set the offset takes precedence and the resulting Location will be FixedZone("", offset)
+
+// Original copyright message:
+
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import (
+	"errors"
+	"time"
+)
+
+const (
+	_                        = iota
+	stdLongMonth             = iota + stdNeedDate  // "January"
+	stdMonth                                       // "Jan"
+	stdNumMonth                                    // "1"
+	stdZeroMonth                                   // "01"
+	stdLongWeekDay                                 // "Monday"
+	stdWeekDay                                     // "Mon"
+	stdDay                                         // "2"
+	stdUnderDay                                    // "_2"
+	stdZeroDay                                     // "02"
+	stdHour                  = iota + stdNeedClock // "15"
+	stdHour12                                      // "3"
+	stdZeroHour12                                  // "03"
+	stdMinute                                      // "4"
+	stdZeroMinute                                  // "04"
+	stdSecond                                      // "5"
+	stdZeroSecond                                  // "05"
+	stdLongYear              = iota + stdNeedDate  // "2006"
+	stdYear                                        // "06"
+	stdPM                    = iota + stdNeedClock // "PM"
+	stdpm                                          // "pm"
+	stdTZ                    = iota                // "MST"
+	stdBracketTZ                                   // "(MST)"
+	stdISO8601TZ                                   // "Z0700"  // prints Z for UTC
+	stdISO8601SecondsTZ                            // "Z070000"
+	stdISO8601ShortTZ                              // "Z07"
+	stdISO8601ColonTZ                              // "Z07:00" // prints Z for UTC
+	stdISO8601ColonSecondsTZ                       // "Z07:00:00"
+	stdNumTZ                                       // "-0700"  // always numeric
+	stdNumSecondsTz                                // "-070000"
+	stdNumShortTZ                                  // "-07"    // always numeric
+	stdNumColonTZ                                  // "-07:00" // always numeric
+	stdNumColonSecondsTZ                           // "-07:00:00"
+	stdFracSecond0                                 // ".0", ".00", ... , trailing zeros included
+	stdFracSecond9                                 // ".9", ".99", ..., trailing zeros omitted
+
+	stdNeedDate  = 1 << 8             // need month, day, year
+	stdNeedClock = 2 << 8             // need hour, minute, second
+	stdArgShift  = 16                 // extra argument in high bits, above low stdArgShift
+	stdMask      = 1<<stdArgShift - 1 // mask out argument
+)
+
+var errBad = errors.New("bad value for field") // placeholder not passed to user
+
+func parseDate(layout, value string, defaultLocation *time.Location) (time.Time, error) {
+	alayout, avalue := layout, value
+	rangeErrString := "" // set if a value is out of range
+	amSet := false       // do we need to subtract 12 from the hour for midnight?
+	pmSet := false       // do we need to add 12 to the hour?
+
+	// Time being constructed.
+	var (
+		year       int
+		month      int = 1 // January
+		day        int = 1
+		hour       int
+		min        int
+		sec        int
+		nsec       int
+		z          *time.Location
+		zoneOffset int = -1
+		zoneName   string
+	)
+
+	// Each iteration processes one std value.
+	for {
+		var err error
+		prefix, std, suffix := nextStdChunk(layout)
+		stdstr := layout[len(prefix) : len(layout)-len(suffix)]
+		value, err = skip(value, prefix)
+		if err != nil {
+			return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, LayoutElem: prefix, ValueElem: value}
+		}
+		if std == 0 {
+			if len(value) != 0 {
+				return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, ValueElem: value, Message: ": extra text: " + value}
+			}
+			break
+		}
+		layout = suffix
+		var p string
+		switch std & stdMask {
+		case stdYear:
+			if len(value) < 2 {
+				err = errBad
+				break
+			}
+			p, value = value[0:2], value[2:]
+			year, err = atoi(p)
+			if year >= 69 { // Unix time starts Dec 31 1969 in some time zones
+				year += 1900
+			} else {
+				year += 2000
+			}
+		case stdLongYear:
+			if len(value) >= 7 && (value[0] == '-' || value[0] == '+') { // extended year
+				neg := value[0] == '-'
+				p, value = value[1:7], value[7:]
+				year, err = atoi(p)
+				if neg {
+					year = -year
+				}
+			} else {
+				if len(value) < 4 || !isDigit(value, 0) {
+					err = errBad
+					break
+				}
+				p, value = value[0:4], value[4:]
+				year, err = atoi(p)
+			}
+
+		case stdMonth:
+			month, value, err = lookup(shortMonthNames, value)
+			month++
+		case stdLongMonth:
+			month, value, err = lookup(longMonthNames, value)
+			month++
+		case stdNumMonth, stdZeroMonth:
+			month, value, err = getnum(value, std == stdZeroMonth)
+			if month <= 0 || 12 < month {
+				rangeErrString = "month"
+			}
+		case stdWeekDay:
+			// Ignore weekday except for error checking.
+			_, value, err = lookup(longDayNames, value)
+			if err != nil {
+				_, value, err = lookup(shortDayNames, value)
+			}
+		case stdLongWeekDay:
+			_, value, err = lookup(longDayNames, value)
+		case stdDay, stdUnderDay, stdZeroDay:
+			if std == stdUnderDay && len(value) > 0 && value[0] == ' ' {
+				value = value[1:]
+			}
+			day, value, err = getnum(value, std == stdZeroDay)
+			if day < 0 {
+				// Note that we allow any one- or two-digit day here.
+				rangeErrString = "day"
+			}
+		case stdHour:
+			hour, value, err = getnum(value, false)
+			if hour < 0 || 24 <= hour {
+				rangeErrString = "hour"
+			}
+		case stdHour12, stdZeroHour12:
+			hour, value, err = getnum(value, std == stdZeroHour12)
+			if hour < 0 || 12 < hour {
+				rangeErrString = "hour"
+			}
+		case stdMinute, stdZeroMinute:
+			min, value, err = getnum(value, std == stdZeroMinute)
+			if min < 0 || 60 <= min {
+				rangeErrString = "minute"
+			}
+		case stdSecond, stdZeroSecond:
+			sec, value, err = getnum(value, std == stdZeroSecond)
+			if sec < 0 || 60 <= sec {
+				rangeErrString = "second"
+				break
+			}
+			// Special case: do we have a fractional second but no
+			// fractional second in the format?
+			if len(value) >= 2 && value[0] == '.' && isDigit(value, 1) {
+				_, std, _ = nextStdChunk(layout)
+				std &= stdMask
+				if std == stdFracSecond0 || std == stdFracSecond9 {
+					// Fractional second in the layout; proceed normally
+					break
+				}
+				// No fractional second in the layout but we have one in the input.
+				n := 2
+				for ; n < len(value) && isDigit(value, n); n++ {
+				}
+				nsec, rangeErrString, err = parseNanoseconds(value, n)
+				value = value[n:]
+			}
+		case stdPM:
+			if len(value) < 2 {
+				err = errBad
+				break
+			}
+			p, value = value[0:2], value[2:]
+			switch p {
+			case "PM":
+				pmSet = true
+			case "AM":
+				amSet = true
+			default:
+				err = errBad
+			}
+		case stdpm:
+			if len(value) < 2 {
+				err = errBad
+				break
+			}
+			p, value = value[0:2], value[2:]
+			switch p {
+			case "pm":
+				pmSet = true
+			case "am":
+				amSet = true
+			default:
+				err = errBad
+			}
+		case stdISO8601TZ, stdISO8601ColonTZ, stdISO8601SecondsTZ, stdISO8601ShortTZ, stdISO8601ColonSecondsTZ, stdNumTZ, stdNumShortTZ, stdNumColonTZ, stdNumSecondsTz, stdNumColonSecondsTZ:
+			if (std == stdISO8601TZ || std == stdISO8601ShortTZ || std == stdISO8601ColonTZ ||
+				std == stdISO8601SecondsTZ || std == stdISO8601ColonSecondsTZ) && len(value) >= 1 && value[0] == 'Z' {
+
+				value = value[1:]
+				z = time.UTC
+				break
+			}
+			var sign, hour, min, seconds string
+			if std == stdISO8601ColonTZ || std == stdNumColonTZ || std == stdNumTZ || std == stdISO8601TZ {
+				if len(value) < 4 {
+					err = errBad
+					break
+				}
+				if value[3] != ':' {
+					if std == stdNumColonTZ || std == stdISO8601ColonTZ || len(value) < 5 {
+						err = errBad
+						break
+					}
+					sign, hour, min, seconds, value = value[0:1], value[1:3], value[3:5], "00", value[5:]
+				} else {
+					if len(value) < 6 {
+						err = errBad
+						break
+					}
+					sign, hour, min, seconds, value = value[0:1], value[1:3], value[4:6], "00", value[6:]
+				}
+			} else if std == stdNumShortTZ || std == stdISO8601ShortTZ {
+				if len(value) < 3 {
+					err = errBad
+					break
+				}
+				sign, hour, min, seconds, value = value[0:1], value[1:3], "00", "00", value[3:]
+			} else if std == stdISO8601ColonSecondsTZ || std == stdNumColonSecondsTZ || std == stdISO8601SecondsTZ || std == stdNumSecondsTz {
+				if len(value) < 7 {
+					err = errBad
+					break
+				}
+				if value[3] != ':' || value[6] != ':' {
+					if std == stdISO8601ColonSecondsTZ || std == stdNumColonSecondsTZ || len(value) < 7 {
+						err = errBad
+						break
+					}
+					sign, hour, min, seconds, value = value[0:1], value[1:3], value[3:5], value[5:7], value[7:]
+				} else {
+					if len(value) < 9 {
+						err = errBad
+						break
+					}
+					sign, hour, min, seconds, value = value[0:1], value[1:3], value[4:6], value[7:9], value[9:]
+				}
+			}
+			var hr, mm, ss int
+			hr, err = atoi(hour)
+			if err == nil {
+				mm, err = atoi(min)
+			}
+			if err == nil {
+				ss, err = atoi(seconds)
+			}
+			zoneOffset = (hr*60+mm)*60 + ss // offset is in seconds
+			switch sign[0] {
+			case '+':
+			case '-':
+				zoneOffset = -zoneOffset
+			default:
+				err = errBad
+			}
+		case stdTZ:
+			// Does it look like a time zone?
+			if len(value) >= 3 && value[0:3] == "UTC" {
+				z = time.UTC
+				value = value[3:]
+				break
+			}
+			n, ok := parseTimeZone(value)
+			if !ok {
+				err = errBad
+				break
+			}
+			zoneName, value = value[:n], value[n:]
+		case stdBracketTZ:
+			if len(value) < 3 || value[0] != '(' {
+				err = errBad
+				break
+			}
+			i := 1
+			for ; ; i++ {
+				if i >= len(value) {
+					err = errBad
+					break
+				}
+				if value[i] == ')' {
+					zoneName, value = value[1:i], value[i+1:]
+					break
+				}
+			}
+
+		case stdFracSecond0:
+			// stdFracSecond0 requires the exact number of digits as specified in
+			// the layout.
+			ndigit := 1 + (std >> stdArgShift)
+			if len(value) < ndigit {
+				err = errBad
+				break
+			}
+			nsec, rangeErrString, err = parseNanoseconds(value, ndigit)
+			value = value[ndigit:]
+
+		case stdFracSecond9:
+			if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1] {
+				// Fractional second omitted.
+				break
+			}
+			// Take any number of digits, even more than asked for,
+			// because it is what the stdSecond case would do.
+			i := 0
+			for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' {
+				i++
+			}
+			nsec, rangeErrString, err = parseNanoseconds(value, 1+i)
+			value = value[1+i:]
+		}
+		if rangeErrString != "" {
+			return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, LayoutElem: stdstr, ValueElem: value, Message: ": " + rangeErrString + " out of range"}
+		}
+		if err != nil {
+			return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, LayoutElem: stdstr, ValueElem: value}
+		}
+	}
+	if pmSet && hour < 12 {
+		hour += 12
+	} else if amSet && hour == 12 {
+		hour = 0
+	}
+
+	// Validate the day of the month.
+	if day < 1 || day > daysIn(time.Month(month), year) {
+		return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, ValueElem: value, Message: ": day out of range"}
+	}
+
+	if z == nil {
+		if zoneOffset == -1 {
+			if zoneName != "" {
+				if z1, err := time.LoadLocation(zoneName); err == nil {
+					z = z1
+				} else {
+					return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, ValueElem: value, Message: ": unknown timezone"}
+				}
+			} else {
+				z = defaultLocation
+			}
+		} else if zoneOffset == 0 {
+			z = time.UTC
+		} else {
+			z = time.FixedZone("", zoneOffset)
+		}
+	}
+
+	return time.Date(year, time.Month(month), day, hour, min, sec, nsec, z), nil
+}
+
+var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
+
+func signedLeadingInt(s string) (x int64, rem string, err error) {
+	neg := false
+	if s != "" && (s[0] == '-' || s[0] == '+') {
+		neg = s[0] == '-'
+		s = s[1:]
+	}
+	x, rem, err = leadingInt(s)
+	if err != nil {
+		return
+	}
+
+	if neg {
+		x = -x
+	}
+	return
+}
+
+// leadingInt consumes the leading [0-9]* from s.
+func leadingInt(s string) (x int64, rem string, err error) {
+	i := 0
+	for ; i < len(s); i++ {
+		c := s[i]
+		if c < '0' || c > '9' {
+			break
+		}
+		if x > (1<<63-1)/10 {
+			// overflow
+			return 0, "", errLeadingInt
+		}
+		x = x*10 + int64(c) - '0'
+		if x < 0 {
+			// overflow
+			return 0, "", errLeadingInt
+		}
+	}
+	return x, s[i:], nil
+}
+
+// nextStdChunk finds the first occurrence of a std string in
+// layout and returns the text before, the std string, and the text after.
+func nextStdChunk(layout string) (prefix string, std int, suffix string) {
+	for i := 0; i < len(layout); i++ {
+		switch c := int(layout[i]); c {
+		case 'J': // January, Jan
+			if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
+				if len(layout) >= i+7 && layout[i:i+7] == "January" {
+					return layout[0:i], stdLongMonth, layout[i+7:]
+				}
+				if !startsWithLowerCase(layout[i+3:]) {
+					return layout[0:i], stdMonth, layout[i+3:]
+				}
+			}
+
+		case 'M': // Monday, Mon, MST
+			if len(layout) >= i+3 {
+				if layout[i:i+3] == "Mon" {
+					if len(layout) >= i+6 && layout[i:i+6] == "Monday" {
+						return layout[0:i], stdLongWeekDay, layout[i+6:]
+					}
+					if !startsWithLowerCase(layout[i+3:]) {
+						return layout[0:i], stdWeekDay, layout[i+3:]
+					}
+				}
+				if layout[i:i+3] == "MST" {
+					return layout[0:i], stdTZ, layout[i+3:]
+				}
+			}
+
+		case '0': // 01, 02, 03, 04, 05, 06
+			if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
+				return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
+			}
+
+		case '1': // 15, 1
+			if len(layout) >= i+2 && layout[i+1] == '5' {
+				return layout[0:i], stdHour, layout[i+2:]
+			}
+			return layout[0:i], stdNumMonth, layout[i+1:]
+
+		case '2': // 2006, 2
+			if len(layout) >= i+4 && layout[i:i+4] == "2006" {
+				return layout[0:i], stdLongYear, layout[i+4:]
+			}
+			return layout[0:i], stdDay, layout[i+1:]
+
+		case '_': // _2, _2006
+			if len(layout) >= i+2 && layout[i+1] == '2' {
+				//_2006 is really a literal _, followed by stdLongYear
+				if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
+					return layout[0 : i+1], stdLongYear, layout[i+5:]
+				}
+				return layout[0:i], stdUnderDay, layout[i+2:]
+			}
+
+		case '3':
+			return layout[0:i], stdHour12, layout[i+1:]
+
+		case '4':
+			return layout[0:i], stdMinute, layout[i+1:]
+
+		case '5':
+			return layout[0:i], stdSecond, layout[i+1:]
+
+		case 'P': // PM
+			if len(layout) >= i+2 && layout[i+1] == 'M' {
+				return layout[0:i], stdPM, layout[i+2:]
+			}
+
+		case 'p': // pm
+			if len(layout) >= i+2 && layout[i+1] == 'm' {
+				return layout[0:i], stdpm, layout[i+2:]
+			}
+
+		case '-': // -070000, -07:00:00, -0700, -07:00, -07
+			if len(layout) >= i+7 && layout[i:i+7] == "-070000" {
+				return layout[0:i], stdNumSecondsTz, layout[i+7:]
+			}
+			if len(layout) >= i+9 && layout[i:i+9] == "-07:00:00" {
+				return layout[0:i], stdNumColonSecondsTZ, layout[i+9:]
+			}
+			if len(layout) >= i+5 && layout[i:i+5] == "-0700" {
+				return layout[0:i], stdNumTZ, layout[i+5:]
+			}
+			if len(layout) >= i+6 && layout[i:i+6] == "-07:00" {
+				return layout[0:i], stdNumColonTZ, layout[i+6:]
+			}
+			if len(layout) >= i+3 && layout[i:i+3] == "-07" {
+				return layout[0:i], stdNumShortTZ, layout[i+3:]
+			}
+
+		case 'Z': // Z070000, Z07:00:00, Z0700, Z07:00,
+			if len(layout) >= i+7 && layout[i:i+7] == "Z070000" {
+				return layout[0:i], stdISO8601SecondsTZ, layout[i+7:]
+			}
+			if len(layout) >= i+9 && layout[i:i+9] == "Z07:00:00" {
+				return layout[0:i], stdISO8601ColonSecondsTZ, layout[i+9:]
+			}
+			if len(layout) >= i+5 && layout[i:i+5] == "Z0700" {
+				return layout[0:i], stdISO8601TZ, layout[i+5:]
+			}
+			if len(layout) >= i+6 && layout[i:i+6] == "Z07:00" {
+				return layout[0:i], stdISO8601ColonTZ, layout[i+6:]
+			}
+			if len(layout) >= i+3 && layout[i:i+3] == "Z07" {
+				return layout[0:i], stdISO8601ShortTZ, layout[i+3:]
+			}
+
+		case '.': // .000 or .999 - repeated digits for fractional seconds.
+			if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') {
+				ch := layout[i+1]
+				j := i + 1
+				for j < len(layout) && layout[j] == ch {
+					j++
+				}
+				// String of digits must end here - only fractional second is all digits.
+				if !isDigit(layout, j) {
+					std := stdFracSecond0
+					if layout[i+1] == '9' {
+						std = stdFracSecond9
+					}
+					std |= (j - (i + 1)) << stdArgShift
+					return layout[0:i], std, layout[j:]
+				}
+			}
+		case '(':
+			if len(layout) >= i+5 && layout[i:i+5] == "(MST)" {
+				return layout[0:i], stdBracketTZ, layout[i+5:]
+			}
+		}
+	}
+	return layout, 0, ""
+}
+
+var longDayNames = []string{
+	"Sunday",
+	"Monday",
+	"Tuesday",
+	"Wednesday",
+	"Thursday",
+	"Friday",
+	"Saturday",
+}
+
+var shortDayNames = []string{
+	"Sun",
+	"Mon",
+	"Tue",
+	"Wed",
+	"Thu",
+	"Fri",
+	"Sat",
+}
+
+var shortMonthNames = []string{
+	"Jan",
+	"Feb",
+	"Mar",
+	"Apr",
+	"May",
+	"Jun",
+	"Jul",
+	"Aug",
+	"Sep",
+	"Oct",
+	"Nov",
+	"Dec",
+}
+
+var longMonthNames = []string{
+	"January",
+	"February",
+	"March",
+	"April",
+	"May",
+	"June",
+	"July",
+	"August",
+	"September",
+	"October",
+	"November",
+	"December",
+}
+
+// isDigit reports whether s[i] is in range and is a decimal digit.
+func isDigit(s string, i int) bool {
+	if len(s) <= i {
+		return false
+	}
+	c := s[i]
+	return '0' <= c && c <= '9'
+}
+
+// getnum parses s[0:1] or s[0:2] (fixed forces the latter)
+// as a decimal integer and returns the integer and the
+// remainder of the string.
+func getnum(s string, fixed bool) (int, string, error) {
+	if !isDigit(s, 0) {
+		return 0, s, errBad
+	}
+	if !isDigit(s, 1) {
+		if fixed {
+			return 0, s, errBad
+		}
+		return int(s[0] - '0'), s[1:], nil
+	}
+	return int(s[0]-'0')*10 + int(s[1]-'0'), s[2:], nil
+}
+
+func cutspace(s string) string {
+	for len(s) > 0 && s[0] == ' ' {
+		s = s[1:]
+	}
+	return s
+}
+
+// skip removes the given prefix from value,
+// treating runs of space characters as equivalent.
+func skip(value, prefix string) (string, error) {
+	for len(prefix) > 0 {
+		if prefix[0] == ' ' {
+			if len(value) > 0 && value[0] != ' ' {
+				return value, errBad
+			}
+			prefix = cutspace(prefix)
+			value = cutspace(value)
+			continue
+		}
+		if len(value) == 0 || value[0] != prefix[0] {
+			return value, errBad
+		}
+		prefix = prefix[1:]
+		value = value[1:]
+	}
+	return value, nil
+}
+
+// Never printed, just needs to be non-nil for return by atoi.
+var atoiError = errors.New("time: invalid number")
+
+// Duplicates functionality in strconv, but avoids dependency.
+func atoi(s string) (x int, err error) {
+	q, rem, err := signedLeadingInt(s)
+	x = int(q)
+	if err != nil || rem != "" {
+		return 0, atoiError
+	}
+	return x, nil
+}
+
+// match reports whether s1 and s2 match ignoring case.
+// It is assumed s1 and s2 are the same length.
+func match(s1, s2 string) bool {
+	for i := 0; i < len(s1); i++ {
+		c1 := s1[i]
+		c2 := s2[i]
+		if c1 != c2 {
+			// Switch to lower-case; 'a'-'A' is known to be a single bit.
+			c1 |= 'a' - 'A'
+			c2 |= 'a' - 'A'
+			if c1 != c2 || c1 < 'a' || c1 > 'z' {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+func lookup(tab []string, val string) (int, string, error) {
+	for i, v := range tab {
+		if len(val) >= len(v) && match(val[0:len(v)], v) {
+			return i, val[len(v):], nil
+		}
+	}
+	return -1, val, errBad
+}
+
+// daysBefore[m] counts the number of days in a non-leap year
+// before month m begins. There is an entry for m=12, counting
+// the number of days before January of next year (365).
+var daysBefore = [...]int32{
+	0,
+	31,
+	31 + 28,
+	31 + 28 + 31,
+	31 + 28 + 31 + 30,
+	31 + 28 + 31 + 30 + 31,
+	31 + 28 + 31 + 30 + 31 + 30,
+	31 + 28 + 31 + 30 + 31 + 30 + 31,
+	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
+	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
+	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
+	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
+	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
+}
+
+func isLeap(year int) bool {
+	return year%4 == 0 && (year%100 != 0 || year%400 == 0)
+}
+
+func daysIn(m time.Month, year int) int {
+	if m == time.February && isLeap(year) {
+		return 29
+	}
+	return int(daysBefore[m] - daysBefore[m-1])
+}
+
+// parseTimeZone parses a time zone string and returns its length. Time zones
+// are human-generated and unpredictable. We can't do precise error checking.
+// On the other hand, for a correct parse there must be a time zone at the
+// beginning of the string, so it's almost always true that there's one
+// there. We look at the beginning of the string for a run of upper-case letters.
+// If there are more than 5, it's an error.
+// If there are 4 or 5 and the last is a T, it's a time zone.
+// If there are 3, it's a time zone.
+// Otherwise, other than special cases, it's not a time zone.
+// GMT is special because it can have an hour offset.
+func parseTimeZone(value string) (length int, ok bool) {
+	if len(value) < 3 {
+		return 0, false
+	}
+	// Special case 1: ChST and MeST are the only zones with a lower-case letter.
+	if len(value) >= 4 && (value[:4] == "ChST" || value[:4] == "MeST") {
+		return 4, true
+	}
+	// Special case 2: GMT may have an hour offset; treat it specially.
+	if value[:3] == "GMT" {
+		length = parseGMT(value)
+		return length, true
+	}
+	// Special Case 3: Some time zones are not named, but have +/-00 format
+	if value[0] == '+' || value[0] == '-' {
+		length = parseSignedOffset(value)
+		return length, true
+	}
+	// How many upper-case letters are there? Need at least three, at most five.
+	var nUpper int
+	for nUpper = 0; nUpper < 6; nUpper++ {
+		if nUpper >= len(value) {
+			break
+		}
+		if c := value[nUpper]; c < 'A' || 'Z' < c {
+			break
+		}
+	}
+	switch nUpper {
+	case 0, 1, 2, 6:
+		return 0, false
+	case 5: // Must end in T to match.
+		if value[4] == 'T' {
+			return 5, true
+		}
+	case 4:
+		// Must end in T, except one special case.
+		if value[3] == 'T' || value[:4] == "WITA" {
+			return 4, true
+		}
+	case 3:
+		return 3, true
+	}
+	return 0, false
+}
+
+// parseGMT parses a GMT time zone. The input string is known to start "GMT".
+// The function checks whether that is followed by a sign and a number in the
+// range -14 through 12 excluding zero.
+func parseGMT(value string) int {
+	value = value[3:]
+	if len(value) == 0 {
+		return 3
+	}
+
+	return 3 + parseSignedOffset(value)
+}
+
+// parseSignedOffset parses a signed timezone offset (e.g. "+03" or "-04").
+// The function checks for a signed number in the range -14 through +12 excluding zero.
+// Returns length of the found offset string or 0 otherwise
+func parseSignedOffset(value string) int {
+	sign := value[0]
+	if sign != '-' && sign != '+' {
+		return 0
+	}
+	x, rem, err := leadingInt(value[1:])
+	if err != nil {
+		return 0
+	}
+	if sign == '-' {
+		x = -x
+	}
+	if x == 0 || x < -14 || 12 < x {
+		return 0
+	}
+	return len(value) - len(rem)
+}
+
+func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) {
+	if value[0] != '.' {
+		err = errBad
+		return
+	}
+	if ns, err = atoi(value[1:nbytes]); err != nil {
+		return
+	}
+	if ns < 0 || 1e9 <= ns {
+		rangeErrString = "fractional second"
+		return
+	}
+	// We need nanoseconds, which means scaling by the number
+	// of missing digits in the format, maximum length 10. If it's
+	// longer than 10, we won't scale.
+	scaleDigits := 10 - nbytes
+	for i := 0; i < scaleDigits; i++ {
+		ns *= 10
+	}
+	return
+}
+
+// std0x records the std values for "01", "02", ..., "06".
+var std0x = [...]int{stdZeroMonth, stdZeroDay, stdZeroHour12, stdZeroMinute, stdZeroSecond, stdYear}
+
+// startsWithLowerCase reports whether the string has a lower-case letter at the beginning.
+// Its purpose is to prevent matching strings like "Month" when looking for "Mon".
+func startsWithLowerCase(str string) bool {
+	if len(str) == 0 {
+		return false
+	}
+	c := str[0]
+	return 'a' <= c && c <= 'z'
+}

+ 31 - 0
date_parser_test.go

@@ -0,0 +1,31 @@
+package goja
+
+import (
+	"testing"
+	"time"
+)
+
+func TestParseDate(t *testing.T) {
+
+	tst := func(layout, value string, expectedTs int64) func(t *testing.T) {
+		return func(t *testing.T) {
+			t.Parallel()
+			tm, err := parseDate(layout, value, time.UTC)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if tm.Unix() != expectedTs {
+				t.Fatal(tm)
+			}
+		}
+	}
+
+	t.Run("1", tst("2006-01-02T15:04:05.000Z070000", "2006-01-02T15:04:05.000+07:00:00", 1136189045))
+	t.Run("2", tst("2006-01-02T15:04:05.000Z07:00:00", "2006-01-02T15:04:05.000+07:00:00", 1136189045))
+	t.Run("3", tst("2006-01-02T15:04:05.000Z07:00", "2006-01-02T15:04:05.000+07:00", 1136189045))
+	t.Run("4", tst("2006-01-02T15:04:05.000Z070000", "2006-01-02T15:04:05.000+070000", 1136189045))
+	t.Run("5", tst("2006-01-02T15:04:05.000Z070000", "2006-01-02T15:04:05.000Z", 1136214245))
+	t.Run("6", tst("2006-01-02T15:04:05.000Z0700", "2006-01-02T15:04:05.000Z", 1136214245))
+	t.Run("7", tst("2006-01-02T15:04:05.000Z07", "2006-01-02T15:04:05.000Z", 1136214245))
+
+}

+ 99 - 0
date_test.go

@@ -217,3 +217,102 @@ func TestDateSetters(t *testing.T) {
 
 	testScript1(TESTLIB+SCRIPT, _undefined, t)
 }
+
+func TestDateParse(t *testing.T) {
+	const SCRIPT = `
+var zero = new Date(0);
+
+assert.sameValue(zero.valueOf(), Date.parse(zero.toString()),
+                 "Date.parse(zeroDate.toString())");
+assert.sameValue(zero.valueOf(), Date.parse(zero.toUTCString()),
+                 "Date.parse(zeroDate.toUTCString())");
+assert.sameValue(zero.valueOf(), Date.parse(zero.toISOString()),
+                 "Date.parse(zeroDate.toISOString())");
+
+assert.sameValue(Date.parse("Mon, 02 Jan 2006 15:04:05 MST"), 1136239445000,
+				 "Date.parse(\"Mon, 02 Jan 2006 15:04:05 MST\")");
+
+assert.sameValue(Date.parse("Mon, 02 Jan 2006 15:04:05 GMT-07:00 (MST)"), 1136239445000,
+				 "Date.parse(\"Mon, 02 Jan 2006 15:04:05 GMT-07:00 (MST)\")");
+
+assert.sameValue(Date.parse("Mon, 02 Jan 2006 15:04:05 -07:00 (MST)"), 1136239445000,
+				 "Date.parse(\"Mon, 02 Jan 2006 15:04:05 -07:00 (MST)\")");
+
+assert.sameValue(Date.parse("Monday, 02 Jan 2006 15:04:05 -0700 (MST)"), 1136239445000,
+				 "Date.parse(\"Monday, 02 Jan 2006 15:04:05 -0700 (MST)\")");
+
+assert.sameValue(Date.parse("Mon Jan 02 2006 15:04:05 GMT-0700 (GMT Standard Time)"), 1136239445000,
+				 "Date.parse(\"Mon Jan 02 2006 15:04:05 GMT-0700 (GMT Standard Time)\")");
+
+assert.sameValue(Date.parse("2006-01-02T15:04:05.000Z"), 1136214245000,
+				 "Date.parse(\"2006-01-02T15:04:05.000Z\")");
+
+assert.sameValue(Date.parse("2006-06-02T15:04:05.000"), 1149260645000,
+				 "Date.parse(\"2006-01-02T15:04:05.000\")");
+
+assert.sameValue(Date.parse("2006-01-02T15:04:05"), 1136214245000,
+				 "Date.parse(\"2006-01-02T15:04:05\")");
+
+assert.sameValue(Date.parse("2006-01-02"), 1136160000000,
+				 "Date.parse(\"2006-01-02\")");
+
+assert.sameValue(Date.parse("2006T15:04-0700"), 1136153040000,
+				 "Date.parse(\"2006T15:04-0700\")");
+
+assert.sameValue(Date.parse("2006T15:04Z"), 1136127840000,
+				 "Date.parse(\"2006T15:04Z\")");
+
+assert.sameValue(Date.parse("Mon Jan 2 15:04:05 MST 2006"), 1136239445000,
+				 "Date.parse(\"Mon Jan 2 15:04:05 MST 2006\")");
+
+assert.sameValue(Date.parse("Mon Jan 02 15:04:05 MST 2006"), 1136239445000,
+				 "Date.parse(\"Mon Jan 02 15:04:05 MST 2006\")");
+
+assert.sameValue(Date.parse("Mon Jan 02 15:04:05 -0700 2006"), 1136239445000,
+				 "Date.parse(\"Mon Jan 02 15:04:05 -0700 2006\")");
+
+var d = new Date("Mon, 02 Jan 2006 15:04:05 MST");
+
+assert.sameValue(d.getUTCHours(), 22,
+				"new Date(\"Mon, 02 Jan 2006 15:04:05 MST\").getUTCHours()");
+
+assert.sameValue(d.getHours(), 17,
+				"new Date(\"Mon, 02 Jan 2006 15:04:05 MST\").getHours()");
+
+assert.sameValue(Date.parse("Mon, 02 Jan 2006 15:04:05 zzz"), NaN,
+				 "Date.parse(\"Mon, 02 Jan 2006 15:04:05 zzz\")");
+
+assert.sameValue(Date.parse("Mon, 02 Jan 2006 15:04:05 ZZZ"), NaN,
+				 "Date.parse(\"Mon, 02 Jan 2006 15:04:05 ZZZ\")");
+
+var minDateStr = "-271821-04-20T00:00:00.000Z";
+var minDate = new Date(-8640000000000000);
+
+assert.sameValue(minDate.toISOString(), minDateStr, "minDateStr");
+assert.sameValue(Date.parse(minDateStr), minDate.valueOf(), "parse minDateStr");
+
+var maxDateStr = "+275760-09-13T00:00:00.000Z";
+var maxDate = new Date(8640000000000000);
+
+assert.sameValue(maxDate.toISOString(), maxDateStr, "maxDateStr");
+assert.sameValue(Date.parse(maxDateStr), maxDate.valueOf(), "parse maxDateStr");
+
+var belowRange = "-271821-04-19T23:59:59.999Z";
+var aboveRange = "+275760-09-13T00:00:00.001Z";
+
+assert.sameValue(Date.parse(belowRange), NaN, "parse below minimum time value");
+assert.sameValue(Date.parse(aboveRange), NaN, "parse above maximum time value");
+	`
+
+	l := time.Local
+	defer func() {
+		time.Local = l
+	}()
+	var err error
+	time.Local, err = time.LoadLocation("America/New_York")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testScript1(TESTLIB+SCRIPT, _undefined, t)
+}