diff --git a/lib/period-length.js b/lib/period-length.js new file mode 100644 index 0000000000000000000000000000000000000000..e62f076ef09aa0a555c62219fbd3a33c5e90baba --- /dev/null +++ b/lib/period-length.js @@ -0,0 +1,49 @@ +import assert from 'assert' + +export default function getPeriodLengthStats(cycleLengths) { + throwIfArgsAreNotInRequiredFormat(cycleLengths) + const periodLengthStats = {} + const sortedCycleLengths = cycleLengths.sort((a, b) => { + return a - b + }) + periodLengthStats.minimum = sortedCycleLengths[0] + periodLengthStats.maximum = sortedCycleLengths[cycleLengths.length - 1] + periodLengthStats.mean = Math.round( + cycleLengths.reduce(getSum) / cycleLengths.length * 100 + ) / 100 + // median + if (cycleLengths.length % 2 == 1) { + periodLengthStats.median = sortedCycleLengths[ + (cycleLengths.length + 1) / 2 - 1 + ] + } else { + const middle = cycleLengths.length / 2 + periodLengthStats.median = (sortedCycleLengths[middle - 1] + + sortedCycleLengths[middle]) / 2 + } + // corrected standard deviation (based on unbiased sample variance) + if (cycleLengths.length > 1) { + const sumOfSquares = cycleLengths.map(cycleLength => { + return Math.pow(cycleLength - periodLengthStats.mean, 2) + }).reduce(getSum) + periodLengthStats.stdDeviation = Math.round( + Math.sqrt(sumOfSquares / (cycleLengths.length - 1 )) * 100 + ) / 100 + } else { + periodLengthStats.stdDeviation = null + } + return periodLengthStats +} + +function getSum(total, num) { + return total + num +} + +function throwIfArgsAreNotInRequiredFormat(cycleLengths) { + assert.ok(Array.isArray(cycleLengths), 'Input should be an array.') + assert.ok(cycleLengths.length > 0, 'Input array should not be empty.') + cycleLengths.forEach(cycleLength => { + assert.equal(typeof cycleLength, 'number', 'Elements in the array should be of type number.') + assert.ok(!isNaN(cycleLength), 'Elements of array should not be NaN.') + }) +} \ No newline at end of file diff --git a/test/periode-length.spec.js b/test/periode-length.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0b9b200b479b828e526295682b756c87065208a9 --- /dev/null +++ b/test/periode-length.spec.js @@ -0,0 +1,64 @@ +import chai from 'chai' +import { AssertionError } from 'assert' + +import periodInfo from '../lib/period-length' + +const expect = chai.expect + +describe.only('getPeriodLengthStats', () => { + it('works for a simple odd-numbered array', () => { + const periodLengths = [99, 5, 1, 2, 100] + const result = periodInfo(periodLengths) + const expectedResult = { + minimum: 1, + maximum: 100, + mean: 41.4, + median: 5, + stdDeviation: 53.06 + } + expect(result).to.eql(expectedResult) + }) + + it('works for a simple even-numbered array', () => { + const periodLengths = [4, 1, 15, 2, 20, 5] + const result = periodInfo(periodLengths) + const expectedResult = { + minimum: 1, + maximum: 20, + mean: 7.83, + median: 4.5, + stdDeviation: 7.78 + } + expect(result).to.eql(expectedResult) + }) + it('works for an one-element array', () => { + const periodLengths = [42] + const result = periodInfo(periodLengths) + const expectedResult = { + minimum: 42, + maximum: 42, + mean: 42, + median: 42, + stdDeviation: null + } + expect(result).to.eql(expectedResult) + }) + describe('when args are wrong', () => { + it('throws when arg object is an empty array', () => { + const periodLengths = [] + expect(() => periodInfo(periodLengths).to.throw(AssertionError)) + }) + it('throws when arg object is not in right format', () => { + const wrongObject = { hello: 'world' } + expect(() => periodInfo(wrongObject).to.throw(AssertionError)) + }) + it('throws when arg array contains a string', () => { + const wrongElement = [4, 1, 15, '2', 20, 5] + expect(() => periodInfo(wrongElement).to.throw(AssertionError)) + }) + it('throws when arg array contains a NaN', () => { + const wrongElement = [4, 1, 15, NaN, 20, 5] + expect(() => periodInfo(wrongElement).to.throw(AssertionError)) + }) + }) +}) \ No newline at end of file