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 offset period aggregation type #1663

Merged
merged 4 commits into from
Nov 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { arrayToAnalytics } from '@tupaia/data-broker';
import { offsetPeriod } from '../../../../analytics/aggregateAnalytics/aggregations/offsetPeriod';

describe('offsetPeriod()', () => {
it('throws an error if `periodType` is not provided', () => {
expect(() => offsetPeriod([], { offset: 1 })).toThrow("'periodType' is required");
});

it('throws an error if `offset` is not provided', () => {
expect(() => offsetPeriod([], { periodType: 'day' })).toThrow("'offset' is required");
});

it('`periodType` is case insensitive', () => {
const analytics = [
{ dataElement: 'BCD1', organisationUnit: 'TO', period: '20190101', value: 1 },
];
const offset = 1;

const resultsForLowerPeriodType = offsetPeriod(analytics, { periodType: 'year', offset });
const resultsForUpperPeriodType = offsetPeriod(analytics, { periodType: 'YEAR', offset });
expect(resultsForLowerPeriodType).toStrictEqual(resultsForUpperPeriodType);
});

describe('adds the specified offset to every analytic', () => {
const analytics = arrayToAnalytics([
['BCD1', 'TO', '20170101', 1],
['BCD2', 'TO', '20190505', 2],
['BCD1', 'PG', '20301231', 3],
]);
const periodType = 'year';
const testData = [
[
-2,
[
['BCD1', 'TO', '20150101', 1],
['BCD2', 'TO', '20170505', 2],
['BCD1', 'PG', '20281231', 3],
],
],
[
-1,
[
['BCD1', 'TO', '20160101', 1],
['BCD2', 'TO', '20180505', 2],
['BCD1', 'PG', '20291231', 3],
],
],
[
0,
[
['BCD1', 'TO', '20170101', 1],
['BCD2', 'TO', '20190505', 2],
['BCD1', 'PG', '20301231', 3],
],
],
[
+1,
[
['BCD1', 'TO', '20180101', 1],
['BCD2', 'TO', '20200505', 2],
['BCD1', 'PG', '20311231', 3],
],
],
[
+2,
[
['BCD1', 'TO', '20190101', 1],
['BCD2', 'TO', '20210505', 2],
['BCD1', 'PG', '20321231', 3],
],
],
];

it.each(testData)('%s', (offset, expected) => {
const expectedAnalytics = arrayToAnalytics(expected);
expect(offsetPeriod(analytics, { periodType, offset })).toStrictEqual(expectedAnalytics);
});
});

describe('supports multiple period types', () => {
const testData = [
[
'year',
{
periodType: 'year',
previous: '20190102',
next: '20200102',
},
],
[
'quarter',
{
periodType: 'quarter',
previous: '20190202',
next: '20190502',
},
],
[
'month',
{
periodType: 'month',
previous: '20191231',
next: '20200131',
},
],

[
'week',
{
periodType: 'week',
previous: '20190102',
next: '20190109',
},
],
[
'day',
{
periodType: 'day',
previous: '20191231',
next: '20200101',
},
],
[
'day - leap year',
{
periodType: 'day',
previous: '20200229',
next: '20200301',
},
],
];

const createAnalytic = period => ({
dataElement: 'BCD1',
organisationUnit: 'TO',
period,
value: 1,
});

const assertPeriodOffsetIsApplied = (inputPeriod, aggregationConfig, expectedPeriod) => {
const analytics = [createAnalytic(inputPeriod)];
const expected = [createAnalytic(expectedPeriod)];
expect(offsetPeriod(analytics, aggregationConfig)).toStrictEqual(expected);
};

it.each(testData)('%s', (_, { periodType, previous, next }) => {
assertPeriodOffsetIsApplied(previous, { periodType, offset: +1 }, next);
assertPeriodOffsetIsApplied(next, { periodType, offset: -1 }, previous);
});

it('month - different total days between months', () => {
const periodType = 'month';
// This is an edge case where applying a - offset is not a reversible with a + offset
assertPeriodOffsetIsApplied('20190531', { periodType, offset: -1 }, '20190430');
assertPeriodOffsetIsApplied('20190430', { periodType, offset: +1 }, '20190530');
});
});
});
1 change: 1 addition & 0 deletions packages/aggregator/src/aggregationTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const AGGREGATION_TYPES = {
FINAL_EACH_YEAR: 'FINAL_EACH_YEAR',
MOST_RECENT_PER_ORG_GROUP: 'MOST_RECENT_PER_ORG_GROUP',
MOST_RECENT: 'MOST_RECENT',
OFFSET_PERIOD: 'OFFSET_PERIOD',
RAW: 'RAW',
REPLACE_ORG_UNIT_WITH_ORG_GROUP: 'REPLACE_ORG_UNIT_WITH_ORG_GROUP',
SUM_EACH_QUARTER: 'SUM_EACH_QUARTER',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
filterLatest,
getFinalValuePerPeriod,
getSumValuePerPeriod,
offsetPeriod,
replaceOrgUnitWithOrgGroup,
sumAcrossPeriods,
sumEachDataElement,
Expand Down Expand Up @@ -38,6 +39,8 @@ export const aggregateAnalytics = (
});
case AGGREGATION_TYPES.SUM_MOST_RECENT_PER_FACILITY:
return sumEachDataElement(filterLatest(analytics, aggregationConfig));
case AGGREGATION_TYPES.OFFSET_PERIOD:
return offsetPeriod(analytics, aggregationConfig);
case AGGREGATION_TYPES.FINAL_EACH_DAY:
return getFinalValuePerPeriod(analytics, aggregationConfig, DAY);
case AGGREGATION_TYPES.FINAL_EACH_DAY_FILL_EMPTY_DAYS:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

