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

Change/timezone locale #6

Merged
merged 7 commits into from
Feb 8, 2017
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
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