Source: lib/offset.js

/**
 * JTSage-DateBox
 * @fileOverview Offset dates
 * @author J.T.Sage <jtsage+datebox@gmail.com>
 * @author {@link https://github.com/jtsage/jtsage-datebox/contributors|GitHub Contributors}
 * @license {@link https://github.com/jtsage/jtsage-datebox/blob/master/LICENSE.txt|MIT}
 * @version 5.2.0
 */


/**
 * This method offsets the current date. Do it in such a way that rollover can be prevented
 * 
 * @param  {string} mode Element to shift
 * @param  {number} amount Amount to shift +/-
 * @param  {boolean} update Set to false, will not update display
 */
JTSageDateBox._offset = function( oper, amount, update ) {
	// Compute a date/time offset.
	//   update = false to prevent controls refresh
	// 
	// The rollover option has no effect on year or meridiem.  It makes no sense with year, and
	// meridiem feels odd when allowed to rollover, so it's set to not.
	var testCurrent, condHigh, condLow, condMulti,
		w               = this,
		o               = w.options,
		operNum         = [ "y", "m", "d", "h", "i", "s" ].indexOf( oper ),
		lastDate        = 32 - w.theDate.copy([0],[0,0,32,13]).getDate(),
		thisYear        = [ // Only used when rollover prevented.
			31, 32 - w.theDate.copy([0],[0,1,32,13]).getDate(), 31,
			30, 31, 30,
			31, 31, 30,
			31, 30, 31
		],
		rolloverAllowed = ( oper !== "a" && ( oper === "y" ||
				typeof o.rolloverMode[oper] === "undefined" ||
				o.rolloverMode[oper] === true) );


	// Trap for un-set argument.
	if ( typeof update === "undefined" ) { update = true; }

	if ( oper === "y" && w.theDate.get( 1 ) === 1 && w.theDate.get( 2 ) === 29 ) {
		// Extreme edge case of altering year when date is set to february 29th (a leap year).

		// For this case, we do not check if the new year is a leap year, I'm willing to be a day 
		// off every 4 years or so.

		// This check is not based on rollover mode, it always happens.
		w.theDate.setD( 2, 28 );
	}
	
	if ( oper === "a" ) {

		// Rollover independant, handle meridiem specially.

		if ( amount % 2 !== 0 ) {

			// we modulus divide by 2, because if we move it twice, it's the same as before.
			// Otherwise, if it is PM, we move 12 hours back, if it's AM, we move 12 hours forward.

			testCurrent = ( w.theDate.get( 3 ) > 11 ) ? -12 : 12;

			w.theDate.adj( 3, testCurrent );
		}
	} else if ( rolloverAllowed ) {

		// Rollover allowed, or year operator, just do the update. (year is always ok)
		w.theDate.adj( operNum, amount );

	} else {

		// Rollover not allowed, need to do it piece by piece.

		switch ( oper ) {
			case "m" : // 12 months in a year, zero based
				condHigh  = 11;
				condLow   = 0;
				condMulti = 12;
				break;
			case "d" : // "lastDate" days in a year, one based
				condHigh  = lastDate;
				condLow   = 1;
				condMulti = lastDate;
				break;
			case "h" : // 24 hours in a day, zero based
				condHigh  = 23;
				condLow   = 0;
				condMulti = 24;
				break;
			case "i" : // 60 minutes in an hour, zero based
			case "s" : // 60 seconds in an hour, zero based
				condHigh  = 59;
				condLow   = 0;
				condMulti = 60;
				break;
		}

		// Get what the new value will be.
		testCurrent = w.theDate.get( operNum ) + amount;

		if ( testCurrent < condLow ) {

			// If it's less than a reasonable minimum, normalize it to be in the range
			testCurrent = ( testCurrent % condMulti ) + condMulti;

		} else if ( testCurrent > condHigh ) {

			// Same for higher than reasonable
			testCurrent = testCurrent % condMulti;

		}

		// Trap for month offset, when the date is very near the end of the month
		// and might make things move oddly.  
		//
		// For instance, May 31 -> offset 1 month previous -> April 30
		//  * note the date change as well, since April does not have 31 days.
		//  * this is done carfully, so that May 31 -> offset 2 month prev -> March 31
		if ( oper === "m" && w.theDate.get( 2 ) > thisYear[ testCurrent ] ) {
			w.theDate.setD( 2, thisYear[ testCurrent ] );
		}

		// Finally, update the value
		w.theDate.setD( operNum, testCurrent );
	}

	// If we wish to update the display, do so
	if ( update === true ) { w.refresh(); }

	// Immediate settting?  do so.
	if ( o.useImmediate ) { w._t( { method : "doset" } ); }

	// This fires when we change the calendar display, but don't set the date.
	if ( o.mode === "calbox" ) {
		w._t( {
			method             : "displayChange",
			selectedDate       : w.originalDate,
			shownDate          : w.theDate,
			thisChange         : oper,
			thisChangeAmount   : amount,
			gridStart          : w.getCalStartGrid(),
			gridEnd            : w.getCalEndGrid(),
			selectedInGrid     : w.isSelectedInCalGrid(),
			selectedInBounds   : w.isSelectedInBounds()
		});
	}
	
	// This is the listener event, to let know whatever might be listening that
	// and offset just occured.
	w._t( {
		method    : "offset",
		type      : oper,
		amount    : amount,
		newDate   : w.theDate
	} );
};

/**
 * Alter a date by the startOffset values.
 * 
 * @param  {object} date JavaScript date object
 * @return {object} JavaScript date object
 */
JTSageDateBox._startOffset = function(date) {
	var o = this.options;

	if ( o.startOffsetYears !== false ) {
		date.adj( 0, o.startOffsetYears );
	}
	if ( o.startOffsetMonths !== false ) {
		date.adj( 1, o.startOffsetMonths );
	}
	if ( o.startOffsetDays !== false ) {
		date.adj( 2, o.startOffsetDays );
	}
	return date;
};