var util = require('./util'), moment = require('moment'); /** * @constructor TimeStep * The class TimeStep is an iterator for dates. You provide a start date and an * end date. The class itself determines the best scale (step size) based on the * provided start Date, end Date, and minimumStep. * * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * * Alternatively, you can set a scale by hand. * After creation, you can initialize the class by executing first(). Then you * can iterate from the start date to the end date via next(). You can check if * the end date is reached with the function hasNext(). After each step, you can * retrieve the current date via getCurrent(). * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, * days, to years. * * Version: 1.2 * * @param {Date} [start] The start date, for example new Date(2010, 9, 21) * or new Date(2010, 9, 21, 23, 45, 00) * @param {Date} [end] The end date * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ TimeStep = function(start, end, minimumStep) { // variables this.current = new Date(); this._start = new Date(); this._end = new Date(); this.autoScale = true; this.scale = TimeStep.SCALE.DAY; this.step = 1; // initialize the range this.setRange(start, end, minimumStep); }; /// enum scale TimeStep.SCALE = { MILLISECOND: 1, SECOND: 2, MINUTE: 3, HOUR: 4, DAY: 5, WEEKDAY: 6, MONTH: 7, YEAR: 8 }; /** * Set a new range * If minimumStep is provided, the step size is chosen as close as possible * to the minimumStep but larger than minimumStep. If minimumStep is not * provided, the scale is set to 1 DAY. * The minimumStep should correspond with the onscreen size of about 6 characters * @param {Date} start The start date and time. * @param {Date} end The end date and time. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds */ TimeStep.prototype.setRange = function(start, end, minimumStep) { if (!(start instanceof Date) || !(end instanceof Date)) { //throw "No legal start or end date in method setRange"; return; } this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); if (this.autoScale) { this.setMinimumStep(minimumStep); } }; /** * Set the range iterator to the start date. */ TimeStep.prototype.first = function() { this.current = new Date(this._start.valueOf()); this.roundToMinor(); }; /** * Round the current date to the first minor date value * This must be executed once when the current date is set to start Date */ TimeStep.prototype.roundToMinor = function() { // round to floor // IMPORTANT: we have no breaks in this switch! (this is no bug) //noinspection FallthroughInSwitchStatementJS switch (this.scale) { case TimeStep.SCALE.YEAR: this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); this.current.setMonth(0); case TimeStep.SCALE.MONTH: this.current.setDate(1); case TimeStep.SCALE.DAY: // intentional fall through case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); case TimeStep.SCALE.HOUR: this.current.setMinutes(0); case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds } if (this.step != 1) { // round down to the first minor value that is a multiple of the current step size switch (this.scale) { case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; case TimeStep.SCALE.WEEKDAY: // intentional fall through case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; default: break; } } }; /** * Check if the there is a next step * @return {boolean} true if the current date has not passed the end date */ TimeStep.prototype.hasNext = function () { return (this.current.valueOf() <= this._end.valueOf()); }; /** * Do the next step */ TimeStep.prototype.next = function() { var prev = this.current.valueOf(); // Two cases, needed to prevent issues with switching daylight savings // (end of March and end of October) if (this.current.getMonth() < 6) { switch (this.scale) { case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; case TimeStep.SCALE.HOUR: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) var h = this.current.getHours(); this.current.setHours(h - (h % this.step)); break; case TimeStep.SCALE.WEEKDAY: // intentional fall through case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; default: break; } } else { switch (this.scale) { case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; case TimeStep.SCALE.WEEKDAY: // intentional fall through case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; default: break; } } if (this.step != 1) { // round down to the correct major value switch (this.scale) { case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; case TimeStep.SCALE.WEEKDAY: // intentional fall through case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; case TimeStep.SCALE.YEAR: break; // nothing to do for year default: break; } } // safety mechanism: if current time is still unchanged, move to the end if (this.current.valueOf() == prev) { this.current = new Date(this._end.valueOf()); } }; /** * Get the current datetime * @return {Date} current The current date */ TimeStep.prototype.getCurrent = function() { return this.current; }; /** * Set a custom scale. Autoscaling will be disabled. * For example setScale(SCALE.MINUTES, 5) will result * in minor steps of 5 minutes, and major steps of an hour. * * @param {TimeStep.SCALE} newScale * A scale. Choose from SCALE.MILLISECOND, * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, * SCALE.YEAR. * @param {Number} newStep A step size, by default 1. Choose for * example 1, 2, 5, or 10. */ TimeStep.prototype.setScale = function(newScale, newStep) { this.scale = newScale; if (newStep > 0) { this.step = newStep; } this.autoScale = false; }; /** * Enable or disable autoscaling * @param {boolean} enable If true, autoascaling is set true */ TimeStep.prototype.setAutoScale = function (enable) { this.autoScale = enable; }; /** * Automatically determine the scale that bests fits the provided minimum step * @param {Number} minimumStep The minimum step size in milliseconds */ TimeStep.prototype.setMinimumStep = function(minimumStep) { if (minimumStep == undefined) { return; } var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); var stepMonth = (1000 * 60 * 60 * 24 * 30); var stepDay = (1000 * 60 * 60 * 24); var stepHour = (1000 * 60 * 60); var stepMinute = (1000 * 60); var stepSecond = (1000); var stepMillisecond= (1); // find the smallest step that is larger than the provided minimumStep if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} }; /** * Snap a date to a rounded value. The snap intervals are dependent on the * current scale and step. * @param {Date} date the date to be snapped */ TimeStep.prototype.snap = function(date) { if (this.scale == TimeStep.SCALE.YEAR) { var year = date.getFullYear() + Math.round(date.getMonth() / 12); date.setFullYear(Math.round(year / this.step) * this.step); date.setMonth(0); date.setDate(0); date.setHours(0); date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.MONTH) { if (date.getDate() > 15) { date.setDate(1); date.setMonth(date.getMonth() + 1); // important: first set Date to 1, after that change the month. } else { date.setDate(1); } date.setHours(0); date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.DAY || this.scale == TimeStep.SCALE.WEEKDAY) { //noinspection FallthroughInSwitchStatementJS switch (this.step) { case 5: case 2: date.setHours(Math.round(date.getHours() / 24) * 24); break; default: date.setHours(Math.round(date.getHours() / 12) * 12); break; } date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.HOUR) { switch (this.step) { case 4: date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; default: date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; } date.setSeconds(0); date.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.MINUTE) { //noinspection FallthroughInSwitchStatementJS switch (this.step) { case 15: case 10: date.setMinutes(Math.round(date.getMinutes() / 5) * 5); date.setSeconds(0); break; case 5: date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; default: date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; } date.setMilliseconds(0); } else if (this.scale == TimeStep.SCALE.SECOND) { //noinspection FallthroughInSwitchStatementJS switch (this.step) { case 15: case 10: date.setSeconds(Math.round(date.getSeconds() / 5) * 5); date.setMilliseconds(0); break; case 5: date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; default: date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; } } else if (this.scale == TimeStep.SCALE.MILLISECOND) { var step = this.step > 5 ? this.step / 2 : 1; date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); } }; /** * Check if the current value is a major value (for example when the step * is DAY, a major value is each first day of the MONTH) * @return {boolean} true if current date is major, else false. */ TimeStep.prototype.isMajor = function() { switch (this.scale) { case TimeStep.SCALE.MILLISECOND: return (this.current.getMilliseconds() == 0); case TimeStep.SCALE.SECOND: return (this.current.getSeconds() == 0); case TimeStep.SCALE.MINUTE: return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); // Note: this is no bug. Major label is equal for both minute and hour scale case TimeStep.SCALE.HOUR: return (this.current.getHours() == 0); case TimeStep.SCALE.WEEKDAY: // intentional fall through case TimeStep.SCALE.DAY: return (this.current.getDate() == 1); case TimeStep.SCALE.MONTH: return (this.current.getMonth() == 0); case TimeStep.SCALE.YEAR: return false; default: return false; } }; /** * Returns formatted text for the minor axislabel, depending on the current * date and the scale. For example when scale is MINUTE, the current time is * formatted as "hh:mm". * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMinor = function(date) { if (date == undefined) { date = this.current; } switch (this.scale) { case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); case TimeStep.SCALE.SECOND: return moment(date).format('s'); case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); case TimeStep.SCALE.DAY: return moment(date).format('D'); case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); default: return ''; } }; /** * Returns formatted text for the major axislabel, depending on the current * date and the scale. For example when scale is MINUTE, the major scale is * hours, and the hour will be formatted as "hh". * @param {Date} [date] custom date. if not provided, current date is taken */ TimeStep.prototype.getLabelMajor = function(date) { if (date == undefined) { date = this.current; } //noinspection FallthroughInSwitchStatementJS switch (this.scale) { case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); case TimeStep.SCALE.MINUTE: case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); case TimeStep.SCALE.WEEKDAY: case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); case TimeStep.SCALE.YEAR: return ''; default: return ''; } }; // exports module.exports = exports = TimeStep;