quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

commit 32e65cc1cd08d050ad6029b009c37cac9566ed0e
parent 4ee75cd021fae12cd3913395dc314333a7140ec6
Author: Charlie Gordon <github@chqrlie.org>
Date:   Sun,  3 Mar 2024 02:59:08 +0100

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

Diffstat:
Mquickjs/quickjs.c | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mquickjs/tests/test_builtin.js | 24++++++++++++++++--------
2 files changed, 122 insertions(+), 39 deletions(-)

diff --git a/quickjs/quickjs.c b/quickjs/quickjs.c @@ -50104,23 +50104,28 @@ static BOOL string_skip_char(const uint8_t *sp, int *pp, int c) { } } -/* skip spaces, update offset */ -static void string_skip_spaces(const uint8_t *sp, int *pp) { - while (sp[*pp] == ' ') +/* skip spaces, update offset, return next char */ +static int string_skip_spaces(const uint8_t *sp, int *pp) { + int c; + while ((c = sp[*pp]) == ' ') *pp += 1; + return c; } /* skip dashes dots and commas */ -static void string_skip_separators(const uint8_t *sp, int *pp) { +static int string_skip_separators(const uint8_t *sp, int *pp) { int c; - while ((c = sp[*pp]) == '-' || c == '.' || c == ',') + while ((c = sp[*pp]) == '-' || c == '/' || c == '.' || c == ',') *pp += 1; + return c; } -/* skip non spaces, update offset */ -static void string_skip_non_spaces(const uint8_t *sp, int *pp) { - while (sp[*pp] != '\0' && sp[*pp] != ' ') +/* skip a word, stop on spaces, digits and separators, update offset */ +static int string_skip_until(const uint8_t *sp, int *pp, const char *stoplist) { + int c; + while (!strchr(stoplist, c = sp[*pp])) *pp += 1; + return c; } /* parse a numeric field (max_digits = 0 -> no maximum) */ @@ -50168,26 +50173,37 @@ static BOOL string_get_milliseconds(const uint8_t *sp, int *pp, int *pval) { return TRUE; } -static BOOL string_get_timezone(const uint8_t *sp, int *pp, int *tzp) { +static BOOL string_get_timezone(const uint8_t *sp, int *pp, int *tzp, BOOL strict) { int tz = 0, sgn, hh, mm, p = *pp; - sgn = sp[p]; + sgn = sp[p++]; if (sgn == '+' || sgn == '-') { - p++; - if (!string_get_digits(sp, &p, &hh, 2, 2)) + int n = p; + if (!string_get_digits(sp, &p, &hh, 1, 9)) return FALSE; - string_skip_char(sp, &p, ':'); /* optional separator */ - if (!string_get_digits(sp, &p, &mm, 2, 2)) + n = p - n; + if (strict && n != 2 && n != 4) return FALSE; + while (n > 4) { + n -= 2; + hh /= 100; + } + if (n > 2) { + mm = hh % 100; + hh = hh / 100; + } else { + mm = 0; + if (string_skip_char(sp, &p, ':') /* optional separator */ + && !string_get_digits(sp, &p, &mm, 2, 2)) + return FALSE; + } if (hh > 23 || mm > 59) return FALSE; tz = hh * 60 + mm; if (sgn != '+') tz = -tz; } else - if (sgn == 'Z') { - p++; - } else { + if (sgn != 'Z') { return FALSE; } *pp = p; @@ -50195,10 +50211,14 @@ static BOOL string_get_timezone(const uint8_t *sp, int *pp, int *tzp) { return TRUE; } +static uint8_t upper_ascii(uint8_t c) { + return c >= 'a' && c <= 'z' ? c - 'a' + 'Z' : c; +} + static BOOL string_match(const uint8_t *sp, int *pp, const char *s) { int p = *pp; while (*s != '\0') { - if (sp[p] != (uint8_t)*s++) + if (upper_ascii(sp[p]) != upper_ascii(*s++)) return FALSE; p++; } @@ -50211,7 +50231,7 @@ static int find_abbrev(const uint8_t *sp, int p, const char *list, int count) { for (n = 0; n < count; n++) { for (i = 0;; i++) { - if (sp[p + i] != (uint8_t)month_names[n * 3 + i]) + if (upper_ascii(sp[p + i]) != upper_ascii(list[n * 3 + i])) break; if (i == 2) return n; @@ -50274,8 +50294,10 @@ static BOOL js_date_parse_isostring(const uint8_t *sp, int fields[9], BOOL *is_l *is_local = TRUE; if (!string_get_digits(sp, &p, &fields[3], 2, 2) /* hour */ || !string_skip_char(sp, &p, ':') - || !string_get_digits(sp, &p, &fields[4], 2, 2)) /* minute */ - return FALSE; + || !string_get_digits(sp, &p, &fields[4], 2, 2)) { /* minute */ + fields[3] = 100; // reject unconditionally + return TRUE; + } if (string_skip_char(sp, &p, ':')) { if (!string_get_digits(sp, &p, &fields[5], 2, 2)) /* second */ return FALSE; @@ -50285,7 +50307,7 @@ static BOOL js_date_parse_isostring(const uint8_t *sp, int fields[9], BOOL *is_l /* parse the time zone offset if present: [+-]HH:mm or [+-]HHmm */ if (sp[p]) { *is_local = FALSE; - if (!string_get_timezone(sp, &p, &fields[8])) + if (!string_get_timezone(sp, &p, &fields[8], TRUE)) return FALSE; } /* error if extraneous characters */ @@ -50310,11 +50332,10 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is } *is_local = TRUE; - while (sp[p] != '\0') { - string_skip_spaces(sp, &p); + while (string_skip_spaces(sp, &p)) { p_start = p; if ((c = sp[p]) == '+' || c == '-') { - if (has_time && string_get_timezone(sp, &p, &fields[8])) { + if (has_time && string_get_timezone(sp, &p, &fields[8], FALSE)) { *is_local = FALSE; } else { p++; @@ -50347,7 +50368,7 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is has_year = TRUE; } else if (val < 1 || val > 31) { - fields[0] = val + (val < 100) * 1900; + fields[0] = val + (val < 100) * 1900 + (val < 50) * 100; has_year = TRUE; } else { if (num_index == 3) @@ -50358,19 +50379,73 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is } else if (string_get_month(sp, &p, &fields[1])) { has_mon = TRUE; + string_skip_until(sp, &p, "0123456789 -/("); } else if (c == 'Z') { *is_local = FALSE; p++; continue; } else - if (string_match(sp, &p, "GMT") || string_match(sp, &p, "UTC")) { + if (has_time && string_match(sp, &p, "PM")) { + if (fields[3] < 12) + fields[3] += 12; + continue; + } else + if (has_time && string_match(sp, &p, "AM")) { + if (fields[3] == 12) + fields[3] -= 12; + continue; + } else + if (string_match(sp, &p, "GMT") + || string_match(sp, &p, "UTC") + || string_match(sp, &p, "UT")) { + *is_local = FALSE; + continue; + } else + if (string_match(sp, &p, "EDT")) { + fields[8] = -4 * 60; + *is_local = FALSE; + continue; + } else + if (string_match(sp, &p, "EST") || string_match(sp, &p, "CDT")) { + fields[8] = -5 * 60; + *is_local = FALSE; + continue; + } else + if (string_match(sp, &p, "CST") || string_match(sp, &p, "MDT")) { + fields[8] = -6 * 60; + *is_local = FALSE; + continue; + } else + if (string_match(sp, &p, "MST") || string_match(sp, &p, "PDT")) { + fields[8] = -7 * 60; *is_local = FALSE; continue; + } else + if (string_match(sp, &p, "PST")) { + fields[8] = -8 * 60; + *is_local = FALSE; + continue; + } else + if (c == '(') { /* skip parenthesized phrase */ + int level = 0; + while ((c = sp[p]) != '\0') { + p++; + level += (c == '('); + level -= (c == ')'); + if (!level) + break; + } + if (level > 0) + return FALSE; + } else + if (c == ')') { + return FALSE; } else { + if (has_year + has_mon + has_time + num_index) + return FALSE; /* skip a word */ - string_skip_non_spaces(sp, &p); - continue; + string_skip_until(sp, &p, " -/("); } string_skip_separators(sp, &p); } @@ -50394,7 +50469,7 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is fields[2] = num[1]; } else if (has_mon) { - fields[0] = num[1] + (num[1] < 100) * 1900; + fields[0] = num[1] + (num[1] < 100) * 1900 + (num[1] < 50) * 100; fields[2] = num[0]; } else { fields[1] = num[0]; @@ -50402,7 +50477,7 @@ static BOOL js_date_parse_otherstring(const uint8_t *sp, int fields[9], BOOL *is } break; case 3: - fields[0] = num[2] + (num[2] < 100) * 1900; + fields[0] = num[2] + (num[2] < 100) * 1900 + (num[2] < 50) * 100; fields[1] = num[0]; fields[2] = num[1]; break; diff --git a/quickjs/tests/test_builtin.js b/quickjs/tests/test_builtin.js @@ -600,6 +600,14 @@ function test_date() assert((new Date("2020-01-01T01:01:01.9999Z")).toISOString(), "2020-01-01T01:01:01.999Z"); + assert(Date.UTC(2017), 1483228800000); + assert(Date.UTC(2017, 9), 1506816000000); + assert(Date.UTC(2017, 9, 22), 1508630400000); + assert(Date.UTC(2017, 9, 22, 18), 1508695200000); + assert(Date.UTC(2017, 9, 22, 18, 10), 1508695800000); + assert(Date.UTC(2017, 9, 22, 18, 10, 11), 1508695811000); + assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91), 1508695811091); + assert(Date.UTC(NaN), NaN); assert(Date.UTC(2017, NaN), NaN); assert(Date.UTC(2017, 9, NaN), NaN); @@ -609,14 +617,14 @@ function test_date() assert(Date.UTC(2017, 9, 22, 18, 10, 11, NaN), NaN); assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91, NaN), 1508695811091); - assert(Date.UTC(2017), 1483228800000); - assert(Date.UTC(2017, 9), 1506816000000); - assert(Date.UTC(2017, 9, 22), 1508630400000); - assert(Date.UTC(2017, 9, 22, 18), 1508695200000); - assert(Date.UTC(2017, 9, 22, 18, 10), 1508695800000); - assert(Date.UTC(2017, 9, 22, 18, 10, 11), 1508695811000); - assert(Date.UTC(2017, 9, 22, 18, 10, 11, 91), 1508695811091); - + // TODO: Fix rounding errors on Windows/Cygwin. + if (!(typeof os !== 'undefined' && ['win32', 'cygwin'].includes(os.platform))) { + // from test262/test/built-ins/Date/UTC/fp-evaluation-order.js + assert(Date.UTC(1970, 0, 1, 80063993375, 29, 1, -288230376151711740), 29312, + 'order of operations / precision in MakeTime'); + assert(Date.UTC(1970, 0, 213503982336, 0, 0, 0, -18446744073709552000), 34447360, + 'precision in MakeDate'); + } //assert(Date.UTC(2017 - 1e9, 9 + 12e9), 1506816000000); // node fails this assert(Date.UTC(2017, 9, 22 - 1e10, 18 + 24e10), 1508695200000); assert(Date.UTC(2017, 9, 22, 18 - 1e10, 10 + 60e10), 1508695800000);