Skip to content

Commit

Permalink
Adds functions to get total and billable days for charging
Browse files Browse the repository at this point in the history
  • Loading branch information
James Carmichael committed Oct 3, 2019
1 parent 9ca5820 commit 1c1061b
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 0 deletions.
129 changes: 129 additions & 0 deletions src/charging/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const Joi = require('joi');
const moment = require('moment');

const MONTH_SCHEMA = Joi.number().integer().min(1).max(12).required();
const DAY_SCHEMA = Joi.number().integer().min(1).max(31).required();

const ABS_PERIOD_SCHEMA = Joi.object({
startMonth: MONTH_SCHEMA,
startDay: DAY_SCHEMA,
endMonth: MONTH_SCHEMA,
endDay: DAY_SCHEMA
});

const DATE_SCHEMA = Joi.string().regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/);

/**
* Given a date, calculates the financial year ending
* E.g. for 2019/2020, returns 2020
* @param {String} date
* @return {Number} year
*/
const getFinancialYear = date => {
const m = moment(date);
return m.month() < 3 ? m.year() : m.year() + 1;
};

/**
* Given an abstraction period day and month, and a financial year,
* returns the actual date as a moment
* @param {Number} day
* @param {Number} month
* @param {Number} financialYear
* @return {Object} moment object
*/
const getAbsPeriodDate = (day, month, financialYear) => {
const year = month < 3 ? financialYear : financialYear - 1;
return moment(`${day}-${month}-${year}`, 'D-M-YYYY');
};

/**
* Limits a number to a minimum possible value of 0
* @param {Number} num
* @return {Number}
*/
const positive = num => num < 0 ? 0 : num;

/**
* Gets the difference in billable days between the two dates in the provided array
* @param {Array} arr
* @return {Number}
*/
const diffDays = arr => positive(1 + moment(arr[1]).diff(arr[0], 'days'));

/**
* Gets the total days in the financial year
* @param {String} startDate - YYYY-MM-DD
* @param {String} endDate YYYY-MM-DD
* @return {Number}
*/
const getTotalDays = (startDate, endDate) => {
// Validate inputs
Joi.assert(startDate, DATE_SCHEMA);
Joi.assert(endDate, DATE_SCHEMA);

return positive(1 + moment(endDate).diff(startDate, 'days'));
};

/**
* Limits date so it is not before the min date and
* not after the max date
* @param {String} date - YYYY-MM-DD
* @param {String} minDate - YYYY-MM-DD
* @param {String} maxDate - YYYY-MM-DD
* @return {String} YYYY-MM-DD
*/
const limitDate = (date, minDate, maxDate) => {
if (moment(date).isBefore(minDate, 'day')) {
return minDate;
}
if (moment(date).isAfter(maxDate, 'day')) {
return maxDate;
}
return date;
};

const mapRange = (range, startDate, endDate) =>
range.map(value => limitDate(value, startDate, endDate));

const isValidRange = ([startDate, endDate]) =>
moment(startDate).isSameOrBefore(endDate, 'day');

const createDateRanges = (startDate, endDate, absStart, absEnd) => {
const dateRanges = moment(absEnd).isBefore(absStart, 'day')
? [ [startDate, absEnd], [absStart, endDate] ]
: [ [ absStart, absEnd ] ];

return dateRanges.filter(isValidRange).map(range =>
mapRange(range, startDate, endDate)
);
};

