Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
/**
|
||
* Medelantal anställda: time-weighted FTE average across a fiscal period.
|
||
*
|
||
* Per ÅRL 5:20 § the medelantal disclosure is the average number of
|
||
* full-time-equivalent employees over the räkenskapsår, not a snapshot
|
||
* headcount. An employee hired on July 1 of a calendar-year FY counts as
|
||
* 0.5; an employee on 50 % degree employed all year counts as 0.5; an
|
||
* employee hired Mar 1 at 80 % and terminated Aug 31 counts as
|
||
* (184 days / 365 days) × 0.80 ≈ 0.40.
|
||
*
|
||
* Inputs come from public.employees:
|
||
* - employment_start: required (DATE NOT NULL)
|
||
* - employment_end: optional (DATE), when null, employee is still active
|
||
* on the period end date
|
||
* - employment_degree: 0 < degree <= 100, default 100
|
||
*
|
||
* The result is rounded to the nearest whole employee per Swedish ÅR
|
||
* disclosure convention (Skatteverket / FAR practice; ÅRL doesn't specify
|
||
* a precision but whole numbers are universal in K2 ÅR for mindre företag).
|
||
*/
|
||
|
||
export interface EmployeePeriodInput {
|
||
employment_start: string
|
||
employment_end: string | null
|
||
/** 0 < degree <= 100. 100 = full-time. */
|
||
employment_degree: number
|
||
}
|
||
|
||
/** Inclusive day count between two ISO dates (UTC). 2025-01-01..2025-12-31 = 365. */
|
||
function inclusiveDays(startIso: string, endIso: string): number {
|
||
const start = new Date(`${startIso}T00:00:00Z`)
|
||
const end = new Date(`${endIso}T00:00:00Z`)
|
||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return 0
|
||
if (end < start) return 0
|
||
return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1
|
||
}
|
||
|
||
/**
|
||
* Compute the FTE-weighted medelantal anställda for a fiscal period.
|
||
*
|
||
* @param employees rows with employment_start / employment_end / employment_degree
|
||
* @param periodStartIso fiscal period start (inclusive), ISO YYYY-MM-DD
|
||
* @param periodEndIso fiscal period end (inclusive), ISO YYYY-MM-DD
|
||
* @returns rounded whole-number medelantal, or 0 if no qualifying overlap
|
||
*/
|
||
export function computeMedelantalAnstallda(
|
||
employees: EmployeePeriodInput[],
|
||
periodStartIso: string,
|
||
periodEndIso: string,
|
||
): number {
|
||
const periodDays = inclusiveDays(periodStartIso, periodEndIso)
|
||
if (periodDays === 0) return 0
|
||
|
||
let totalFteDays = 0
|
||
for (const e of employees) {
|
||
const overlapStart =
|
||
e.employment_start > periodStartIso ? e.employment_start : periodStartIso
|
||
const employmentEnd = e.employment_end ?? periodEndIso
|
||
const overlapEnd =
|
||
employmentEnd < periodEndIso ? employmentEnd : periodEndIso
|
||
if (overlapStart > overlapEnd) continue
|
||
const overlapDays = inclusiveDays(overlapStart, overlapEnd)
|
||
const degreeFactor = Math.min(100, Math.max(0, e.employment_degree)) / 100
|
||
totalFteDays += overlapDays * degreeFactor
|
||
}
|
||
|
||
return Math.round(totalFteDays / periodDays)
|
||
}
|