All files / scripts/statskontoret/domain headcount.ts

96% Statements 24/25
76.92% Branches 20/26
100% Functions 6/6
100% Lines 22/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77                                                            3x 3x 10x 10x   10x         10x     10x 10x 10x 10x 10x 10x 10x     3x   7x 7x             6x             4x 2x 1x   4x 2x    
/**
 * @module scripts/statskontoret/domain/headcount
 * @description Headcount/authority-count aggregation derived from
 * Statskontoret myndighetsförteckning workbooks.
 *
 * Consumes the records produced by `rowsToRecords` and groups by
 * `(year, department)` returning a sorted, deterministic time series.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import { rowsToRecords } from '../extractors/rows-to-records.js';
import {
  buildRecordLookup,
  findField,
  parseStatskontoretOptionalInt,
  parseStatskontoretSwedishNumber,
  roundOneDecimal,
} from '../internal/text.js';
import type {
  StatskontoretHeadcountOptions,
  StatskontoretHeadcountRow,
  StatskontoretWorkbook,
} from '../types.js';
 
export function aggregateHeadcountByDepartment(
  records: readonly Record<string, string>[],
  fallbackYear?: number,
): StatskontoretHeadcountRow[] {
  const aggregate = new Map<string, { headcount: number; authorities: Set<string> }>();
  for (const record of records) {
    const lookup = buildRecordLookup(record);
    const year =
      parseStatskontoretOptionalInt(findField(lookup, ['år', 'ar', 'year']) ?? '') ?? fallbackYear;
    const department = findField(lookup, [
      'departement',
      'departementstillhörighet',
      'departementstillhorighet',
    ])?.trim();
    const headcountValue = parseStatskontoretSwedishNumber(
      findField(lookup, ['årsarbetskrafter', 'arsarbetskrafter', 'åa', 'aa']) ?? '',
    );
    Iif (!year || !department || headcountValue === undefined) continue;
    const authority = findField(lookup, ['myndighet', 'myndighetsnamn', 'namn'])?.trim() ?? '';
    const key = `${year}::${department}`;
    const current = aggregate.get(key) ?? { headcount: 0, authorities: new Set<string>() };
    current.headcount += headcountValue;
    Eif (authority) current.authorities.add(authority);
    aggregate.set(key, current);
  }
 
  return [...aggregate.entries()]
    .map(([key, value]) => {
      const [yearRaw, department] = key.split('::');
      return {
        year: Number.parseInt(yearRaw, 10),
        department,
        headcount: roundOneDecimal(value.headcount),
        authorityCount: value.authorities.size,
      };
    })
    .sort((a, b) => a.year - b.year || a.department.localeCompare(b.department, 'sv'));
}
 
export function buildHeadcountTimeSeries(
  workbook: StatskontoretWorkbook,
  options: StatskontoretHeadcountOptions = {},
): StatskontoretHeadcountRow[] {
  const sheet = options.sheetNamePattern
    ? workbook.sheets.find((candidate) => options.sheetNamePattern?.test(candidate.name))
    : workbook.sheets.find((candidate) => /förteckning|forteckning/i.test(candidate.name)) ??
      workbook.sheets[0];
  if (!sheet) return [];
  return aggregateHeadcountByDepartment(rowsToRecords(sheet.rows), options.fallbackYear);
}