Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved string and dates util modules #343

Merged
merged 5 commits into from
Jul 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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