/**
* Gets the number of billable days between the start and end date,
* when the abstraction period is taken into account
* @param {Object} absPeriod - the abstraction period start/end day/month
* @param {String} startDate - start of the billing period
* @param {String} endDate - end of the billing period
*/
const getBillableDays = (absPeriod, startDate, endDate) => {
// Validate inputs
Joi.assert(absPeriod, ABS_PERIOD_SCHEMA);
Joi.assert(startDate, DATE_SCHEMA);
Joi.assert(endDate, DATE_SCHEMA);

// Calculate important dates
const financialYear = getFinancialYear(startDate);

const absStart = getAbsPeriodDate(absPeriod.startDay, absPeriod.startMonth, financialYear);
const absEnd = getAbsPeriodDate(absPeriod.endDay, absPeriod.endMonth, financialYear);

// Create date ranges
const dateRanges = createDateRanges(startDate, endDate, absStart, absEnd);

// Return billable days
return dateRanges.reduce((acc, range) => acc + diffDays(range), 0);
};

exports.getTotalDays = getTotalDays;
exports.getBillableDays = getBillableDays;
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
exports.charging = require('./charging');
exports.logger = require('./logger');
exports.nald = require('./nald');
exports.regions = require('./regions');
Expand Down
91 changes: 91 additions & 0 deletions test/charging/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const {
experiment,
test
} = exports.lab = require('lab').script();
const { expect } = require('code');

const charging = require('../../src/charging');

const absPeriods = {
allYear: {
startDay: 1,
startMonth: 1,
endDay: 31,
endMonth: 12
},
singleRange: {
startDay: 1,
startMonth: 4,
endDay: 31,
endMonth: 10
},
doubleRange: {
startDay: 1,
startMonth: 12,
endDay: 30,
endMonth: 4
}
};

experiment('charging.getTotalDays', () => {
test('a full financial not containing a leap year gives 365 chargeable days', async () => {
const result = charging.getTotalDays('2018-04-01', '2019-03-31');
expect(result).to.equal(365);
});

test('a full financial containing a leap year gives 366 chargeable days', async () => {
const result = charging.getTotalDays('2019-04-01', '2020-03-31');
expect(result).to.equal(366);
});
});

experiment('charging.getBillableDays', () => {
test('all-year abstraction in a financial year not containing a leap year gives 365 days', async () => {
const result = charging.getBillableDays(absPeriods.allYear, '2018-04-01', '2019-03-31');
expect(result).to.equal(365);
});

test('all-year abstraction in a financial year containing a leap year gives 366 days', async () => {
const result = charging.getBillableDays(absPeriods.allYear, '2019-04-01', '2020-03-31');
expect(result).to.equal(366);
});

experiment('when the abs period is a single range within a calendar year', async () => {
test('for a full year, the result is the days within the abs period', async () => {
const result = charging.getBillableDays(absPeriods.singleRange, '2018-04-01', '2019-03-31');
expect(result).to.equal(214);
});

test('when the end date is on or after the end of the abs period, the billable days are unaffected', async () => {
const result = charging.getBillableDays(absPeriods.singleRange, '2018-04-01', '2018-10-31');
expect(result).to.equal(214);
});

test('when the end date is before the end of the abs period, the billable days are reduced', async () => {
const result = charging.getBillableDays(absPeriods.singleRange, '2018-04-01', '2018-09-30');
expect(result).to.equal(183);
});

test('when the start date is after the start of the abs period, the billable days are reduced', async () => {
const result = charging.getBillableDays(absPeriods.singleRange, '2018-05-01', '2019-03-31');
expect(result).to.equal(184);
});
});

experiment('when the abs period is two ranges within a calendar year', async () => {
test('for a full year, the result is the days within the abs period', async () => {
const result = charging.getBillableDays(absPeriods.doubleRange, '2018-04-01', '2019-03-31');
expect(result).to.equal(151);
});

test('when the end date is before the end of the abs period, the billable days are reduced', async () => {
const result = charging.getBillableDays(absPeriods.doubleRange, '2018-04-01', '2018-12-31');
expect(result).to.equal(61);
});

test('when the start date is after the start of the abs period, the billable days are reduced', async () => {
const result = charging.getBillableDays(absPeriods.doubleRange, '2018-05-01', '2019-03-31');
expect(result).to.equal(121);
});
});
});

0 comments on commit 1c1061b

Please sign in to comment.