public/lib/date-service.js (172 lines of code) (raw):

/** * Date service providing date formatting and parsing functionality taking * timezone settings into account. */ // 3rd party dependencies import angular from 'angular'; // moment-timezone should require moment as dependency import moment from 'moment-timezone'; import timezoneData from './date-service/timezone-data.json'; import 'sugar'; // local libs import './location-service'; // Load in timezone data for moment // Only the necessary data required has been manually extracted from: // moment-timezone/data/packed/2014e.json // This will need to be manually updated when a new version of the data // is available in moment-timezone // TODO: extract latest data automatically in build process moment.tz.load(timezoneData); // maps locations to their timezone data var timezones = { 'LON': { tzKey: 'Europe/London', locale: 'en-UK' }, 'NYC': { tzKey: 'America/New_York', locale: 'en-US' }, 'SYD': { tzKey: 'Australia/Sydney', locale: 'en-AU' }, 'SFO': { tzKey: 'America/Los_Angeles', locale: 'en-US' } }; function getTimezoneForLocation(location) { if (!timezones[location]) { throw new Error('Unknown location: ' + location); } return timezones[location].tzKey; } function getTimezoneLocaleForLocation(location) { if (!timezones[location]) { throw new Error('Unknown location: ' + location); } return timezones[location].locale; } angular.module('wfDateService', ['wfLocationService']) .factory('wfDateParser', ['wfLocationService', 'wfFormatDateTimeFilter', function (wfLocationService, wfFormatDateTimeFilter) { class DateParser { /** * Parses a Date from an input string. * * @param {string} input string to parse. * @param {string} locationKey * * @return {Date} */ parseDate(input, locationKey) { return this.localiseDateTime( this.normaliseDateString(input, locationKey), locationKey ).toDate(); } /** * Normalises a date input string to YYYY-MM-DD HH:mm, accepting * natural language inputs such as "today", "tuesday next week 18:00" */ normaliseDateString(input, locationKey) { locationKey = locationKey || wfLocationService.getCurrentLocationKey(); var parsed; if (moment.isMoment(input)) { parsed = input; } else { // Parse input using sugar.js catering for natural language, ie: "next week" parsed = moment(Date.create(input, getTimezoneLocaleForLocation(locationKey))); } if (!parsed.isValid()) { throw new Error('Could not parse date: ' + input); } return parsed.format('YYYY-MM-DD HH:mm'); } now() { return new Date(); } /** * Retrieve a date range between two explicit dates. * * @returns {{from: Date, to: Date}} */ createRange(from, until) { return { from: moment(from).toDate(), until: moment(until).toDate() }; } /** * Retrieve a range from the start of a day, til the start of the next. * * @returns {{from: Date, to: Date}} */ createDayRange(day) { var dayStart = moment(day).startOf('day'); return this.createRange(dayStart, dayStart.clone().add(1, 'days')); } /** * Minuses a second from a "from" date * This allows us to filter content dates within a range without exlcuding those equal to the "from" value. * The content API removes any data with a date value equal to the "from" value when querying. * * @param {Date} fromDate * * @return {Date} */ getFromDate(fromDate){ if(!!fromDate && moment(fromDate).isValid()){ return moment(fromDate).subtract(1, 'seconds').toDate(); } return null; } /** * Parses a date range using simple natural language strings * (eg: "tomorrow") and explicit standard date formatted date strings, * such as in YYYY-MM-DD. * * @returns {{from: Date, to: Date}} */ parseRangeFromString(input, locationKey) { var now = this.localiseDateTime(this.now(), locationKey).clone(); if (!input) { return {}; } // Parses some natural language dates - doesn't use sugar as I'd like // to remove it as a dependency one day as it modifies the global Date object if (input == 'today') { const migrationDate = moment.tz("2017-04-25 11:00", "Europe/London"); const fixedDate = moment.tz("2017-04-26 11:00", "Europe/London"); if (now > fixedDate) { return this.createDayRange(now); } else { var dayStart = migrationDate; return this.createRange(dayStart, dayStart.clone().add(1, 'days')); } } else if (input == 'tomorrow') { return this.createDayRange(now.add(1, 'days')); } else if (input == 'weekend') { var weekendStart = now.day(6).startOf('day'); return this.createRange(weekendStart, weekendStart.clone().add(2, 'days')); } else if (input == 'yesterday') { return this.createDayRange(now.subtract(1, 'days')); } else if (input === 'last24') { return this.createRange(now.clone().subtract(24, 'hours'), now); } else if (input === 'last48') { return this.createRange(now.clone().subtract(48, 'hours'), now); } else { var parsed = this.localiseDateTime(input, locationKey); if (!parsed.isValid()) { throw new Error('Could not parse date: ' + input); } return this.createDayRange(parsed); } } /** * Retrieve either string representation of 'day', or day object * * @returns {String, or <Date>} */ parseQueryString(date) { if (!date) return undefined; if (date === 'today' || date === 'tomorrow' || date === 'weekend') { return date; } else if (moment(date, ["YYYY-MM-DD"]).isValid()) { return moment(date, ["YYYY-MM-DD"]); } } /** * Formats date to string if passed a date object, otherwise return what is passsed * * @returns {String, or <Date>} */ setQueryString(date) { if (!date) return undefined; var dateFormat = wfFormatDateTimeFilter(date, "YYYY-MM-DD"); if (dateFormat !== 'Invalid date') { return dateFormat; } else return date; } /** * Retrieves an Array of day starts for the localised week. * * @returns {Array.<Date>} */ getDaysThisWeek(locationKey) { var today = this.localiseDateTime(this.now(), locationKey).startOf('day'), choices = [ moment(today.toDate()) ]; for (var i = 1; i < 7; i++) { choices.push(moment(today.clone().add(i, 'days').toDate())); } return choices; } localiseDateTime(dateValue, location) { location = location || wfLocationService.getCurrentLocationKey(); return moment.tz(dateValue, getTimezoneForLocation(location)); } } return new DateParser(); }]) /** * Localises a date input value to the specified value. * Stateless and requires a "location" to be passed to filter. * @return {moment} */ .filter('wfLocaliseDateTime', [function () { return function (dateValue, location) { if (!dateValue) { return dateValue; } if (!location) { console.warn('DEPRECATED. Specifying a location parameter is required'); return dateValue; } // Must return a moment object, as JS date seems to lose timezone info. return moment.tz(dateValue, getTimezoneForLocation(location)); }; }]) .filter('wfFormatDateTime', [function () { return function wfFormatDateTime(dateValue, dateFormat = 'ddd D MMM YYYY, HH:mm') { if (!dateValue) { return ''; } if (dateFormat === 'long') { dateFormat = 'dddd D MMMM YYYY, HH:mm z'; } if (dateFormat === 'date') { dateFormat = 'dddd D MMMM YYYY'; } if (dateFormat === 'ISO8601') { return dateValue.toISOString(); } return moment(dateValue).format(dateFormat); }; }]);