diff --git a/lib/index.js b/lib/index.js index 9e20ec8..247b6ec 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,17 @@ 'use strict'; -var _ = require('lodash'); -var SugarDate = require('sugar-date').Date; +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 */ @@ -406,6 +410,7 @@ Utils.cloneTerse = function(input) { * - 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') @@ -426,12 +431,33 @@ Utils.parseDateTimeField = function(field, options) { var getRtnObject = function(parsed, isValid) { return formatParsedObject('date', field, isValid, parsed); }; - options = options || {}; + // 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 (_.isFunction(options.newDateInternal)) { + if (timezone) { previousNewDateInternal = SugarDate.getOption('newDateInternal'); - SugarDate.setOption('newDateInternal', options.newDateInternal); + SugarDate.setOption('newDateInternal', function() { + return moment().tz(timezone).toDate(); + }); } var parsedDate; @@ -503,8 +529,29 @@ Utils.parseDateTimeField = function(field, options) { 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, SugarDate.isValid(parsedDate)); + var rtnObject = getRtnObject(parsedDate, SugarDate.isValid(parsedDate)); + + // Add the dateMoment to the return object + if (dateMoment) { + rtnObject.moment = dateMoment; + } + return rtnObject; }; /** diff --git a/package.json b/package.json index 6563073..7861e32 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "bluebird": "^3.4.1", "lodash": "^4.14.2", + "moment-timezone": "^0.5.11", "sugar-date": "^2.0.4" } } diff --git a/spec/specs.js b/spec/specs.js index 9b63fde..e583656 100644 --- a/spec/specs.js +++ b/spec/specs.js @@ -765,7 +765,8 @@ describe('Utils', function() { type: 'date', input: input, valid: true, - parsed: expected + parsed: expected, + moment: jasmine.any(Object) }); }; @@ -782,6 +783,7 @@ describe('Utils', function() { beforeEach(function() { epoch = new Date(0); + // spyOn(Utils, '_getFutureDate').and.callThrough(); spyOn(Utils, '_getFutureDate').and.callFake(function(str) { var parsed = SugarDate.create(str, {future: true}); return SugarDate.isValid(parsed) ? epoch : parsed; @@ -800,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() { @@ -839,7 +841,7 @@ describe('Utils', function() { describe('Locales', function() { it('parses a date without locale', function() { Utils.parseDateTimeField('now +1d'); - expect(Utils._getFutureDate).toHaveBeenCalledWith('now', {}); + expect(Utils._getFutureDate).toHaveBeenCalledWith('now', {locale: 'en', fromUTC: true}); epoch = SugarDate.create('1/11/2017', {fromUTC: true}); var d = Utils.parseDateTimeField('1/11/2017'); @@ -848,18 +850,44 @@ describe('Utils', function() { it('parses a date with locale', function() { Utils.parseDateTimeField('now +1d', {locale: 'en-GB'}); - expect(Utils._getFutureDate).toHaveBeenCalledWith('now', {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() { @@ -962,7 +990,7 @@ describe('Utils', function() { it('should not apply the offset modifier if it is 0', function() { spyOn(SugarDate, 'addSeconds'); - Utils.parseDateTimeField('now'); + // console.log(Utils.parseDateTimeField('now')); expect(SugarDate.addSeconds) .not.toHaveBeenCalled(); });