export { filterLatest } from './filterLatest';
export { getFinalValuePerPeriod } from './getFinalValuePerPeriod';
export { getSumValuePerPeriod } from './getSumValuePerPeriod';
export { sumAcrossPeriods } from './sumAcrossPeriods';
export { sumEachDataElement } from './sumEachDataElement';
export { sumPreviousPerPeriod } from './sumPreviousPerPeriod';
export {
sumPerOrgGroup,
sumPerPeriodPerOrgGroup,
countPerOrgGroup,
countPerPeriodPerOrgGroup,
} from './entityAggregations';
export { filterLatest } from './filterLatest';
export { getFinalValuePerPeriod } from './getFinalValuePerPeriod';
export { getSumValuePerPeriod } from './getSumValuePerPeriod';
export { offsetPeriod } from './offsetPeriod';
export { sumAcrossPeriods } from './sumAcrossPeriods';
export { sumEachDataElement } from './sumEachDataElement';
export { sumPreviousPerPeriod } from './sumPreviousPerPeriod';
export { replaceOrgUnitWithOrgGroup } from './replaceOrgUnitWithOrgGroup';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { momentToPeriod, periodToType, periodTypeToMomentUnit, utcMoment } from '@tupaia/utils';

class RequiredConfigFieldError extends Error {
constructor(fieldKey) {
super(`'${fieldKey}' is required in offsetPeriod() config`);
this.name = 'RequiredConfigFieldError';
}
}

export const offsetPeriod = (analytics, aggregationConfig = {}) => {
const { periodType, offset } = aggregationConfig;
if (!periodType) {
throw new RequiredConfigFieldError('periodType');
}
if (!offset && offset !== 0) {
throw new RequiredConfigFieldError('offset');
}

const momentUnit = periodTypeToMomentUnit(periodType.toUpperCase());
return analytics.map(analytic => {
const { period } = analytic;
const momentWithOffset = utcMoment(period).add(offset, momentUnit);
const currentPeriodType = periodToType(period);
return { ...analytic, period: momentToPeriod(momentWithOffset, currentPeriodType) };
});
};