| //------------------------------------------------------------------------------------------------------- |
| // Copyright (C) Microsoft. All rights reserved. |
| // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. |
| //------------------------------------------------------------------------------------------------------- |
| |
| var total = 0, accepted = 0, failed = 0; |
| |
| echo("////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////"); |
| echo("// Definitely valid ISO strings"); |
| echo(""); |
| |
| echo("// Auto-generated"); |
| echo(""); |
| initializeGenerateDateStrings(); |
| var yearDigits = 0, monthDigits = 0, dayDigits = 0, hourMinuteDigits = 0, secondDigits = 0, millisecondDigits = 0; |
| for (yearDigits = 4; yearDigits <= 6; yearDigits += 2) { |
| dayDigits = monthDigits = 0; |
| runGenerateTestWithValidTime(); |
| monthDigits = 2; |
| runGenerateTestWithValidTime(); |
| dayDigits = 2; |
| runGenerateTestWithValidTime(); |
| } |
| function runGenerateTestWithValidTime() { |
| millisecondDigits = secondDigits = hourMinuteDigits = 0; |
| runGenerateTest(); |
| hourMinuteDigits = 2; |
| runGenerateTest(); |
| secondDigits = 2; |
| runGenerateTest(); |
| millisecondDigits = 3; |
| runGenerateTest(); |
| } |
| |
| writeStats(); |
| |
| echo("////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////"); |
| echo("// Definitely invalid ISO strings"); |
| echo(""); |
| |
| echo("// Field value outside valid range"); |
| echo(""); |
| runTest("0001-00-01T01:01:01.001Z"); |
| runTest("0001-13-01T01:01:01.001Z"); |
| runTest("0001-01-00T01:01:01.001Z"); |
| runTest("0001-01-32T01:01:01.001Z"); |
| runTest("0001-01-01T25:01:01.001Z"); |
| runTest("0001-01-01T01:01:01.001+25:00"); |
| runTest("0001-01-01T01:60:01.001Z"); |
| runTest("0001-01-01T01:01:01.001+00:60"); |
| runTest("0001-01-01T01:01:60.001Z"); |
| |
| echo("// Time value outside valid range"); |
| echo(""); |
| runTest("-300000-01-01T01:01:01.001Z"); |
| runTest("+300000-01-01T01:01:01.001Z"); |
| |
| // Many other invalid ISO strings are tested in "potential cross-browser compatibility issues" section |
| |
| writeStats(); |
| |
| echo("////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////"); |
| echo("// Potential cross-browser compatilibity issues"); |
| echo(""); |
| |
| echo("// Leading and trailing whitespace, nulls, or non-whitespace non-nulls"); |
| echo(""); |
| var s = "0001-01-01T01:01:01.001Z"; |
| var spaceNulls = ["", "\0", "\t", "\n", "\v", "\f", "\r", " ", "\u00a0", "\u2028", "\u2029", "\ufeff"]; |
| for (var i = 0; i < spaceNulls.length; ++i) { |
| if (s !== "") { |
| runTest(spaceNulls[i] + s); |
| runTest(s + spaceNulls[i]); |
| } |
| runTest(s + spaceNulls[i] + "x"); |
| } |
| |
| echo("// Less and more digits per field"); |
| echo(""); |
| runTest("001-01-01T01:01:01.001Z"); |
| runTest("00001-01-01T01:01:01.001Z"); |
| runTest("0001-1-01T01:01:01.001Z"); |
| runTest("0001-001-01T01:01:01.001Z"); |
| runTest("0001-01-1T01:01:01.001Z"); |
| runTest("0001-01-001T01:01:01.001Z"); |
| runTest("0001-01-01T1:01:01.001Z"); |
| runTest("0001-01-01T001:01:01.001Z"); |
| runTest("0001-01-01T01:1:01.001Z"); |
| runTest("0001-01-01T01:001:01.001Z"); |
| runTest("0001-01-01T01:01:1.001Z"); |
| runTest("0001-01-01T01:01:001.001Z"); |
| runTest("0001-01-01T01:01:01.01Z"); |
| runTest("0001-01-01T01:01:01.0001Z"); |
| |
| echo("// Date-only forms with UTC offset"); |
| echo(""); |
| runTest("0001Z"); |
| runTest("0001-01Z"); |
| runTest("0001-01-01Z"); // note: this is rejected by the ISO parser as it should be, but it's accepted by the fallback parser |
| |
| echo("// Optionality of minutes"); |
| echo(""); |
| runTest("0001-01-01T01Z"); |
| runTest("0001-01-01T01:01:01.001+01"); |
| |
| echo("// Time-only forms"); |
| echo(""); |
| runTest("T01:01Z"); |
| runTest("T01:01:01Z"); |
| runTest("T01:01:01.001Z"); |
| |
| echo("// Field before missing optional field ending with separator"); |
| echo(""); |
| runTest("0001-"); |
| runTest("0001-01-"); |
| runTest("0001-T01:01:01.001Z"); |
| runTest("0001-01-T01:01:01.001Z"); |
| runTest("0001-01-01T01:01:Z"); |
| runTest("0001-01-01T01:01:01.Z"); |
| |
| echo("// Optionality and type of sign on years"); |
| echo(""); |
| runTest("+0001-01-01T01:01:01.001Z"); |
| runTest("-0001-01-01T01:01:01.001Z"); |
| runTest("010000-01-01T01:01:01.001Z"); |
| runTest("-000000-01-01T01:01:01.001Z"); |
| |
| echo("// Test support for zones without colons (DEVDIV2: 481975)"); |
| echo(""); |
| runTest("2012-02-22T03:08:26+0000"); |
| |
| writeStats(); |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| // Test-specific helpers |
| |
| function runTest(s) { |
| ++total; |
| echo(s); |
| safeCall(function () { |
| var iso = new Date(s); |
| var timeValue1 = iso.getTime(); |
| if (isNaN(timeValue1)) { |
| echo(iso); |
| } else { |
| iso = iso.toISOString(); |
| echo(iso); |
| var timeValue2 = new Date(iso).getTime(); |
| echo(timeValue1 + " " + (timeValue1 === timeValue2 ? "===" : "!==") + " " + timeValue2); |
| if (iso.indexOf("Invalid", 0) === -1) { |
| if (timeValue1 === timeValue2) |
| ++accepted; |
| else |
| ++failed; |
| } |
| } |
| }); |
| echo(""); |
| } |
| |
| function runGenerateTest() { |
| var s = |
| generateDateStrings( |
| yearDigits, |
| monthDigits, |
| dayDigits, |
| hourMinuteDigits, |
| hourMinuteDigits, |
| secondDigits, |
| millisecondDigits); |
| for (var i = 0; i < s.length; ++i) |
| runTest(s[i]); |
| } |
| |
| var signs, zones; |
| function initializeGenerateDateStrings() { |
| signs = ["+", "-"]; |
| zones = ["", "Z"]; |
| var zoneDigitCombinations = ["00", "01", "10"]; |
| for (var i = 0; i < zoneDigitCombinations.length; ++i) |
| for (var j = 0; j < zoneDigitCombinations.length; ++j) |
| for (var k = 0; k < signs.length; ++k) |
| zones.push(signs[k] + zoneDigitCombinations[i] + ":" + zoneDigitCombinations[j]); |
| } |
| |
| // Generates date strings in the following format: |
| // date format: "[+|-]YYYYYY[-MM[-DD]]" |
| // separator: "T| " |
| // time format: "HH:mm[:ss[.sss]]" |
| // time zone: "Z|(+|-)HH:mm" |
| // - The separator is required only if both the date and time portions are included in the string. |
| // - Zero-padding is optional |
| // - Positive sign (+) is optional when the year is nonnegative |
| // - Negative sign (-) is optional when the year is zero |
| // - Time zone is optional |
| // |
| // The function will return an array of strings to test against, based on the parameters. |
| function generateDateStrings( |
| yearDigits, // number of digits to include for the year (0-6), 0 - exclude the year (monthDigits must also be 0) |
| monthDigits, // number of digits to include for the month (0-2), 0 - exclude the month (dayDigits must also be 0) |
| dayDigits, // number of digits to include for the day (0-2), 0 - exclude the day |
| hourDigits, // number of digits to include for the hour (0-2), 0 - exclude the hour (minuteDigits must also be 0) |
| minuteDigits, // number of digits to include for the minute (0-2), 0 - exclude the minute (hourDigits and secondDigits must also be 0) |
| secondDigits, // number of digits to include for the second (0-2), 0 - exclude the second (millisecondDigits must also be 0) |
| millisecondDigits) // number of digits to include for the millisecond (0-3), 0 - exclude the millisecond |
| { |
| if (yearDigits === 0 && monthDigits !== 0 || |
| monthDigits === 0 && dayDigits !== 0 || |
| hourDigits === 0 && minuteDigits !== 0 || |
| minuteDigits === 0 && (hourDigits !== 0 || secondDigits !== 0) || |
| secondDigits === 0 && millisecondDigits !== 0 || |
| yearDigits === 0 && (hourDigits === 0 || minuteDigits === 0)) |
| return []; |
| |
| var s = [""]; |
| |
| if (yearDigits !== 0) { |
| appendDigits(s, yearDigits, true); |
| if (monthDigits !== 0) { |
| append(s, ["-"]); |
| appendDigits(s, monthDigits, false); |
| if (dayDigits !== 0) { |
| append(s, ["-"]); |
| appendDigits(s, dayDigits, false); |
| } |
| } |
| } |
| |
| if (hourDigits !== 0 && minuteDigits !== 0) { |
| append(s, ["T"]); |
| appendDigits(s, hourDigits, true); |
| append(s, [":"]); |
| appendDigits(s, minuteDigits, true); |
| if (secondDigits !== 0) { |
| append(s, [":"]); |
| appendDigits(s, secondDigits, true); |
| if (millisecondDigits !== 0) { |
| append(s, ["."]); |
| appendDigits(s, millisecondDigits, true); |
| } |
| } |
| } |
| |
| if (yearDigits !== 0 && hourDigits !== 0 && minuteDigits !== 0) |
| s = applyToEach(s, zones, function (str, zone) { return str + zone; }); |
| if(yearDigits === 6) { |
| s = |
| applyToEach( |
| s, |
| signs, |
| function (str, sign) { |
| if(sign === "-" && str.length >= 6 && str.substring(0, 6) === "000000") |
| return undefined; // "-000000" is not allowed |
| return sign + str; |
| }); |
| } |
| |
| return s; |
| } |
| |
| // Appends interesting combinations of n digits to the string array |
| function appendDigits(a, n, includeZero) { |
| var d = []; |
| switch (n) { |
| case 0: |
| break; |
| |
| case 1: |
| if (includeZero) |
| d.push("0"); |
| d.push("1"); |
| append(a, d); |
| break; |
| |
| case 3: |
| case 6: |
| if (n === 3) |
| d.push("010"); |
| else |
| d.push("010010"); |
| |
| default: |
| var z = zeroes(n - 1); |
| if (includeZero) |
| d.push(z + "0"); |
| d.push(z + "1"); |
| d.push("1" + z); |
| append(a, d); |
| break; |
| } |
| } |
| |
| // Returns a string of n zeroes |
| function zeroes(n) { |
| var s = ""; |
| while (n-- > 0) |
| s += "0"; |
| return s; |
| } |
| |
| // Appends patterns to the string array. The array is extended to acommodate the number of patterns, and the patterns are |
| // repeated to acommodate the length of the array. |
| function append(a, p) { |
| extend(a, p.length); |
| for (var i = 0; i < a.length; ++i) |
| a[i] += p[i % p.length]; |
| } |
| |
| // Applies the function 'f' to each combination of elements in 'a' and 'p'. 'f' will receive the element of 'a' on which it |
| // should apply the pattern from 'p' and it should return the modified string. The string returned by 'f' will be pushed onto a |
| // new array, which will be returned. |
| function applyToEach(a, p, f) { |
| var a2 = []; |
| for(var i = 0; i < a.length; ++i) { |
| for(var j = 0; j < p.length; ++j) { |
| var transformed = f(a[i], p[j]); |
| if(transformed !== undefined) |
| a2.push(transformed); |
| } |
| } |
| return a2; |
| } |
| |
| // Extends an array to have length n, by copying the last element as necessary |
| function extend(a, n) { |
| var originalLength = a.length; |
| for (var i = originalLength; i < n; ++i) |
| a.push(a[originalLength - 1]); |
| } |
| |
| function writeStats() { |
| echo("Total: " + total); |
| echo("Accepted: " + accepted); |
| echo("Rejected: " + (total - accepted - failed)); |
| echo("Failed: " + failed); |
| echo(""); |
| failed = accepted = total = 0; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| // General helpers |
| |
| function toString(o, quoteStrings) { |
| switch (o) { |
| case null: |
| case undefined: |
| return "" + o; |
| } |
| |
| switch (typeof o) { |
| case "boolean": |
| case "number": |
| return "" + o; |
| |
| case "string": |
| { |
| var hex = "0123456789abcdef"; |
| var s = ""; |
| for (var i = 0; i < o.length; ++i) { |
| var c = o.charCodeAt(i); |
| if (c === 0) |
| s += "\\0"; |
| else if (c >= 0x20 && c < 0x7f) |
| s += quoteStrings && o.charAt(i) === "\"" ? "\\\"" : o.charAt(i); |
| else if (c <= 0xff) |
| s += "\\x" + hex.charAt((c >> 4) & 0xf) + hex.charAt(c & 0xf); |
| else |
| s += "\\u" + hex.charAt((c >> 12) & 0xf) + hex.charAt((c >> 8) & 0xf) + hex.charAt((c >> 4) & 0xf) + hex.charAt(c & 0xf); |
| } |
| if (quoteStrings) |
| s = "\"" + s + "\""; |
| return s; |
| } |
| |
| case "object": |
| case "function": |
| break; |
| |
| default: |
| return "<unknown type '" + typeof o + "'>"; |
| } |
| |
| if (o instanceof Array) { |
| var s = "["; |
| for (var i = 0; i < o.length; ++i) { |
| if (i) |
| s += ", "; |
| |
| s += this.toString(o[i], true); |
| } |
| return s + "]"; |
| } |
| if (o instanceof Error) |
| return o.name + ": " + o.message; |
| if (o instanceof RegExp) |
| return o.toString() + (o.lastIndex === 0 ? "" : " (lastIndex: " + o.lastIndex + ")"); |
| return "" + o; |
| } |
| |
| function echo(o) { |
| var s = this.toString(o); |
| try { |
| document.write(s + "<br/>"); |
| } catch (ex) { |
| try { |
| WScript.Echo(s); |
| } catch (ex2) { |
| print(s); |
| } |
| } |
| } |
| |
| function safeCall(f) { |
| var args = []; |
| for (var a = 1; a < arguments.length; ++a) |
| args.push(arguments[a]); |
| try { |
| return f.apply(this, args); |
| } catch (ex) { |
| echo(ex); |
| } |
| } |