Files
Investidor-V2/resources/js/lib/demographic-reading.ts
T
2026-11-15 18:56:39 +02:00

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),
),
};
}