Skip to content

Commit 35b7b3c

Browse files
committed
Improve Date.parse
- accept many more alternative date/time formats - add test cases in tests/test_builtin.js - match month and timezone names case insensitively - accept AM and PM markers - recognize US timezone names - skip parenthesized stuff - fix almost all v8 test cases
1 parent 8d64731 commit 35b7b3c

File tree

2 files changed

+122
-39
lines changed

2 files changed

+122
-39
lines changed

quickjs.c

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -49886,23 +49886,28 @@ static BOOL string_skip_char(const uint8_t *sp, int *pp, int c) {
4988649886
}
4988749887
}
4988849888

49889-
/* skip spaces, update offset */
49890-
static void string_skip_spaces(const uint8_t *sp, int *pp) {
49891-
while (sp[*pp] == ' ')
49889+
/* skip spaces, update offset, return next char */
49890+
static int string_skip_spaces(const uint8_t *sp, int *pp) {
49891+
int c;
49892+
while ((c = sp[*pp]) == ' ')
4989249893
*pp += 1;
49894+
return c;
4989349895
}
4989449896

4989549897
/* skip dashes dots and commas */
49896-
static void string_skip_separators(const uint8_t *sp, int *pp) {
49898+
static int string_skip_separators(const uint8_t *sp, int *pp) {
4989749899
int c;
49898-
while ((c = sp[*pp]) == '-' || c == '.' || c == ',')
49900+
while ((c = sp[*pp]) == '-' || c == '/' || c == '.' || c == ',')
4989949901
*pp += 1;
49902+
return c;
4990049903
}
4990149904

49902-
/* skip non spaces, update offset */
49903-
static void string_skip_non_spaces(const uint8_t *sp, int *pp) {
49904-
while (sp[*pp] != '\0' && sp[*pp] != ' ')
49905+
/* skip a word, stop on spaces, digits and separators, update offset */
49906+
static int string_skip_until(const uint8_t *sp, int *pp, const char *stoplist) {
49907+
int c;
49908+
while (!strchr(stoplist, c = sp[*pp]))
4990549909
*pp += 1;
49910+
return c;
4990649911
}
4990749912

4990849913
/* parse a numeric field (max_digits = 0 -> no maximum) */
@@ -49950,37 +49955,52 @@ static BOOL string_get_milliseconds(const uint8_t *sp, int *pp, int *pval) {
4995049955
return TRUE;
4995149956
}
4995249957

49953-
static BOOL string_get_timezone(const uint8_t *sp, int *pp, int *tzp) {
49958+
static BOOL string_get_timezone(const uint8_t *sp, int *pp, int *tzp, BOOL strict) {
4995449959
int tz = 0, sgn, hh, mm, p = *pp;
4995549960

49956-
sgn = sp[p];
49961+
sgn = sp[p++];
4995749962
if (sgn == '+' || sgn == '-') {
49958-
p++;
49959-
if (!string_get_digits(sp, &p, &hh, 2, 2))
49963+
int n = p;
49964+
if (!string_get_digits(sp, &p, &hh, 1, 9))
4996049965
return FALSE;
49961-
string_skip_char(sp, &p, ':'); /* optional separator */
49962-
if (!string_get_digits(sp, &p, &mm, 2, 2))
49966+
n = p - n;
49967+
if (strict && n != 2 && n != 4)
4996349968
return FALSE;
49969+
while (n > 4) {
49970+
n -= 2;
49971+
hh /= 100;
49972+
}
49973+
if (n > 2) {
49974+
mm = hh % 100;
49975+
hh = hh / 100;
49976+
} else {
49977+
mm = 0;
49978+
if (string_skip_char(sp, &p, ':') /* optional separator */
49979+
&& !string_get_digits(sp, &p, &mm, 2, 2))
49980+
return FALSE;
49981+
}
4996449982
if (hh > 23 || mm > 59)
4996549983
return FALSE;
4996649984
tz = hh * 60 + mm;
4996749985
if (sgn != '+')
4996849986
tz = -tz;
4996949987
} else
49970-
if (sgn == 'Z') {
49971-
p++;
49972-
} else {
49988+
if (sgn != 'Z') {
4997349989
return FALSE;
4997449990
}
4997549991
*pp = p;
4997649992
*tzp = tz;
4997749993
return TRUE;
4997849994
}
4997949995

49996+
static uint8_t upper_ascii(uint8_t c) {
49997+
return c >= 'a' && c <= 'z' ? c - 'a' + 'Z' : c;
49998+
}
49999+
4998050000
static BOOL string_match(const uint8_t *sp, int *pp, const char *s) {
4998150001
int p = *pp;
4998250002
while (*s != '\0') {
49983-
if (sp[p] != (uint8_t)*s++)
50003+
if (upper_ascii(sp[p]) != upper_ascii(*s++))
4998450004
return FALSE;
4998550005
p++;
4998650006
}
@@ -49993,7 +50013,7 @@ static int find_abbrev(const uint8_t *sp, int p, const char *list, int count) {
4999350013

4999450014
for (n = 0; n < count; n++) {
4999550015
for (i = 0;; i++) {
49996-
if (sp[p + i] != (uint8_t)month_names[n * 3 + i])
50016+
if (upper_ascii(sp[p + i]) != upper_ascii(list[n * 3 + i]))
4999750017
break;
4999850018
if (i == 2)
4999950019
return n;
@@ -50056,8 +50076,10 @@ static BOOL js_date_parse_isostring(const uint8_t *sp, int fields[9], BOOL *is_l
5005650076
*is_local = TRUE;
5005750077
if (!string_get_digits(sp, &p, &fields[3], 2, 2) /* hour */
5005850078
|| !string_skip_char(sp, &p, ':')
50059-
|| !string_get_digits(sp, &p, &fields[4], 2, 2)) /* minute */
50060-
return FALSE;
50079+
|| !string_get_digits(sp, &p, &fields[4], 2, 2)) { /* minute */
50080+
fields[3] = 100; // reject unconditionally
50081+
return TRUE;
50082+
}
5006150083
if (string_skip_char(sp, &p, ':')) {
5006250084
if (!string_get_digits(sp, &p, &fields[5], 2, 2)) /* second */
5006350085
return FALSE;
@@ -50067,7 +50089,7 @@ static BOOL js_date_parse_isostring(const uint8_t *sp, int fields[9], BOOL *is_l
5006750089
/* parse the time zone offset if present: [+-]HH:mm or [+-]HHmm */
5006850090
if (sp[p]) {
5006950091
*is_local = FALSE;
50070-
if (!string_get_timezone(sp, &p, &fields[8]))
50092+
if (!string_get_timezone(sp, &p, &fields[8], TRUE))
5007150093
return FALSE;
5007250094
}
5007350095
/* error if extraneous characters */
@@ -50092,11 +50114,10 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is
5009250114
}
5009350115
*is_local = TRUE;
5009450116

50095-
while (sp[p] != '\0') {
50096-
string_skip_spaces(sp, &p);
50117+
while (string_skip_spaces(sp, &p)) {
5009750118
p_start = p;
5009850119
if ((c = sp[p]) == '+' || c == '-') {
50099-
if (has_time && string_get_timezone(sp, &p, &fields[8])) {
50120+
if (has_time && string_get_timezone(sp, &p, &fields[8], FALSE)) {
5010050121
*is_local = FALSE;
5010150122
} else {
5010250123
p++;
@@ -50129,7 +50150,7 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is
5012950150
has_year = TRUE;
5013050151
} else
5013150152
if (val < 1 || val > 31) {
50132-
fields[0] = val + (val < 100) * 1900;
50153+
fields[0] = val + (val < 100) * 1900 + (val < 50) * 100;
5013350154
has_year = TRUE;
5013450155
} else {
5013550156
if (num_index == 3)
@@ -50140,19 +50161,73 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is
5014050161
} else
5014150162
if (string_get_month(sp, &p, &fields[1])) {
5014250163
has_mon = TRUE;
50164+
string_skip_until(sp, &p, "0123456789 -/(");
5014350165
} else
5014450166
if (c == 'Z') {
5014550167
*is_local = FALSE;
5014650168
p++;
5014750169
continue;
5014850170
} else
50149-
if (string_match(sp, &p, "GMT") || string_match(sp, &p, "UTC")) {
50171+
if (has_time && string_match(sp, &p, "PM")) {
50172+
if (fields[3] < 12)
50173+
fields[3] += 12;
50174+
continue;
50175+
} else
50176+
if (has_time && string_match(sp, &p, "AM")) {
50177+
if (fields[3] == 12)
50178+
fields[3] -= 12;
50179+
continue;
50180+
} else
50181+
if (string_match(sp, &p, "GMT")
50182+
|| string_match(sp, &p, "UTC")
50183+
|| string_match(sp, &p, "UT")) {
50184+
*is_local = FALSE;
50185+
continue;
50186+
} else
50187+
if (string_match(sp, &p, "EDT")) {
50188+
fields[8] = -4 * 60;
50189+
*is_local = FALSE;
50190+
continue;
50191+
} else
50192+
if (string_match(sp, &p, "EST") || string_match(sp, &p, "CDT")) {
50193+
fields[8] = -5 * 60;
50194+
*is_local = FALSE;
50195+
continue;
50196+
} else
50197+
if (string_match(sp, &p, "CST") || string_match(sp, &p, "MDT")) {
50198+
fields[8] = -6 * 60;
50199+
*is_local = FALSE;
50200+
continue;
50201+
} else
50202+
if (string_match(sp, &p, "MST") || string_match(sp, &p, "PDT")) {
50203+
fields[8] = -7 * 60;
5015050204
*is_local = FALSE;
5015150205
continue;
50206+
} else
50207+
if (string_match(sp, &p, "PST")) {
50208+
fields[8] = -8 * 60;
50209+
*is_local = FALSE;
50210+
continue;
50211+
} else
50212+
if (c == '(') { /* skip parenthesized phrase */
50213+
int level = 0;
50214+
while ((c = sp[p]) != '\0') {
50215+
p++;
50216+
level += (c == '(');
50217+
level -= (c == ')');
50218+
if (!level)
50219+
break;
50220+
}
50221+
if (level > 0)
50222+
return FALSE;
50223+
} else
50224+
if (c == ')') {
50225+
return FALSE;
5015250226
} else {
50227+
if (has_year + has_mon + has_time + num_index)
50228+
return FALSE;
5015350229
/* skip a word */
50154-
string_skip_non_spaces(sp, &p);
50155-
continue;
50230+
string_skip_until(sp, &p, " -/(");
5015650231
}
5015750232
string_skip_separators(sp, &p);
5015850233
}
@@ -50176,15 +50251,15 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is
5017650251
fields[2] = num[1];
5017750252
} else
5017850253
if (has_mon) {
50179-
fields[0] = num[1] + (num[1] < 100) * 1900;
50254+
fields[0] = num[1] + (num[1] < 100) * 1900 + (num[1] < 50) * 100;
5018050255
fields[2] = num[0];
5018150256
} else {
5018250257
fields[1] = num[0];
5018350258
fields[2] = num[1];
5018450259
}
5018550260
break;
5018650261
case 3:
50187-
fields[0] = num[2] + (num[2] < 100) * 1900;
50262+
fields[0] = num[2] + (num[2] < 100) * 1900 + (num[2] < 50) * 100;
5018850263
fields[1] = num[0];
5018950264
fields[2] = num[1];
5019050265
break;

tests/test_builtin.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,14 @@ function test_date()
600600
assert((new Date("2020-01-01T01:01:01.9999Z")).toISOString(),
601601
"2020-01-01T01:01:01.999Z");
602602

603+
assert(Date.UTC(2017), 1483228800000);
604+
assert(Date.UTC(2017, 9), 1506816000000);
605+
assert(Date.UTC(2017, 9, 22), 1508630400000);
606+
assert(Date.UTC(2017, 9, 22, 18), 1508695200000);
607+
assert(Date.UTC(2017, 9, 22, 18, 10), 1508695800000);
608+
assert(Date.UTC(2017, 9, 22, 18, 10, 11), 1508695811000);
609+
assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91), 1508695811091);
610+
603611
assert(Date.UTC(NaN), NaN);
604612
assert(Date.UTC(2017, NaN), NaN);
605613
assert(Date.UTC(2017, 9, NaN), NaN);
@@ -609,14 +617,14 @@ function test_date()
609617
assert(Date.UTC(2017, 9, 22, 18, 10, 11, NaN), NaN);
610618
assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91, NaN), 1508695811091);
611619

612-
assert(Date.UTC(2017), 1483228800000);
613-
assert(Date.UTC(2017, 9), 1506816000000);
614-
assert(Date.UTC(2017, 9, 22), 1508630400000);
615-
assert(Date.UTC(2017, 9, 22, 18), 1508695200000);
616-
assert(Date.UTC(2017, 9, 22, 18, 10), 1508695800000);
617-
assert(Date.UTC(2017, 9, 22, 18, 10, 11), 1508695811000);
618-
assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91), 1508695811091);
619-
620+
// TODO: Fix rounding errors on Windows/Cygwin.
621+
if (!(typeof os !== 'undefined' && ['win32', 'cygwin'].includes(os.platform))) {
622+
// from test262/test/built-ins/Date/UTC/fp-evaluation-order.js
623+
assert(Date.UTC(1970, 0, 1, 80063993375, 29, 1, -288230376151711740), 29312,
624+
'order of operations / precision in MakeTime');
625+
assert(Date.UTC(1970, 0, 213503982336, 0, 0, 0, -18446744073709552000), 34447360,
626+
'precision in MakeDate');
627+
}
620628
//assert(Date.UTC(2017 - 1e9, 9 + 12e9), 1506816000000); // node fails this
621629
assert(Date.UTC(2017, 9, 22 - 1e10, 18 + 24e10), 1508695200000);
622630
assert(Date.UTC(2017, 9, 22, 18 - 1e10, 10 + 60e10), 1508695800000);

0 commit comments

Comments
 (0)