125 lines
3.9 KiB
TypeScript
125 lines
3.9 KiB
TypeScript
import type { DemographicAnalysis } from '../components/demographic-evidence';
|
|
|
|
type Series = DemographicAnalysis['territories'][number]['series'][number];
|
|
type Row = Series['values'][number];
|
|
const children = ['0-4', '5-9', '10-14'];
|
|
const adults = [
|
|
'15-19',
|
|
'20-24',
|
|
'25-29',
|
|
'30-34',
|
|
'35-39',
|
|
'40-44',
|
|
'45-49',
|
|
'50-54',
|
|
'55-59',
|
|
'60-64',
|
|
];
|
|
|
|
// Display-only derivation. Original decimals, persisted evidence and fiscal calculations are untouched.
|
|
function numeric(row: Row | undefined): number | null {
|
|
if (
|
|
!row ||
|
|
!['pessoas', 'persons'].includes(row.unit) ||
|
|
!/^\d+(\.\d+)?$/.test(row.value)
|
|
)
|
|
return null;
|
|
const n = Number(row.value);
|
|
return Number.isFinite(n) && n <= Number.MAX_SAFE_INTEGER ? n : null;
|
|
}
|
|
function one(rows: Row[], age: string, residence = 'all'): Row | undefined {
|
|
const found = rows.filter(
|
|
(r) =>
|
|
r.indicator === 'population' &&
|
|
r.age_group === age &&
|
|
r.residence === residence,
|
|
);
|
|
return found.length === 1 ? found[0] : undefined;
|
|
}
|
|
export const populationLabel = (n: number | null) =>
|
|
n === null
|
|
? 'Sem dados'
|
|
: new Intl.NumberFormat('pt-MZ', { maximumFractionDigits: 0 }).format(
|
|
n,
|
|
);
|
|
export const percentageLabel = (n: number | null) =>
|
|
n === null
|
|
? 'Sem dados'
|
|
: new Intl.NumberFormat('pt-MZ', {
|
|
maximumFractionDigits: 1,
|
|
minimumFractionDigits: 1,
|
|
}).format(n) + '%';
|
|
|
|
export function demographicReading(series: Series, start: number, end: number) {
|
|
function year(year: number) {
|
|
const rows = series.values.filter((r) => r.year === year);
|
|
const totalRow = one(rows, 'all');
|
|
const total = numeric(totalRow);
|
|
const compatible = (r: Row | undefined) =>
|
|
r &&
|
|
totalRow &&
|
|
r.unit === totalRow.unit &&
|
|
r.data_kind === totalRow.data_kind;
|
|
const part = (age: string, residence = 'all') => {
|
|
const r = one(rows, age, residence);
|
|
return compatible(r) ? numeric(r) : null;
|
|
};
|
|
const sum = (ages: string[]) => {
|
|
const values = ages.map((age) => part(age));
|
|
return values.every((v) => v !== null)
|
|
? values.reduce<number>((a, v) => a + (v ?? 0), 0)
|
|
: null;
|
|
};
|
|
const share = (n: number | null) =>
|
|
total !== null && total > 0 && n !== null && n <= total
|
|
? (n / total) * 100
|
|
: null;
|
|
const urban = part('all', 'urban'),
|
|
rural = part('all', 'rural');
|
|
const residenceReconciled =
|
|
total !== null &&
|
|
urban !== null &&
|
|
rural !== null &&
|
|
Math.abs(urban + rural - total) <=
|
|
Math.max(0.000001, total * 1e-10);
|
|
const under15 = sum(children),
|
|
age15to64 = sum(adults);
|
|
return {
|
|
year,
|
|
total,
|
|
kind: totalRow?.data_kind,
|
|
unit: totalRow?.unit,
|
|
urban,
|
|
rural,
|
|
urbanShare: residenceReconciled ? share(urban) : null,
|
|
ruralShare: residenceReconciled ? share(rural) : null,
|
|
under15,
|
|
under15Share: share(under15),
|
|
age15to64,
|
|
age15to64Share: share(age15to64),
|
|
};
|
|
}
|
|
const first = year(start),
|
|
last = year(end);
|
|
const comparable =
|
|
end > start &&
|
|
first.total !== null &&
|
|
last.total !== null &&
|
|
first.kind === last.kind &&
|
|
first.unit === last.unit;
|
|
const delta = comparable ? last.total! - first.total! : null;
|
|
const growth =
|
|
delta !== null && first.total! > 0
|
|
? (delta / first.total!) * 100
|
|
: null;
|
|
return {
|
|
first,
|
|
last,
|
|
delta,
|
|
growth,
|
|
years: Array.from({ length: end - start + 1 }, (_, i) =>
|
|
year(start + i),
|
|
),
|
|
};
|
|
}
|