Files
HealthLog/src/lib/validations/kvnr.ts
T
Marc-André BombeckandClaude Opus 4.8 50e0a65228 chore(release): v1.7.0
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>
2026-05-31 14:36:17 +02:00

64 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
}