/** @module caltime/timespan
*
* @copyright Michael McCarthy <michael.mccarthy@ieee.org> 2017
* @license MIT
*/
'use strict';
/* dependencies */
const _ = require('lodash');
const modconst = require('./constants');
/* constants */
/* maximum value of the hour of the day */
const MAX_HOUR_OF_DAY = 23;
/* maximum value of minutes past the hour */
const MAX_MINUTES = 59;
/* maximum value of seconds in the arguments inSeconds and inDurationSecs. */
const MAX_SECONDS = 59;
/* maximum value of milliseconds permitted in the arguments inMilliseconds
* and inDurationMs. */
const MAX_MILLISECONDS = 999;
/**
* Functional constructor which creates an instance of a TimeSpan.
* Do not use with the 'new' operator.
* Each TimeSpan object is immutable. The inclusive end time of a timespan
* cannot exceed 23:59:59:999 i.e. the exclusive end time cannot exceed
* 12 midnight (00:00:00:000 of the following day).
* The total duration of a timespan is equal to:
* inDurationMins + inDurationSecs + inDurationMs
* @param {number} inHours Integer. Represents the 'hour of day' component
* of the start time of the timespan. Valid values are 0-23.
* An error is thrown if the integer is not specified or is not
* within the valid range.
* @param {number} inMinutes Integer. Represents the minutes component
* of the start time of the timespan. Valid values are 0-59.
* An error is thrown if the integer is not specified or is not within
* the valid range.
* @param {number} inSeconds Integer. Represents the seconds component
* of the start time of the timespan. Valid values are 0-59.
* An error is thrown if the integer is not specified or is not within
* the valid range.
* @param {number} inMilliseconds Integer. Represents the milliseconds component
* of the start time of the timespan. Valid values are 0-999.
* An error is thrown if the integer is not specified or is not within
* the valid range.
* @param {number} inDurationMins Positive integer representing the duration of
* the timespan in [minutes]. An error is thrown if this argument is not
* specified or it is not a valid, positive integer. Valid range is 0-1440.
* @param {number} [inDurationSecs] Positive integer representing the [seconds]
* component of the duration. An error is thrown if this argument is not
* specified or it is not a valid, positive integer. Valid range is 0-59.
* @param {number} [inDurationMs] Positive integer representing the
* [milliseconds] component of the duration. An error is thrown if this argument
* is not specified or it is not a valid, positive integer.
* Valid range is 0-999.
* @return {object} A new instance of the TimeSpan object.
*/
let timeSpan = function timeSpanCtor(inHours,
inMinutes,
inSeconds,
inMilliseconds,
inDurationMins,
inDurationSecs=0,
inDurationMs=0) {
/* private ******************************************************************/
/* new instance of object */
const that = {};
/* hour when the timespan begins during the day. */
const hours = inHours;
/* minutes after the hour when the timespan begins. */
const minutes = inMinutes;
/* seconds component of the time the timespan begins. */
const seconds = inSeconds;
/* milliseconds component of the time the timespan begins. */
const milliseconds = inMilliseconds;
/* duration of the timespan in [minutes] */
const durationMins = inDurationMins;
/* [seconds] component of the duration of the timespan */
const durationSecs = inDurationSecs;
/* [milliseconds] component of the duration of the timespan */
const durationMs = inDurationMs;
// check values are within permitted ranges
if (_.isInteger(inHours) === false
|| inHours < 0
|| inHours > MAX_HOUR_OF_DAY) {
throw new Error('Invalid inHours argument.');
};
if (_.isInteger(inMinutes) === false
|| inMinutes < 0
|| inMinutes > MAX_MINUTES) {
throw new Error('Invalid inMinutes argument.');
};
if (_.isInteger(inSeconds) === false
|| inSeconds < 0
|| inSeconds > MAX_SECONDS) {
throw new Error('Invalid inSeconds argument.');
};
if (_.isInteger(inMilliseconds) === false
|| inMilliseconds < 0
|| inMilliseconds > MAX_MILLISECONDS) {
throw new Error('Invalid inMilliseconds argument.');
};
if (_.isInteger(inDurationMins) === false
|| inDurationMins < 0
|| inDurationMins > modconst.MAX_MINS_PER_DAY) {
throw new Error('Invalid inDurationMins argument.');
};
if (_.isInteger(inDurationSecs)
&& (inDurationSecs < 0
|| inDurationSecs > MAX_SECONDS)) {
throw new Error('Invalid inDurationSecs argument.');
};
if (_.isInteger(inDurationMs)
&& (inDurationMs < 0
|| inDurationMs > MAX_MILLISECONDS)) {
throw new Error('Invalid inDurationMs argument.');
};
// check end time of timespan hasn't passed midnight
if (((hours * modconst.MSECS_PER_HOUR)
+ (minutes * modconst.MSECS_PER_MIN)
+ (seconds * 1000)
+ (milliseconds)
+ (inDurationMins * modconst.MSECS_PER_MIN)
+ (inDurationSecs * 1000)
+ (inDurationMs)) > (modconst.MAX_MINS_PER_DAY * modconst.MSECS_PER_MIN)) {
throw new Error('End time of timespan exceeds midnight.');
};
/* public methods ***********************************************************/
/**
* Get the hour of the day value.
* @return {number} Integer between 0 and 23.
*/
that.getHours = function getHoursFunc() {
return hours;
};
/**
* Get the minutes value i.e. minutes of the time.
* @return {number} Integer between 0 and 59.
*/
that.getMinutes = function getMinutesFunc() {
return minutes;
};
/**
* Get the seconds value i.e. seconds component of the start time.
* @return {number} Integer between 0 and 59.
*/
that.getSeconds = function getSecondsFunc() {
return seconds;
};
/**
* Get the milliseconds value i.e. milliseconds component of the start time.
* @return {number} Integer between 0 and 999.
*/
that.getMilliseconds = function getMillisecondsFunc() {
return milliseconds;
};
/**
* Get the [minutes] component of the duration. This is not the total
* duration, as this also has [seconds] and [milliseconds] components.
* @return {number} Integer between 1 and MAX_MINS_PER_DAY.
* Units are [minutes].
*/
that.getDurationMins = function getDurationMinsFunc() {
return durationMins;
};
/**
* Get the [seconds] component of the duration. This is not the total
* duration, as this also has [minutes] and [milliseconds] components.
* @return {number} Integer between 1 and 59. Units are [seconds].
*/
that.getDurationSecs = function getDurationSecsFunc() {
return durationSecs;
};
/**
* Get the [milliseconds] component of the duration. This is not the total
* duration, as this also has [minutes] and [seconds] components.
* @return {number} Integer between 1 and 999. Units are [milliseconds].
*/
that.getDurationMs = function getDurationMsFunc() {
return durationMs;
};
/**
* Get the total duration of the timespan in [milliseconds].
* @return {number} Integer representing the duration of the timespan.
* Units are [milliseconds].
*/
that.getTotalDuration = function getTotalDurationFunc() {
let retval = 0;
retval += (durationMins * modconst.MSECS_PER_MIN);
retval += (durationSecs * 1000);
retval += durationMs;
return retval;
};
/**
* Query if two TimeSpan objects are equal. Equality means they have the
* exact same start time and duration.
* @param {Object} inTimeSpan Another instance of TimeSpan. An Error is
* thrown if this is not a valid TimeSpan object.
* @return {boolean} true if the DateSpan objects are equal, otherwise false.
* @throws {Error}
*/
that.isEqual = function isEqualFunc(inTimeSpan) {
let retval = false;
if (_.isObject(inTimeSpan) === false) {
throw new Error('Method expects a valid TimeSpan object.');
}
if (hours == inTimeSpan.getHours()
&& minutes == inTimeSpan.getMinutes()
&& seconds == inTimeSpan.getSeconds()
&& milliseconds === inTimeSpan.getMilliseconds()
&& that.getTotalDuration() === inTimeSpan.getTotalDuration()) {
retval = true;
}
return retval;
};
/**
* Query if a time-span intersects (overlaps) with a different time-span.
* The end-times of time-spans are exclusive therefore more than the end
* time must overlap.
* @param {Object} inTimeSpan TimeSpan object. An Error is thrown if the
* argument is not a valid TimeSpan object.
* @throws {Error}
*/
that.isIntersect = function isIntersectFunc(inTimeSpan) {
let retval = true;
if(_.isObject(inTimeSpan) === false) {
throw new Error('Invalid argument. Expects TimeSpan object.');
}
if(calcEndMs(that) <= calcBeginMs(inTimeSpan)
|| calcBeginMs(that) >= calcEndMs(inTimeSpan)) {
retval = false;
}
return retval;
}
/**
* Create a new TimeSpan object which represents the intersection of
* two other TimeSpan objects.
* @param {Object} inTimeSpan A valid TimeSpan object. An Error is thrown
* if an invalid value is passed.
* @return {Object|null} A TimeSpan object or null if there is no
* intersection (overlapping).
*
*/
that.intersect = function intersectFunc(inTimeSpan) {
let retval = null;
let beginSpan = null;
let endSpan = null;
let newDurationMs = 0;
if (_.isObject(inTimeSpan) === false) {
throw new Error('Invalid TimeSpan argument passed to function.');
}
if (that.isIntersect(inTimeSpan)) {
beginSpan = (calcBeginMs(that) > calcBeginMs(inTimeSpan)
&& calcBeginMs(that) < calcEndMs(inTimeSpan))?that:inTimeSpan;
endSpan = (calcEndMs(that) > calcBeginMs(inTimeSpan)
&& calcEndMs(that) < calcEndMs(inTimeSpan))?that:inTimeSpan;
newDurationMs = calcEndMs(endSpan) - calcBeginMs(beginSpan);
retval = timeSpan(beginSpan.getHours(),
beginSpan.getMinutes(),
beginSpan.getSeconds(),
beginSpan.getMilliseconds(),
Math.floor(newDurationMs / modconst.MSECS_PER_MIN),
Math.floor((newDurationMs % modconst.MSECS_PER_MIN) / modconst.MSECS_PER_SEC),
Math.floor(newDurationMs % modconst.MSECS_PER_SEC));
}
return retval;
}
/**
* Create a new TimeSpan object which represents the union of
* this TimeSpan object with another TimeSpan object. The time-spans must
* intersect for the union to be possible.
* @param {Object} inTimeSpan A valid TimeSpan object. An Error is thrown
* if an invalid value is passed.
* @return {Object|null} A TimeSpan object or null if there is no
* intersection (overlapping).
*
*/
that.union = function unionFunc(inTimeSpan) {
let retval = null;
if(_.isObject(inTimeSpan) === false) {
throw new Error('Invalid TimeSpan argument passed to function.');
}
const beginSpan = (calcBeginMs(that) < calcBeginMs(inTimeSpan))?that:inTimeSpan;
const endSpan = (calcEndMs(that) > calcEndMs(inTimeSpan))?that:inTimeSpan;
if(that.isIntersect(inTimeSpan)) {
let newDurationMs = calcEndMs(endSpan) - calcBeginMs(beginSpan);
retval = timeSpan(beginSpan.getHours(),
beginSpan.getMinutes(),
beginSpan.getSeconds(),
beginSpan.getMilliseconds(),
Math.floor(newDurationMs / modconst.MSECS_PER_MIN),
Math.floor((newDurationMs % modconst.MSECS_PER_MIN) / modconst.MSECS_PER_SEC),
Math.floor(newDurationMs % modconst.MSECS_PER_SEC));
}
return retval;
}
/**
* Create one or two TimeSpans object which represent the subtraction of
* one time-span from another time-span. inTimeSpan must fully overlap
* with this time-span, which it will be subtracted from.
* @param {Object} inTimeSpan A valid TimeSpan object. An Error is thrown
* if an invalid value is passed.
* @return {Array|null} An array of TimeSpan objects or null if there was
* not enough intersection (overlapping) for the subtraction to be performed.
* Subtracting two equal time-spans will produce an empty array.
*/
that.subtract = function subtractFunc(inTimeSpan) {
let retval = [];
let newBegin = null;
let newEnd = null;
let newBeginSplit = null;
let newDurationSplit = null;
let newSpan = null;
if(_.isObject(inTimeSpan) === false) {
throw new Error('Invalid TimeSpan argument passed to function.');
}
if (that.isIntersect(inTimeSpan)
&& calcBeginMs(that) <= calcBeginMs(inTimeSpan)
&& calcEndMs(that) >= calcEndMs(inTimeSpan)) {
// remainder time-span at end of this timespan.
if (calcBeginMs(that) <= calcBeginMs(inTimeSpan)
&& calcEndMs(that) > calcEndMs(inTimeSpan)) {
newBeginSplit = splitTime(calcEndMs(inTimeSpan));
newDurationSplit = splitTime(calcEndMs(that) - calcEndMs(inTimeSpan));
newSpan = timeSpan(newBeginSplit.hours,
newBeginSplit.minutes,
newBeginSplit.seconds,
newBeginSplit.milliseconds,
(newDurationSplit.hours * 60) + newDurationSplit.minutes,
newDurationSplit.seconds,
newDurationSplit.milliseconds);
retval.push(newSpan);
// remainder timespan at beginning of this timespan
}
if (calcEndMs(that) >= calcEndMs(inTimeSpan)
&& calcBeginMs(that) < calcBeginMs(inTimeSpan)) {
newBeginSplit = splitTime(calcBeginMs(that));
newDurationSplit = splitTime(calcBeginMs(inTimeSpan) - calcBeginMs(that));
newSpan = timeSpan(newBeginSplit.hours,
newBeginSplit.minutes,
newBeginSplit.seconds,
newBeginSplit.milliseconds,
(newDurationSplit.hours * 60) + newDurationSplit.minutes,
newDurationSplit.seconds,
newDurationSplit.milliseconds);
retval.push(newSpan);
}
} else if (calcBeginMs(that) > calcBeginMs(inTimeSpan)
|| calcEndMs(that) < calcEndMs(inTimeSpan)) {
// insufficient overlap for subtraction
retval = null;
}
return retval;
}
/**
* Convert the state of the TimeSpan object to a string. Method is only
* intended for debugging purposes. The format of the string will change
* in future releases.
* @return {string} String holding the state of the time-span.
*/
that.toString = function toStringFunc() {
// TODO finalise the format of the string
let retval = `[[begin: ${hours}h ${minutes}m ${seconds}s `;
retval = retval + `${milliseconds}ms:]`;
retval = retval + `[duration: ${durationMins}m ${durationSecs}s `;
retval = retval + `${durationMs}ms]]`;
return retval;
}
/* functional constructor returns a new instance of TimeSpan object. */
return that;
};
/* private module functions ***************************************************/
/**
* Get the inclusive begin time of a TimeSpan in milliseconds. This is
* the time elapsed since 00:00:00 (midnight) of the same day.
* @param {Object} inTimeSpan A valid TimeSpan object. An error if thrown
* if the object is not an instance of TimeSpan.
* @return {number} Start time in [milliseconds].
* @throws {Error}
*/
const calcBeginMs = function calcBeginMsFunc(inTimeSpan) {
let retval = 0;
if(_.isObject(inTimeSpan) === false) {
throw new Error('Invalid TimeSpan argument passed to function.');
}
retval += (inTimeSpan.getHours() * modconst.MSECS_PER_HOUR);
retval += (inTimeSpan.getMinutes() * modconst.MSECS_PER_MIN);
retval += (inTimeSpan.getSeconds() * 1000);
retval += inTimeSpan.getMilliseconds();
return retval;
}
/**
* Get the exclusive end time of a TimeSpan in milliseconds. This is
* the time elapsed since 00:00:00 (midnight) of the same day.
* @param {Object} inTimeSpan A valid TimeSpan object. An error if thrown
* if the object is not an instance of TimeSpan.
* @return {number} End time in [milliseconds].
* @throws {Error}
*/
const calcEndMs = function calcEndMsecsFunc(inTimeSpan) {
let retval = 0;
if(_.isObject(inTimeSpan) === false) {
throw new Error('Invalid TimeSpan argument passed to function.');
}
retval = calcBeginMs(inTimeSpan);
retval += inTimeSpan.getTotalDuration();
return retval;
}
/**
* Split the time of the day, when expressed in [milliseconds] into hours,
* minutes, seconds and milliseconds.
* @param {number} inMilliseconds Time of the day in milliseconds i.e. time
* elapsed since the previous midnight.
* @return {Object} An object with four data members, one each for the time
* component in hours, minutes, seconds and milliseconds.
*/
const splitTime = function splitTimeFunc(inMilliseconds) {
let retval = {};
let doneMs = 0;
retval.hours = Math.floor(inMilliseconds / modconst.MSECS_PER_HOUR);
doneMs = retval.hours * modconst.MSECS_PER_HOUR;
retval.minutes = Math.floor((inMilliseconds - doneMs) / modconst.MSECS_PER_MIN);
doneMs += retval.minutes * modconst.MSECS_PER_MIN;
retval.seconds = Math.floor((inMilliseconds - doneMs) / modconst.MSECS_PER_SEC);
retval.milliseconds = Math.floor(inMilliseconds % modconst.MSECS_PER_SEC);;
return retval;
}
/* module exports the functional constructor */
module.exports.timeSpan = timeSpan;