Skip to content

Commit

Permalink
Merge pull request #6 from shepherdsam/change/timezone-locale
Browse files Browse the repository at this point in the history
Add timezone and locale support to parseDateTimeField
  • Loading branch information
shepherdsam authored Feb 8, 2017
2 parents 0a7e1d1 + c5568e5 commit a7f8d98
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 43 deletions.
100 changes: 79 additions & 21 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
'use strict';

var _ = require('lodash');
var _ = require('lodash'),
moment = require('moment-timezone'),
SugarDate = require('sugar-date').Date;
require('sugar-date/locales');

var Utils = {};

var DEFAULT_FLATTENED_DELIMITER = '__';
var DEFAULT_FLATTENED_ARRAY_DELIMITER = '_+_';
var TIMEZONE_TO_LOCALE = {
'Europe/London': 'en-GB'
};

var humanize = function(str) {
/* istanbul ignore if */
Expand All @@ -25,30 +31,28 @@ var formatParsedObject = function(type, input, valid, parsed) {
};

/* istanbul ignore next */
Utils._getCurrentDate = function() {
Utils._getCurrentDate = function(options) {
// Makes unit testing possible, by allowing this
// function to be mocked.
return new Date();
return SugarDate.create('now', options);
};

/* istanbul ignore next */
Utils._getFutureDate = function(str) {
Utils._getFutureDate = function(str, options) {
// Makes unit testing possible, by allowing this
// function to be mocked.
return Date.future(str);
return SugarDate.create(str, _.assign({}, options, {future: true}));
};

Utils.activateDateParser = function() {
// Add support for the 'enhanced' date object.
// This augments the Date prototype with extra methods,
// and (importantly) allows use to use the Sugar.js
// date parsing algorithms.
// In an ideal world, we wouldn't augment the prototype,
// but our hands are ties if we want to use the date parsing
// feature that Sugar.js gives us.
// Perhaps one day this can be extracted and added as a
// moment.js plugin.
require('sugar-date');

// v2 of SugarDate supports optionally extending.
// This is still here to support legacy dependencies
SugarDate.extend();
};

/**
Expand Down Expand Up @@ -405,6 +409,9 @@ Utils.cloneTerse = function(input) {
* - Utils.parseDateTimeField('may 25th of next year')
* - Utils.parseDateTimeField('2014-01-18 09:30:00')
* - Utils.parseDateTimeField('2014-01-18 09:30:00 -0400')
* - Utils.parseDateTimeField('2014-01-18 09:30:00', {locale: 'en-GB'})
* - Utils.parseDateTimeField('2014-01-18 09:30:00', {timezone: 'America/Chicago'})
* - Utils.parseDateTimeField('2014-01-18 09:30:00', {fromUTC: true, setUTC: true})
* - Utils.parseDateTimeField('in 2 days')
* - Utils.parseDateTimeField('5 minutes from now')
* - Utils.parseDateTimeField('2014-01-18 09:30:00 -0400 +2d +30m')
Expand All @@ -417,24 +424,50 @@ Utils.cloneTerse = function(input) {
* - -4d -6h will reduce the parsed date by 4 days and 6 hours
*
* @param {String} field the date string to parse.
* @param {Object} options passed through to SugarDate.create
* @return {Object} an object containing the parsed date, or the passed field if it was not a String.
*/
Utils.parseDateTimeField = function(field) {
Utils.parseDateTimeField = function(field, options) {
var getRtnObject = function(parsed, isValid) {
return formatParsedObject('date', field, isValid, parsed);
};

var parsedDate;
// Copy options
options = _.assign({}, options);

// Default timezone
var timezone = options.timezone || 'UTC';
var isUTC = timezone.toLowerCase() === 'utc';

// Lookup Locale. Check options, then look up based on timezone. Default to 'en'
options.locale = options.locale || TIMEZONE_TO_LOCALE[timezone] || 'en';

// If invalid
if (moment.tz.zone(timezone) === null) {
Utils.Logger.warn('Utils.parseDateTimeField - Timezone "' + timezone + '" is invalid. Assuming UTC');
timezone = 'UTC';
}

// This special case is here to get around a bug in SugarDate. Issue: #582
options.fromUTC = field === 'now' ? false : isUTC;

// Help SugarDate be aware of timezones
var previousNewDateInternal;
if (timezone) {
previousNewDateInternal = SugarDate.getOption('newDateInternal');
SugarDate.setOption('newDateInternal', function() {
return moment().tz(timezone).toDate();
});
}

// Ensure we have Date superpowers
Utils.activateDateParser();
var parsedDate;

if(_.isDate(field)) {
parsedDate = field;

} else if(!_.isString(field)) {
// Just create a date from the passed value.
parsedDate = Date.create(field);
parsedDate = SugarDate.create(field, options);

} else {
// Regex for parsing a offset modifier.
Expand Down Expand Up @@ -482,18 +515,43 @@ Utils.parseDateTimeField = function(field) {
// Otherwise, parse the string to create a date in the future.
parsedDate =
withoutOffsetModifiers === '' && hasOffsetModifier ?
Utils._getCurrentDate() :
Utils._getFutureDate(withoutOffsetModifiers);
Utils._getCurrentDate(options) :
Utils._getFutureDate(withoutOffsetModifiers, options);

if(parsedDate.isValid() && hasOffsetModifier && offsetSecs) {
if(SugarDate.isValid(parsedDate) && hasOffsetModifier && offsetSecs) {
// Apply the offset modifier.
// If it is negative, it will subtract.
parsedDate.addSeconds(offsetSecs);
SugarDate.addSeconds(parsedDate, offsetSecs);
}
}

if (previousNewDateInternal) {
SugarDate.setOption('newDateInternal', previousNewDateInternal);
}

var dateMoment;
if (SugarDate.isValid(parsedDate)) {
var hasTZOffset = new RegExp('[+-]{1}[0-9]{2}:?[0-9]{2}').test(field);

// Convert to string. Important to remove offset/tz info (if none was provided originally)
// or moment will ignore the passed in tz in later step.
var tzFormatStr = hasTZOffset ? '{Z}' : '';
var dateString = SugarDate.format(SugarDate.setUTC(parsedDate, isUTC), '{yyyy}-{MM}-{dd}T{HH}:{mm}:{ss}' + tzFormatStr);
Utils.Logger.debug('Utils.parseDateTimeField: dateString:' + dateString);

// This parses the dateString in the timezone specified.
dateMoment = moment.tz(dateString, timezone);
// Utils.Logger.debug('Utils.parseDateTimeField: moment:' + dateMoment);
}

// parsedDate will always be a date at this point.
return getRtnObject(parsedDate, parsedDate.isValid());
var rtnObject = getRtnObject(parsedDate, SugarDate.isValid(parsedDate));

// Add the dateMoment to the return object
if (dateMoment) {
rtnObject.moment = dateMoment;
}
return rtnObject;
};

/**
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"bluebird": "^3.4.1",
"lodash": "^4.14.2",
"sugar-date": "1.5.1"
"moment-timezone": "^0.5.11",
"sugar-date": "^2.0.4"
}
}
104 changes: 83 additions & 21 deletions spec/specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

var util = require('util'),
Utils = require('../lib/index.js'),
_ = require('lodash');
_ = require('lodash'),
SugarDate = require('sugar-date').Date;

describe('Utils', function() {
it('should convert an array to a hashtable', function() {
Expand Down Expand Up @@ -764,7 +765,8 @@ describe('Utils', function() {
type: 'date',
input: input,
valid: true,
parsed: expected
parsed: expected,
moment: jasmine.any(Object)
});
};

Expand All @@ -776,14 +778,15 @@ describe('Utils', function() {
valid: false,
parsed: jasmine.any(Date)
});
expect(parsedDate.parsed.isValid()).toBe(false);
expect(SugarDate.isValid(parsedDate.parsed)).toBe(false);
};

beforeEach(function() {
epoch = new Date(0);
// spyOn(Utils, '_getFutureDate').and.callThrough();
spyOn(Utils, '_getFutureDate').and.callFake(function(str) {
var parsed = Date.future(str);
return parsed.isValid() ? epoch : parsed;
var parsed = SugarDate.create(str, {future: true});
return SugarDate.isValid(parsed) ? epoch : parsed;
});
});

Expand All @@ -799,7 +802,7 @@ describe('Utils', function() {

it('should parse a datetime string', function() {
expectValidDate('tomorrow', epoch);
expect(Utils._getFutureDate).toHaveBeenCalledWith('tomorrow');
expect(Utils._getFutureDate).toHaveBeenCalledWith('tomorrow', {locale: 'en', fromUTC: true});
});

it('should return as invalid if no string is passed', function() {
Expand All @@ -814,10 +817,77 @@ describe('Utils', function() {
expectInvalidDate('invalid');
});

it('should handle a integer', function() {
epoch = new Date(1485410400000);
expectValidDate(1485410400000, new Date(1485410400000));
});

it('should handle a Date', function() {
epoch = new Date(1485410400000);
expectValidDate(new Date(1485410400000), new Date(1485410400000));
});

it('should support newDateInternal as an option', function() {
epoch = SugarDate.create('2017-02-06 1am', {fromUTC: true});
var fn = function() {
var d = new Date();
d.setTime(d.getTime() + (60 * 60 * 1000));
return d;
};
var dateObj = Utils.parseDateTimeField('2017-02-06', {newDateInternal: fn});
expect(dateObj.parsed.toISOString()).toEqual('2017-02-06T01:00:00.000Z');
});

describe('Locales', function() {
it('parses a date without locale', function() {
Utils.parseDateTimeField('now +1d');
expect(Utils._getFutureDate).toHaveBeenCalledWith('now', {locale: 'en', fromUTC: true});

epoch = SugarDate.create('1/11/2017', {fromUTC: true});
var d = Utils.parseDateTimeField('1/11/2017');
expect(d.parsed.toISOString()).toEqual('2017-01-11T00:00:00.000Z');
});

it('parses a date with locale', function() {
Utils.parseDateTimeField('now +1d', {locale: 'en-GB'});
expect(Utils._getFutureDate).toHaveBeenCalledWith('now', {locale: 'en-GB', fromUTC: true});

epoch = SugarDate.create('1/11/2017', {fromUTC: true});
var d = Utils.parseDateTimeField('11/1/2017');
expect(d.parsed.toISOString()).toEqual('2017-01-11T00:00:00.000Z');
});

});

describe('Timezones', function() {
it('parses a date without timezone', function() {
Utils.parseDateTimeField('1/11/2017 +1d');
expect(Utils._getFutureDate).toHaveBeenCalledWith('1/11/2017', {locale: 'en', fromUTC: true});

epoch = SugarDate.create('1/12/2017', {fromUTC: true});
var d = Utils.parseDateTimeField('1/12/2017');
expect(d.moment.toISOString()).toEqual('2017-01-12T00:00:00.000Z');
});

it('parses a date with timezone', function() {
Utils.parseDateTimeField('1/11/2017 +1d', {timezone: 'America/New_York'});
expect(Utils._getFutureDate).toHaveBeenCalledWith('1/11/2017', {
locale: 'en',
timezone: 'America/New_York',
fromUTC: false
});

epoch = SugarDate.create('1/12/2017');
var d = Utils.parseDateTimeField('1/12/2017', {timezone: 'America/New_York'});
expect(d.moment.toISOString()).toEqual('2017-01-12T05:00:00.000Z');
});

});

describe('Offset Modifiers', function() {
it('should strip offset modifier from string to parse', function() {
Utils.parseDateTimeField('now +1d');
expect(Utils._getFutureDate).toHaveBeenCalledWith('now');
expect(Utils._getFutureDate).toHaveBeenCalledWith('now', {locale: 'en', fromUTC: true});
});

describe('Increment', function() {
Expand Down Expand Up @@ -919,17 +989,17 @@ describe('Utils', function() {
});

it('should not apply the offset modifier if it is 0', function() {
spyOn(epoch, 'addSeconds');
Utils.parseDateTimeField('now');
expect(epoch.addSeconds)
spyOn(SugarDate, 'addSeconds');
// console.log(Utils.parseDateTimeField('now'));
expect(SugarDate.addSeconds)
.not.toHaveBeenCalled();
});

it('should not apply the offset modifier if the date is invalid', function() {
epoch = new Date('invalid');
spyOn(epoch, 'addSeconds');
// epoch = new Date('invalid');
spyOn(SugarDate, 'addSeconds');
Utils.parseDateTimeField('some_invalid_date +40h');
expect(epoch.addSeconds)
expect(SugarDate.addSeconds)
.not.toHaveBeenCalled();
});

Expand All @@ -942,10 +1012,6 @@ describe('Utils', function() {
expectValidDate('2013-02-08 09:30-0100', epoch);
});

it('should work with the YYYY-MM-DD HHZZ format when the offset is Z', function() {
expectValidDate('2013-02-08 09Z', epoch);
});

it('should work with the YYYY-MM-DD HH:mm:ss.SSSZ format', function() {
expectValidDate('2013-02-08 09:30:26.123+07:00', epoch);
});
Expand All @@ -966,10 +1032,6 @@ describe('Utils', function() {
expectValidDate('2013-02-08 09:30-0100 +1d - 12h', new Date(43200000));
});

it('should work with the YYYY-MM-DD HHZZ format when the offset is Z and modifiers', function() {
expectValidDate('2013-02-08 09Z +1d - 12h', new Date(43200000));
});

it('should work with the YYYY-MM-DD HH:mm:ss.SSSZ format and modifiers', function() {
expectValidDate('2013-02-08 09:30:26.123+07:00 +1d - 12h', new Date(43200000));
});
Expand Down

0 comments on commit a7f8d98

Please sign in to comment.