|
|
- var moment = require('../module/moment');
- var DateUtil = require('./DateUtil');
-
- /**
- * @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
- */
- function TimeStep(start, end, minimumStep, hiddenDates) {
- // 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);
-
- // hidden Dates options
- this.switchedDay = false;
- this.switchedMonth = false;
- this.switchedYear = false;
- this.hiddenDates = hiddenDates;
- if (hiddenDates === undefined) {
- this.hiddenDates = [];
- }
- }
-
- // enum scale
- TimeStep.SCALE = {
- MILLISECOND: 1,
- SECOND: 2,
- MINUTE: 3,
- HOUR: 4,
- DAY: 5,
- WEEKDAY: 6,
- MONTH: 7,
- YEAR: 8
- };
-
- // array with lower case scale names
- TimeStep.SCALE_INDEX = {};
- Object.keys(TimeStep.SCALE).forEach(function (key) {
- var index = TimeStep.SCALE[key];
- TimeStep.SCALE_INDEX[index] = key.toLowerCase();
- });
-
- // Time formatting
- TimeStep.FORMAT = {
- minorLabels: {
- 'millisecond':'SSS',
- 'second': 's',
- 'minute': 'HH:mm',
- 'hour': 'HH:mm',
- 'weekday': 'ddd D',
- 'day': 'D',
- 'month': 'MMM',
- 'year': 'YYYY'
- },
- majorLabels: {
- 'millisecond':'HH:mm:ss',
- 'second': 'D MMMM HH:mm',
- 'minute': 'ddd D MMMM',
- 'hour': 'ddd D MMMM',
- 'weekday': 'MMMM YYYY',
- 'day': 'MMMM YYYY',
- 'month': 'YYYY',
- 'year': ''
- }
- };
-
- /**
- * 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";
- }
-
- 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());
- }
-
- DateUtil.stepOverHiddenDates(this, prev);
- };
-
-
- /**
- * 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 b = asc + ds;
-
- 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.
- * @return {Date} snappedDate
- */
- TimeStep.prototype.snap = function(date) {
- var clone = new Date(date.valueOf());
-
- if (this.scale == TimeStep.SCALE.YEAR) {
- var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
- clone.setFullYear(Math.round(year / this.step) * this.step);
- clone.setMonth(0);
- clone.setDate(0);
- clone.setHours(0);
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.MONTH) {
- if (clone.getDate() > 15) {
- clone.setDate(1);
- clone.setMonth(clone.getMonth() + 1);
- // important: first set Date to 1, after that change the month.
- }
- else {
- clone.setDate(1);
- }
-
- clone.setHours(0);
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.DAY) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 5:
- case 2:
- clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
- default:
- clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
- }
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.WEEKDAY) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 5:
- case 2:
- clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
- default:
- clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
- }
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.HOUR) {
- switch (this.step) {
- case 4:
- clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
- default:
- clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
- }
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- } else if (this.scale == TimeStep.SCALE.MINUTE) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 15:
- case 10:
- clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
- clone.setSeconds(0);
- break;
- case 5:
- clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
- default:
- clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
- }
- clone.setMilliseconds(0);
- }
- else if (this.scale == TimeStep.SCALE.SECOND) {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 15:
- case 10:
- clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
- clone.setMilliseconds(0);
- break;
- case 5:
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
- default:
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
- }
- }
- else if (this.scale == TimeStep.SCALE.MILLISECOND) {
- var step = this.step > 5 ? this.step / 2 : 1;
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
- }
-
- return clone;
- };
-
- /**
- * 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() {
- if (this.switchedYear == true) {
- this.switchedYear = false;
- switch (this.scale) {
- case TimeStep.SCALE.YEAR:
- case TimeStep.SCALE.MONTH:
- case TimeStep.SCALE.WEEKDAY:
- case TimeStep.SCALE.DAY:
- case TimeStep.SCALE.HOUR:
- case TimeStep.SCALE.MINUTE:
- case TimeStep.SCALE.SECOND:
- case TimeStep.SCALE.MILLISECOND:
- return true;
- default:
- return false;
- }
- }
- else if (this.switchedMonth == true) {
- this.switchedMonth = false;
- switch (this.scale) {
- case TimeStep.SCALE.WEEKDAY:
- case TimeStep.SCALE.DAY:
- case TimeStep.SCALE.HOUR:
- case TimeStep.SCALE.MINUTE:
- case TimeStep.SCALE.SECOND:
- case TimeStep.SCALE.MILLISECOND:
- return true;
- default:
- return false;
- }
- }
- else if (this.switchedDay == true) {
- this.switchedDay = false;
- switch (this.scale) {
- case TimeStep.SCALE.MILLISECOND:
- case TimeStep.SCALE.SECOND:
- case TimeStep.SCALE.MINUTE:
- case TimeStep.SCALE.HOUR:
- return true;
- default:
- return false;
- }
- }
-
- 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);
- 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;
- }
-
- var scaleName = TimeStep.SCALE_INDEX[this.scale];
- var format = TimeStep.FORMAT.minorLabels[scaleName];
- return (format && format.length > 0) ? moment(date).format(format) : '';
- };
-
- /**
- * Returns formatted text for the major axis label, 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;
- }
-
- var scaleName = TimeStep.SCALE_INDEX[this.scale];
- var format = TimeStep.FORMAT.majorLabels[scaleName];
- return (format && format.length > 0) ? moment(date).format(format) : '';
- };
-
- module.exports = TimeStep;
|