Skip to content

Commit

Permalink
Improved string and dates util modules (#343)
Browse files Browse the repository at this point in the history
* Adds custom date format to dates.parse()
* Adds toggle for lenient parsing
* Adds strings.isDate() check
* Removed unnecessary import of binary module
* Improved docs and added example
  • Loading branch information
botic authored Jul 11, 2016
1 parent f934191 commit 4108e33
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 80 deletions.
198 changes: 131 additions & 67 deletions modules/ringo/utils/dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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 <code>SimpleDateFormat</code>.
* 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)
Expand All @@ -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 <code>java.util.Locale</code> 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 <code>NaN</code> for unrecognizable strings
* @see <a href="http://tools.ietf.org/html/rfc3339">RFC 3339: Date and Time on the Internet: Timestamps</a>
* @see <a href="http://www.w3.org/TR/NOTE-datetime">W3C Note: Date and Time Formats</a>
* @see <a href="https://es5.github.io/#x15.9.4.2">ES5 Date.parse()</a>
*/
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;
Expand Down
60 changes: 47 additions & 13 deletions modules/ringo/utils/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
* <a href="http://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html">
* java.text.SimpleDateFormat
* </a>.
* 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));
};
14 changes: 14 additions & 0 deletions test/ringo/utils/dates_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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());
Expand Down
Loading

0 comments on commit 4108e33

Please sign in to comment.