diff --git a/modules/ringo/utils/dates.js b/modules/ringo/utils/dates.js index f22c80502..467aa3572 100644 --- a/modules/ringo/utils/dates.js +++ b/modules/ringo/utils/dates.js @@ -92,18 +92,24 @@ export( * dates.format(y2k, "yyyy-MM-dd HH:mm:ss z", "de", "GMT-10"); */ function format(date, format, locale, timezone) { - if (!format) + if (!format) { return date.toString(); + } + if (typeof locale == "string") { locale = new java.util.Locale(locale); } + if (typeof timezone == "string") { timezone = java.util.TimeZone.getTimeZone(timezone); } - var sdf = locale ? new java.text.SimpleDateFormat(format, locale) - : new java.text.SimpleDateFormat(format); - if (timezone && timezone != sdf.getTimeZone()) + + var sdf = locale ? new java.text.SimpleDateFormat(format, locale) : new java.text.SimpleDateFormat(format); + + if (timezone && timezone != sdf.getTimeZone()) { sdf.setTimeZone(timezone); + } + return sdf.format(date); } @@ -682,11 +688,24 @@ function fromUTCDate(year, month, date, hour, minute, second, millisecond) { } /** - * Parse a string to a date. - * The date string follows the format specified for timestamps - * on the internet described in RFC 3339. + * Parse a string to a date using date and time patterns from Java's SimpleDateFormat. + * If no format is provided, the default follows RFC 3339 for timestamps on the internet. + * The parser matches the pattern against the string with lenient parsing: Even if the input is not + * strictly in the form of the pattern, but can be parsed with heuristics, then the parse succeeds. + * + * @example // parses input string as local time: + * // Wed Jun 29 2016 12:11:10 GMT+0200 (MESZ) + * let pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + * dates.parse("2016-06-29T12:11:10.001", pattern); + * + * // enforces UTC on the input date string + * // Wed Jun 29 2016 14:11:10 GMT+0200 (MESZ) + * dates.parse("2016-06-29T12:11:10.001", pattern, "en", "UTC"); * - * @example // Fri Jan 01 2016 01:00:00 GMT+0100 (MEZ) + * // accepting month names in German + * dates.parse("29. Juni 2016", "dd. MMM yyyy", "de", "UTC"); + * + * // Fri Jan 01 2016 01:00:00 GMT+0100 (MEZ) * dates.parse("2016"); * * // Sat Aug 06 2016 02:00:00 GMT+0200 (MESZ) @@ -699,85 +718,130 @@ function fromUTCDate(year, month, date, hour, minute, second, millisecond) { * dates.parse("2016-08-06T16:04:30-06"); * * @param {String} str The date string. + * @param {String} format (optional) a specific format pattern for the parser + * @param {String|java.util.Locale} locale (optional) the locale as java.util.Locale object or + * an ISO-639 alpha-2 code (e.g. "en", "de") as string + * @param {String|java.util.TimeZone} timezone (optional) the timezone as java TimeZone + * object or an abbreviation such as "PST", a full name such as "America/Los_Angeles", + * or a custom ID such as "GMT-8:00". If the id is not provided, the default timezone is used. + * If the timezone id is provided but cannot be understood, the "GMT" timezone is used. + * @param {Boolean} lenient (optional) disables lenient parsing if set to false. * @returns {Date|NaN} a date representing the given string, or NaN for unrecognizable strings * @see RFC 3339: Date and Time on the Internet: Timestamps * @see W3C Note: Date and Time Formats * @see ES5 Date.parse() */ -function parse(str) { +function parse(str, format, locale, timezone, lenient) { var date; - // first check if the native parse method can parse it - var elapsed = Date.parse(str); - if (!isNaN(elapsed)) { - date = new Date(elapsed); - } else { - var match = str.match(/^(?:(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?)?(?:T(\d{1,2}):(\d{2})(?::(\d{2}(?:\.\d+)?))?(Z|(?:[+-]\d{1,2}(?::(\d{2}))?))?)?$/); - var date; - if (match && (match[1] || match[7])) { // must have at least year or time - var year = parseInt(match[1], 10) || 0; - var month = (parseInt(match[2], 10) - 1) || 0; - var day = parseInt(match[3], 10) || 1; + // if a format is provided, use java.text.SimpleDateFormat + if (typeof format === "string") { + if (typeof locale === "string") { + locale = new java.util.Locale(locale); + } - date = new Date(Date.UTC(year, month, day)); + if (typeof timezone === "string") { + timezone = java.util.TimeZone.getTimeZone(timezone); + } + + var sdf = locale ? new java.text.SimpleDateFormat(format, locale) : new java.text.SimpleDateFormat(format); + + if (timezone && timezone != sdf.getTimeZone()) { + sdf.setTimeZone(timezone); + } - // Check if the given date is valid - if (date.getUTCMonth() != month || date.getUTCDate() != day) { + try { + // disables lenient mode, switches to strict parsing + if (lenient === false) { + sdf.setLenient(false); + } + + var ppos = new java.text.ParsePosition(0); + var javaDate = sdf.parse(str, ppos); + + // strict parsing & error during parsing --> return NaN + if (lenient === false && ppos.getErrorIndex() >= 0) { return NaN; } - // optional time - if (match[4] !== undefined) { - var type = match[7]; - var hours = parseInt(match[4], 10); - var minutes = parseInt(match[5], 10); - var secFrac = parseFloat(match[6]) || 0; - var seconds = secFrac | 0; - var milliseconds = Math.round(1000 * (secFrac - seconds)); - - // Checks if the time string is a valid time. - var validTimeValues = function(hours, minutes, seconds) { - if (hours === 24) { - if (minutes !== 0 || seconds !== 0 || milliseconds !== 0) { + date = javaDate != null ? new Date(javaDate.getTime()) : NaN; + } catch (e if e.javaException instanceof java.text.ParseException) { + date = NaN; + } + } else { + // no date format provided, fall back to RFC 3339 + // first check if the native parse method can parse it + var elapsed = Date.parse(str); + if (!isNaN(elapsed)) { + date = new Date(elapsed); + } else { + var match = str.match(/^(?:(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?)?(?:T(\d{1,2}):(\d{2})(?::(\d{2}(?:\.\d+)?))?(Z|(?:[+-]\d{1,2}(?::(\d{2}))?))?)?$/); + var date; + if (match && (match[1] || match[7])) { // must have at least year or time + var year = parseInt(match[1], 10) || 0; + var month = (parseInt(match[2], 10) - 1) || 0; + var day = parseInt(match[3], 10) || 1; + + date = new Date(Date.UTC(year, month, day)); + + // Check if the given date is valid + if (date.getUTCMonth() != month || date.getUTCDate() != day) { + return NaN; + } + + // optional time + if (match[4] !== undefined) { + var type = match[7]; + var hours = parseInt(match[4], 10); + var minutes = parseInt(match[5], 10); + var secFrac = parseFloat(match[6]) || 0; + var seconds = secFrac | 0; + var milliseconds = Math.round(1000 * (secFrac - seconds)); + + // Checks if the time string is a valid time. + var validTimeValues = function (hours, minutes, seconds) { + if (hours === 24) { + if (minutes !== 0 || seconds !== 0 || milliseconds !== 0) { + return false; + } + } else { return false; } - } else { - return false; - } - return true; - }; - - // Use UTC or local time - if (type !== undefined) { - date.setUTCHours(hours, minutes, seconds, milliseconds); - if (date.getUTCHours() != hours || date.getUTCMinutes() != minutes || date.getUTCSeconds() != seconds) { - if(!validTimeValues(hours, minutes, seconds, milliseconds)) { - return NaN; + return true; + }; + + // Use UTC or local time + if (type !== undefined) { + date.setUTCHours(hours, minutes, seconds, milliseconds); + if (date.getUTCHours() != hours || date.getUTCMinutes() != minutes || date.getUTCSeconds() != seconds) { + if (!validTimeValues(hours, minutes, seconds, milliseconds)) { + return NaN; + } } - } - - // Check offset - if (type !== "Z") { - var hoursOffset = parseInt(type, 10); - var minutesOffset = parseInt(match[8]) || 0; - var offset = -1000 * (60 * (hoursOffset * 60) + minutesOffset * 60); - // check maximal timezone offset (24 hours) - if (Math.abs(offset) >= 86400000) { - return NaN; + // Check offset + if (type !== "Z") { + var hoursOffset = parseInt(type, 10); + var minutesOffset = parseInt(match[8]) || 0; + var offset = -1000 * (60 * (hoursOffset * 60) + minutesOffset * 60); + + // check maximal timezone offset (24 hours) + if (Math.abs(offset) >= 86400000) { + return NaN; + } + date = new Date(date.getTime() + offset); } - date = new Date(date.getTime() + offset); - } - } else { - date.setHours(hours, minutes, seconds, milliseconds); - if (date.getHours() != hours || date.getMinutes() != minutes || date.getSeconds() != seconds) { - if(!validTimeValues(hours, minutes, seconds, milliseconds)) { - return NaN; + } else { + date.setHours(hours, minutes, seconds, milliseconds); + if (date.getHours() != hours || date.getMinutes() != minutes || date.getSeconds() != seconds) { + if (!validTimeValues(hours, minutes, seconds, milliseconds)) { + return NaN; + } } } } + } else { + date = NaN; } - } else { - date = NaN; } } return date; diff --git a/modules/ringo/utils/strings.js b/modules/ringo/utils/strings.js index ba921f95f..13fe02310 100644 --- a/modules/ringo/utils/strings.js +++ b/modules/ringo/utils/strings.js @@ -14,20 +14,20 @@ * $Date: 2007-12-13 13:21:48 +0100 (Don, 13 Dez 2007) $ */ -var ANUMPATTERN = /[^a-zA-Z0-9]/; -var APATTERN = /[^a-zA-Z]/; -var NUMPATTERN = /[^0-9]/; -var FILEPATTERN = /[^a-zA-Z0-9-_\. ]/; -var HEXPATTERN = /[^a-fA-F0-9]/; +const ANUMPATTERN = /[^a-zA-Z0-9]/; +const APATTERN = /[^a-zA-Z]/; +const NUMPATTERN = /[^0-9]/; +const FILEPATTERN = /[^a-zA-Z0-9-_\. ]/; +const HEXPATTERN = /[^a-fA-F0-9]/; // Email RegExp contributed by Scott Gonzalez (http://projects.scottsplayground.com/email_address_validation/) // licensed unter MIT license - http://www.opensource.org/licenses/mit-license.php -var EMAILPATTERN = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i; +const EMAILPATTERN = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i; // URL RegExp contributed by Diego Perini // licensed unter MIT license - https://gist.github.com/dperini/729294 // Copyright (c) 2010-2013 Diego Perini (http://www.iport.it) -var URLPATTERN = java.util.regex.Pattern.compile("^" + +const URLPATTERN = java.util.regex.Pattern.compile("^" + // protocol identifier "(?:(?:https?|ftp)://)" + // user:pass authentication @@ -62,11 +62,11 @@ var URLPATTERN = java.util.regex.Pattern.compile("^" + // Copyright (c) 2014 Chris O'Hara cohara87@gmail.com // licensed unter MIT license - https://github.com/chriso/validator.js/blob/master/LICENSE -var INT = /^(?:[-+]?(?:0|[1-9][0-9]*))$/; -var FLOAT = /^(?:[-+]?(?:[0-9]*))(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$/; +const INT = /^(?:[-+]?(?:0|[1-9][0-9]*))$/; +const FLOAT = /^(?:[-+]?(?:[0-9]*))(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$/; -var {Binary, ByteArray, ByteString} = require('binary'); -var base64; +const base64 = require('ringo/base64'); +const dates = require('ringo/utils/dates'); /** * @fileoverview Adds useful methods to the JavaScript String type. @@ -115,7 +115,8 @@ export('isDateFormat', 'isUpperCase', 'isLowerCase', 'isInt', - 'isFloat'); + 'isFloat', + 'isDate'); /** * Checks if a date format pattern is correct and a valid string to create a @@ -625,7 +626,6 @@ function count(string, pattern) { * @example strings.b64encode("foob"); // --> "Zm9vYg==" */ function b64encode(string, encoding) { - if (!base64) base64 = require('ringo/base64'); return base64.encode(string, encoding); } @@ -964,3 +964,37 @@ function isInt(string) { function isFloat(string) { return string !== '' && FLOAT.test(string) && !INT.test(string); }; + +/** + * Returns true if the string is matching a date format. + * By default the parser matches the pattern against the string with lenient parsing: Even if the input + * is not strictly in the form of the pattern, but can be parsed with heuristics, then the parse succeeds. + * For details on the format pattern, see + * + * java.text.SimpleDateFormat + * . + * If no format is provided, the check uses a patter matching RFC 3339 (timestamps on the internet). + * @param {String} string + * @param {String} format (optional) date format pattern + * @param {String|java.util.Locale} locale (optional) the locale as java Locale object or + * lowercase two-letter ISO-639 code (e.g. "en") + * @param {String|java.util.TimeZone} timezone (optional) the timezone as java TimeZone + * object or an abbreviation such as "PST", a full name such as "America/Los_Angeles", + * or a custom ID such as "GMT-8:00". If the id is not provided, the default timezone + * is used. If the timezone id is provided but cannot be understood, the "GMT" timezone + * is used. + * @param {Boolean} lenient (optional) disables lenient parsing if set to false. + * @returns {Boolean} true if valid date string, false otherwise + * @example // true + * strings.isDate("2016"); + * strings.isDate("01-01-2016", "MM-dd-yyyy"); + * + * // true, since lenient parsing + * strings.isDate("20-40-2016", "MM-dd-yyyy"); + * + * // false, since strict parsing with lenient=false + * strings.isDate("20-40-2016", "MM-dd-yyyy", "en", "UTC", false); + */ +function isDate(string, format, locale, timezone, lenient) { + return !isNaN(dates.parse(string, format, locale, timezone, lenient)); +}; diff --git a/test/ringo/utils/dates_test.js b/test/ringo/utils/dates_test.js index 74f0e5b24..ed3065b1a 100644 --- a/test/ringo/utils/dates_test.js +++ b/test/ringo/utils/dates_test.js @@ -725,10 +725,15 @@ exports.testParse = function() { assert.isNaN(dates.parse("asdf")); assert.isFalse(dates.parse("asdf") instanceof Date, "parse should return NaN and not an invalid Date"); assert.isNaN(dates.parse("2010-")); + assert.isNaN(dates.parse("2010-", "yyyy-MM-dd")); assert.isFalse(dates.parse("2010-") instanceof Date, "parse should return NaN and not an invalid Date"); assert.isNaN(dates.parse("2010-99")); + assert.isNaN(dates.parse("2010-99", "yyyy-MM-dd")); assert.isFalse(dates.parse("2010-99") instanceof Date, "parse should return NaN and not an invalid Date"); assert.isNaN(dates.parse("2010-01-99")); + assert.isNotNaN(dates.parse("2010-01-99", "yyyy-MM-dd")); + assert.isNotNaN(dates.parse("2010-01-99", "yyyy-MM-dd", null, null, true)); + assert.isNaN(dates.parse("2010-01-99", "yyyy-MM-dd", null, null, false)); assert.isFalse(dates.parse("2010-01-99") instanceof Date, "parse should return NaN and not an invalid Date"); assert.isNaN(dates.parse("2010-01-01T24:59Z")); assert.isFalse(dates.parse("2010-01-01T24:59Z") instanceof Date, "parse should return NaN and not an invalid Date"); @@ -739,6 +744,15 @@ exports.testParse = function() { assert.isNaN(dates.parse("2010-01-01T23:00-25:00")); assert.isFalse(dates.parse("2010-01-01T23:00-25:00") instanceof Date, "parse should return NaN and not an invalid Date"); + // java date format tests + assert.strictEqual(dates.parse("2016-06-29T12:11:10.001", "yyyy-MM-dd'T'HH:mm:ss.SSS").getTime(), (new Date(2016,05,29,12,11,10,1)).getTime()); + assert.strictEqual(dates.parse("2016-06-29T12:11:10.001", "yyyy-MM-dd'T'HH:mm:ss.SSS", "en", "UTC").getTime(), 1467202270001); + assert.strictEqual(dates.parse("2016-06-29T12:11:10.001-01:00", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "en", "UTC").getTime(), 1467205870001); + assert.strictEqual(dates.parse("29. Juni 2016", "dd. MMM yyyy", "de", "UTC").getTime(), 1467158400000); + + // this might be a JDK bug?! Time zone given, but not in pattern --> should return NaN, but doesn't + assert.isNotNaN(dates.parse("2016-06-30T12:11:10.001-24:00", "yyyy-MM-dd'T'HH:mm:ss.SSS", "en", "UTC", false)); + // Check for not NaN // FIXME no exact checks because of local time... assert.isNotNaN(dates.parse("2010-01-01T01:01").getTime()); diff --git a/test/ringo/utils/strings_test.js b/test/ringo/utils/strings_test.js index 910ddd413..98d4f993f 100644 --- a/test/ringo/utils/strings_test.js +++ b/test/ringo/utils/strings_test.js @@ -435,6 +435,25 @@ exports.testY64decode = function() { assert.strictEqual("????", strings.y64decode("Pz8_Pw--", "UTF-8")); }; +exports.testIsDate = function() { + assert.isTrue(strings.isDate("1.1.2016", "d.M.yyyy")); + assert.isTrue(strings.isDate("1.1.16", "d.M.yy")); + assert.isTrue(strings.isDate("01.01.2016", "dd.MM.yyyy")); + assert.isTrue(strings.isDate("10.10.2016", "d.M.yyyy")); + assert.isTrue(strings.isDate("10.10.16", "d.M.yy")); + assert.isTrue(strings.isDate("29. Juni 2016", "dd. MMM yyyy", "de", "UTC")); + assert.isTrue(strings.isDate("2016-06-29T12:11:10.001Zasdfasdf", "yyyy-MM-dd'T'HH:mm:ss.SSS", "en", "UTC")); + assert.isTrue(strings.isDate("31.09.16", "d.M.yy")); + assert.isTrue(strings.isDate("32.10.16", "d.M.yy")); + assert.isTrue(strings.isDate("1.13.16", "d.M.yy")); + + assert.isFalse(strings.isDate("31.09.16", "d.M.yy", null, null, false)); + assert.isFalse(strings.isDate("a.b.c", "d.M.yy")); + assert.isFalse(strings.isDate("32.10.16", "d.M.yy", null, null, false)); + assert.isFalse(strings.isDate("1.13.16", "d.M.yy", null, null, false)); + assert.isFalse(strings.isDate("29. June 2016", "dd. MMM yyyy", "de", "UTC")); +}; + if (require.main === module) { require('system').exit(require("test").run(module.id)); } \ No newline at end of file