mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
v1.7.0 — health-record export (PDF + FHIR), flexible medication schedules, first-paint dashboard snapshot, full HealthKit coverage, offline sync delta feed, Coach data clustering. See CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
/**
|
||
* German KVNR (Krankenversichertennummer) validation.
|
||
*
|
||
* The unveränderbarer Teil of a KVNR is a 10-character identifier:
|
||
* - position 1: one uppercase letter A–Z
|
||
* - positions 2–9: eight digits
|
||
* - position 10: a mod-10 check digit
|
||
*
|
||
* Check-digit algorithm (per the GKV-Spitzenverband specification):
|
||
* 1. Map the leading letter to a two-digit value: A → 01, B → 02, … Z → 26.
|
||
* 2. Concatenate that two-digit value with positions 2–9 to get a
|
||
* 10-digit numeric base.
|
||
* 3. Weight the ten digits alternately 1, 2, 1, 2, … from the left.
|
||
* When a weight-2 product is ≥ 10, replace it with its cross-sum
|
||
* (i.e. subtract 9 — equivalent for single-digit products).
|
||
* 4. Sum the weighted values; the check digit is that sum mod 10.
|
||
* 5. The computed check digit must equal position 10.
|
||
*
|
||
* The function is total: any malformed input (wrong length, lowercase,
|
||
* non-letter lead, non-digit body) returns `false` rather than throwing.
|
||
*/
|
||
|
||
const KVNR_PATTERN = /^[A-Z][0-9]{9}$/;
|
||
|
||
/** Compute the expected KVNR check digit for a `letter + 8 digits` stem. */
|
||
function computeKvnrCheckDigit(letter: string, eightDigits: string): number {
|
||
// Letter → two-digit value (A=01 … Z=26), left-padded.
|
||
const letterValue = letter.charCodeAt(0) - "A".charCodeAt(0) + 1;
|
||
const base = `${String(letterValue).padStart(2, "0")}${eightDigits}`;
|
||
|
||
let sum = 0;
|
||
for (let i = 0; i < base.length; i++) {
|
||
const digit = base.charCodeAt(i) - "0".charCodeAt(0);
|
||
// Alternating 1,2,1,2,… weighting from the left.
|
||
const weighted = i % 2 === 0 ? digit : digit * 2;
|
||
sum += weighted >= 10 ? weighted - 9 : weighted;
|
||
}
|
||
return sum % 10;
|
||
}
|
||
|
||
/**
|
||
* Validate a KVNR string. Accepts the canonical 10-character form
|
||
* (one letter + nine digits, the last being the check digit).
|
||
*
|
||
* Returns `true` only when the format matches AND the trailing check
|
||
* digit matches the computed value.
|
||
*/
|
||
export function isValidKvnr(value: string): boolean {
|
||
if (typeof value !== "string") return false;
|
||
if (!KVNR_PATTERN.test(value)) return false;
|
||
const letter = value[0];
|
||
const eightDigits = value.slice(1, 9);
|
||
const checkDigit = value.charCodeAt(9) - "0".charCodeAt(0);
|
||
return computeKvnrCheckDigit(letter, eightDigits) === checkDigit;
|
||
}
|
||
|
||
/**
|
||
* Normalise free-text KVNR input: strip whitespace + uppercase. Returns
|
||
* the cleaned string (NOT validated — pair with `isValidKvnr`).
|
||
*/
|
||
export function normaliseKvnr(value: string): string {
|
||
return value.replace(/\s+/g, "").toUpperCase();
|
||
}
|