Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subtractDateSpans: array subtraction operation #9

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions lib/datespan.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 *********************************************************/
175 changes: 175 additions & 0 deletions test/unit-datespan.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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]');
});


});