diff --git a/src/loaders/__tests__/thematicLoader.spec.js b/src/loaders/__tests__/thematicLoader.spec.js new file mode 100644 index 000000000..62e84f291 --- /dev/null +++ b/src/loaders/__tests__/thematicLoader.spec.js @@ -0,0 +1,58 @@ +import { getValuesByPeriod, getValueById } from '../thematicLoader.js' + +const headers = [{ name: 'pe' }, { name: 'ou' }, { name: 'value' }] + +describe('thematicLoader value extraction', () => { + // Analytics returns row values as strings, and computed items (indicators, + // calculations) serialize whole numbers with a trailing ".0" (e.g. "16082.0"). + // Both extractors must yield numbers so integers render without decimals. + // See DHIS2-21037. + + test('getValueById coerces string values to numbers', () => { + const data = { + headers, + rows: [ + ['202501', 'ou1', '16082.0'], + ['202501', 'ou2', '118.13'], + ], + } + + expect(getValueById(data)).toEqual({ + ou1: 16082, + ou2: 118.13, + }) + }) + + test('getValuesByPeriod coerces string values to numbers per period', () => { + const data = { + headers, + rows: [ + ['202501', 'ou1', '16082.0'], + ['202501', 'ou2', '118.13'], + ['202502', 'ou1', '238.0'], + ], + } + + expect(getValuesByPeriod(data)).toEqual({ + 202501: { + ou1: { value: 16082 }, + ou2: { value: 118.13 }, + }, + 202502: { + ou1: { value: 238 }, + }, + }) + }) + + test('getValuesByPeriod and getValueById agree on the same value', () => { + const data = { + headers, + rows: [['202501', 'ou1', '238.0']], + } + + const byId = getValueById(data) + const byPeriod = getValuesByPeriod(data) + + expect(byPeriod['202501'].ou1.value).toBe(byId.ou1) + }) +}) diff --git a/src/loaders/thematicLoader.js b/src/loaders/thematicLoader.js index 860570a70..5854f4eaa 100644 --- a/src/loaders/thematicLoader.js +++ b/src/loaders/thematicLoader.js @@ -345,7 +345,7 @@ const getPeriodsFromMetaData = ({ dimensions, items }) => } }) -const getValuesByPeriod = (data) => { +export const getValuesByPeriod = (data) => { const { headers, rows } = data const periodIndex = findIndex(['name', 'pe'], headers) const ouIndex = findIndex(['name', 'ou'], headers) @@ -355,20 +355,20 @@ const getValuesByPeriod = (data) => { const period = row[periodIndex] const periodObj = (obj[period] = obj[period] || {}) periodObj[row[ouIndex]] = { - value: row[valueIndex], + value: Number.parseFloat(row[valueIndex]), } return obj }, {}) } // Returns an object mapping org. units and values -const getValueById = (data) => { +export const getValueById = (data) => { const { headers, rows } = data const ouIndex = findIndex(['name', 'ou'], headers) const valueIndex = findIndex(['name', 'value'], headers) return rows.reduce((obj, row) => { - obj[row[ouIndex]] = parseFloat(row[valueIndex]) + obj[row[ouIndex]] = Number.parseFloat(row[valueIndex]) return obj }, {}) } @@ -378,7 +378,9 @@ const getOrderedValues = (data) => { const { headers, rows } = data const valueIndex = findIndex(['name', 'value'], headers) - return rows.map((row) => parseFloat(row[valueIndex])).sort((a, b) => a - b) + return rows + .map((row) => Number.parseFloat(row[valueIndex])) + .sort((a, b) => a - b) } // Load features and data values from api