diff --git a/README.md b/README.md index 8506ff3..55ce889 100644 --- a/README.md +++ b/README.md @@ -755,6 +755,44 @@ result[0].getBegin(); // 10:00am result[0].getEnd(); // 10:30am ``` +### subtractDateSpans() + +Function takes two Arrays of `DateSpan` objects and returns a new Array which +contains new `DateSpan` objects, each representing an subtraction between +a `DateSpan` object from each Array. + +```js +const caltime = require('caltime'); +const datespanCtor = caltime.dateSpan; +const subtractDateSpans = caltime.subtractDateSpans; +let spanListA = null; +let spanListB = null; +// DateSpan object which represents 09:00am - 12:00am. +var beginDate = new Date(2017, 10, 15, 9, 0, 0, 0); +var spanA = datespanCtor(beginDate, null, 180, 0, 0); +// create a DateSpan object which represents 14:00am - 16:00am. +beginDate = new Date(2017, 10, 15, 14, 0, 0, 0); +var spanB = datespanCtor(beginDate, null, 120, 0, 0); +// create a DateSpan object which represents 10:00am - 11:00am. +beginDate = new Date(2017, 10, 15, 10, 0, 0, 0); +var spanC = datespanCtor(beginDate, null, 60, 0, 0); +// create a DateSpan object which represents 15:00 - 16:00. +beginDate = new Date(2017, 10, 15, 15, 0, 0, 0); +var spanD = datespanCtor(beginDate, null, 60, 0, 0); +// populate the arrays +spanListA = [ spanA, spanB ]; +spanListB = [ spanC, spanD ]; +// sort in descending order +let result = subtractDateSpans(spanListA, spanListB); +result.length; // 3 +result[0].getBegin(); // 09:00am +result[0].getEnd(); // 10:00am +result[1].getBegin(); // 11:00am +result[1].getEnd(); // 12:00am +result[2].getBegin(); // 14:00am +result[2].getEnd(); // 15:00am +``` + ## TimeRule The `TimeRule` object allows logic to be defined which can then be used to diff --git a/index.js b/index.js index d22299a..ad7663c 100644 --- a/index.js +++ b/index.js @@ -67,6 +67,13 @@ module.exports.measureDateSpans = datespanModule.measureSpans; */ module.exports.intersectDateSpans = datespanModule.intersectSpans; +/** + * Function used to intersect the DateSpan objects in two arrays. + * @public + * @see {@link module:caltime/datespan~subtractSpans} + */ +module.exports.subtractDateSpans = datespanModule.subtractSpans; + /** * Function used to merge TimeSpan objects in an array. * @public diff --git a/lib/datespan.js b/lib/datespan.js index 83185b0..cf4c32e 100644 --- a/lib/datespan.js +++ b/lib/datespan.js @@ -622,11 +622,78 @@ const intersectSpans = function(inSpansA, inSpansB) { return retList; } +/** + * Provided with two arrays of DateSpan objects, generate an array containing + * DateSpan objects for each of the intervals of subtraction between the two + * arrays. + * @param {Array} inSpansA Array of DateSpan objects. + * @param {Array} inSpansB Array of DateSpan objects. + * @return {Array} Array of DateSpan objects. + */ +const subtractSpans = function(inSpansA, inSpansB) { + const retList = []; + const mergedListA = mergeSpans(inSpansA); + const mergedListB = mergeSpans(inSpansB); + + if (mergedListA.length === 0) { + return []; + } else if (mergedListB.length === 0) { + return mergedListA; + } + + let aPointer = 0; + let bPointer = 0; + let a = mergedListA[aPointer]; + let b = mergedListB[bPointer]; + let overlapSpan = null; + + while (a !== undefined && b !== undefined) { + if (a.getEnd().getTime() <= b.getBegin().getTime()) { + // A strictly before B, save and proceed with next A + retList.push(a); + a = mergedListA[++aPointer]; + continue; + } else if (b.getEnd().getTime() <= a.getBegin().getTime()) { + // B strictly before A, proceed with next B + b = mergedListB[++bPointer]; + continue; + } + + // We have some kind of overlap between cur_a and cur_b + overlapSpan = a.subtract(b); + + if (overlapSpan !== null) { + retList.push(overlapSpan[0]); + + if (overlapSpan.length == 2) { + a = overlapSpan[1]; + b = mergedListB[++bPointer]; + // No next B, no more overlap, save what we got and proceed with next A + if (b === undefined) { + aPointer++; + retList.push(overlapSpan[1]); + } + } else { + a = mergedListA[++aPointer]; + } + } else { + // cur_b totally consumes cur_a + a = mergedListA[++aPointer]; + } + } + + // Push any remainder in mergedListA to retList + mergedListA.slice(aPointer).forEach(function(v) {retList.push(v)}, retList); + + return retList; +} + /* interface exported by the module */ module.exports.dateSpan = dateSpan; module.exports.mergeSpans = mergeSpans; module.exports.sortSpans = sortSpans; module.exports.measureSpans = measureSpans; module.exports.intersectSpans = intersectSpans; +module.exports.subtractSpans = subtractSpans; /** private functions *********************************************************/ diff --git a/test/unit-datespan.js b/test/unit-datespan.js index 0718fe6..2884070 100644 --- a/test/unit-datespan.js +++ b/test/unit-datespan.js @@ -17,6 +17,7 @@ tc.mergeDateSpans = require('../').mergeDateSpans; tc.sortDateSpans = require('../').sortDateSpans; tc.measureDateSpans = require('../').measureDateSpans; tc.intersectDateSpans = require('../').intersectDateSpans; +tc.subtractDateSpans = require('../').subtractDateSpans; tc.constants = require('../').constants; /* useful Date objects for testing */ @@ -1041,3 +1042,177 @@ describe('DateSpan - Intersect Arrays', function() { assert.equal(result[0].getDurationMins(), 4*60, 'Expected a different duration'); }); }); + +describe('DateSpan - Subtract Arrays', function() { + + it('Pass invalid arguments', function() { + const spanArray = []; + // check first argument + assert.throws(function() { + tc.subtractDateSpans(null, spanArray); + }, + Error, + 'Expected method to throw an error.'); + assert.throws(function() { + tc.subtractDateSpans(undefined, spanArray); + }, + Error, + 'Expected method to throw an error.'); + assert.throws(function() { + tc.subtractDateSpans({}, spanArray); + }, + Error, + 'Expected method to throw an error.'); + // check second argument + assert.throws(function() { + tc.subtractDateSpans(spanArray, null); + }, + Error, + 'Expected method to throw an error.'); + assert.throws(function() { + tc.subtractDateSpans(spanArray, undefined); + }, + Error, + 'Expected method to throw an error.'); + assert.throws(function() { + tc.subtractDateSpans(spanArray, {}); + }, + Error, + 'Expected method to throw an error.'); + }); + + it('Pass two empty arrays', function() { + const spansA = []; + const spansB = []; + let result = tc.subtractDateSpans(spansA, spansB); + assert.notEqual(result, null, 'Function should return an array.'); + assert.equal(_.isArray(result), true, 'Method should return an array.'); + }); + + it('Subtract arrays with no overlap', function() { + const spansA = []; + const spansB = []; + const dateSpanA = tc.dateSpanCtor(dateA, null, 1*60); // 1 hr + const dateSpanB = tc.dateSpanCtor(dateB, null, 1*60); // 1 hr + const dateSpanC = tc.dateSpanCtor(dateC, null, 1*60); // 1 hr + const dateSpanD = tc.dateSpanCtor(dateF, null, 1*60); // 1 hr + spansA.push(dateSpanA); + spansA.push(dateSpanB); + spansB.push(dateSpanC); + spansB.push(dateSpanD); + const result = tc.subtractDateSpans(spansA, spansB); + assert.equal(_.isArray(result), true, 'Function should return an array.'); + assert.equal(result.length, spansA.length, 'Expected same amount of elements in array as in spansA.'); + assert.notEqual(result, spansA, 'Method should return a new array object (but with same contents as spansA).'); + assert.notEqual(result, spansB, 'Method should return a new array object.'); + assert.equal(result[0].isEqual(spansA[0]), true, 'Method should return a new array object (but with same contents as spansA).'); + assert.equal(result[1].isEqual(spansA[1]), true, 'Method should return a new array object (but with same contents as spansA).'); + }); + + it('Subtract arrays, two overlaps', function() { + const spansA = []; + const spansB = []; + const dateSpanA = tc.dateSpanCtor(dateA, null, 2*60); // 2 hr + const dateSpanB = tc.dateSpanCtor(dateB, null, 2*60); // 2 hr + const dateSpanC = tc.dateSpanCtor(dateA, null, 1*60); // 1 hrs + const dateSpanD = tc.dateSpanCtor(dateB, null, 1*60); // 1 hrs + const dateSpanE = tc.dateSpanCtor(dateF, null, 1*60); // 1 hr + spansA.push(dateSpanA); + spansA.push(dateSpanB); + spansB.push(dateSpanC); + spansB.push(dateSpanD); + spansB.push(dateSpanE); + const result = tc.subtractDateSpans(spansA, spansB); + assert.equal(_.isArray(result), true, 'Function should return an array.'); + assert.equal(result.length, 2, 'Expected 2 elements in array.'); + assert.notEqual(result, spansA, 'Method should return a new array object.'); + assert.notEqual(result, spansB, 'Method should return a new array object.'); + assert.equal(result[0].getBegin().getTime(), dateA.getTime() + 3600000, 'Expected a different start date on first time span'); + assert.equal(result[0].getDurationMins(), 1*60, 'Expected a different duration'); + assert.equal(result[1].getBegin().getTime(), dateB.getTime() + 3600000, 'Expected a different start date on second time span'); + assert.equal(result[1].getDurationMins(), 1*60, 'Expected a different duration'); + }); + + it('Subtract arrays, multiple overlaps', function() { + const spansA = []; + const spansB = []; + const dateSpanA = tc.dateSpanCtor(dateB, null, 1*60); // Sunday 16/7/2017, 12:00 + const dateSpanB = tc.dateSpanCtor(dateC, null, 3*60); // Monday 17/7/2017, 12:00 - 15:00 + const dateSpanC = tc.dateSpanCtor(dateE, null, 1*60); // Monday 17/7/2017, 18:00 + const dateSpanD = tc.dateSpanCtor(dateF, null, 1*60); // Tuesday 18/7/2017, 12:00 + const dateSpanE = tc.dateSpanCtor(dateH, null, 1*60); // Sunday 23/7/2017, 12:00 + const dateSpanF = tc.dateSpanCtor(dateD, null, 1*60); // Monday 17/7/2017, 13:00 - 14:00 + spansA.push(dateSpanA); + spansA.push(dateSpanB); + spansA.push(dateSpanC); + spansA.push(dateSpanD); + spansA.push(dateSpanE); + spansB.push(dateSpanF); + const result = tc.subtractDateSpans(spansA, spansB); + assert.equal(_.isArray(result), true, 'Function should return an array.'); + assert.equal(result.length, 6, 'Expected 6 elements in array.'); + assert.notEqual(result, spansA, 'Method should return a new array object.'); + assert.notEqual(result, spansB, 'Method should return a new array object.'); + assert.equal(result[0].getBegin().getTime(), dateB.getTime(), 'Expected a different start date'); + assert.equal(result[0].getDurationMins(), 1*60, 'Expected a different duration'); + assert.equal(result[1].getBegin().getTime(), dateC.getTime(), 'Expected a different start date'); + assert.equal(result[1].getDurationMins(), 1*60, 'Expected a different duration'); + assert.equal(result[2].getBegin().getTime(), dateD.getTime() + 3600000, 'Expected a different start date'); + assert.equal(result[2].getDurationMins(), 1*60, 'Expected a different duration'); + assert.equal(result[result.length-1].getBegin().getTime(), dateH.getTime(), 'Expected a different start date'); + assert.equal(result[result.length-1].getDurationMins(), 1*60, 'Expected a different duration'); + }); + + it('Subtract arrays, CalCost scenario', function() { + const spansA = []; + const spansX = []; + const dateSD = new Date(Date.UTC(2017, 6, 16, 12, 0, 0, 0)); // Sunday 16th, 12:00 + const spanD = tc.dateSpanCtor(dateSD, null, 4*60, 0, 0); // Sunday, 12:00 - 16:00 + spansA.push(spanD); + const dateXB = new Date(Date.UTC(2017, 6, 14, 1, 0, 0, 0)); // 14th July, 01:00 + const dateXC = new Date(Date.UTC(2017, 6, 15, 1, 0, 0, 0)); // 15th July, 01:00 + const dateXD = new Date(Date.UTC(2017, 6, 16, 1, 0, 0, 0)); // 16th July, 01:00 + const spanXB = tc.dateSpanCtor(dateXB, null, (22*60), 0, 0); // 01:00-23:00 + const spanXC = tc.dateSpanCtor(dateXC, null, (22*60), 0, 0); // 01:00-23:00 + const spanXD = tc.dateSpanCtor(dateXD, null, (22*60), 0, 0); // 01:00-23:00 + spansX.push(spanXB); + spansX.push(spanXC); + spansX.push(spanXD); + const result = tc.subtractDateSpans(spansA, spansX); + assert.equal(_.isArray(result), true, 'Function should return an array.'); + assert.equal(result.length, 0, 'Expected 0 element in array.'); + assert.notEqual(result, spansA, 'Method should return a new array object.'); + assert.notEqual(result, spansX, 'Method should return a new array object.'); + }); + + it('Subtract arrays, single timespan in the subtractor', function() { + const spansA = []; // Think of as Presence + const spansB = []; // Think of as Absence + + // Presence + const dateA = new Date(Date.UTC(2017, 5, 2, 9, 0, 0, 0)); // [ 2017-06-02T09:00:00.000Z, 420:0:0 ] + const dateB = new Date(Date.UTC(2017, 5, 5, 9, 0, 0, 0)); // [ 2017-06-05T09:00:00.000Z, 420:0:0 ] + const dateC = new Date(Date.UTC(2017, 5, 6, 9, 0, 0, 0)); // [ 2017-06-06T09:00:00.000Z, 420:0:0 ] + const spanA = tc.dateSpanCtor(dateA, null, 8*60, 0, 0); + const spanB = tc.dateSpanCtor(dateB, null, 8*60, 0, 0); + const spanC = tc.dateSpanCtor(dateC, null, 8*60, 0, 0); + spansA.push(spanA); + spansA.push(spanB); + spansA.push(spanC); + + // Absence + const dateH = new Date(Date.UTC(2017, 5, 4, 9, 0, 0, 0)); // [ 2017-06-04T09:00:00.000Z, 480:0:0 ] + const spanH = tc.dateSpanCtor(dateH, null, 8*60, 0, 0); + spansB.push(spanH); + + const result = tc.subtractDateSpans(spansA, spansB); + + assert.equal(_.isArray(result), true, 'Function should return an array.'); + assert.equal(result.length, 3, 'Expected 3 element in array.'); + assert.equal(spansA[0].isEqual(result[0]), true, 'Expected same timespan as dateA[0]'); + assert.equal(spansA[1].isEqual(result[1]), true, 'Expected same timespan as dateA[1]'); + assert.equal(spansA[2].isEqual(result[2]), true, 'Expected same timespan as dateA[2]'); + }); + + +});