Coverage for presidio_analyzer / predefined_recognizers / country_specific / germany / de_health_insurance_recognizer.py: 100%
28 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
1import re
2from typing import List, Optional
4from presidio_analyzer import Pattern, PatternRecognizer
7class DeHealthInsuranceRecognizer(PatternRecognizer):
8 """
9 Recognizes German statutory health insurance numbers (KVNR).
11 Also called Krankenversicherungsnummer, Krankenversichertennummer, or
12 Versichertennummer.
14 The KVNR is assigned to every person insured under the German statutory
15 health insurance system (gesetzliche Krankenversicherung, GKV). It is printed on the
16 Gesundheitskarte (eGK – elektronische Gesundheitskarte).
18 Legal basis: § 290 SGB V (Sozialgesetzbuch Fünftes Buch – Gesetzliche
19 Krankenversicherung).
20 Data protection: DSGVO Art. 9 (besondere Kategorien personenbezogener Daten –
21 Gesundheitsdaten), BDSG § 22.
23 Format (10 characters):
24 Pos 1: Buchstabe (first letter of birth surname, A–Z)
25 Pos 2–9: 8 digits (birth date encoded + serial)
26 Pos 10: Prüfziffer (check digit, 0–9)
28 Example (fictitious): A123456787
30 Check digit algorithm (GKV-Spitzenverband specification):
31 1. Convert the letter at position 1 to its 2-digit ordinal value
32 (A=01, B=02, …, Z=26), yielding an effective 11-digit string.
33 2. Apply weights [2, 9, 8, 7, 6, 5, 4, 3, 2, 1] to the first 10
34 effective digits (before the check digit at effective position 11).
35 Note: the check digit is the last digit of the original 10-char string
36 (position 10), which maps to effective position 11.
37 3. For each product ≥ 10, replace it with the sum of its digits.
38 4. Sum all 10 values, compute sum mod 10.
39 5. The result must equal the check digit at position 10.
41 :param patterns: List of patterns to be used by this recognizer
42 :param context: List of context words to increase confidence in detection
43 :param supported_language: Language this recognizer supports
44 :param supported_entity: The entity this recognizer can detect
45 """
47 # Accuracy note: The base pattern `[A-Z]\d{9}` is intentionally broad (any
48 # uppercase letter followed by 9 digits) because no more specific structural
49 # constraint exists in the KVNR format beyond length and the leading letter.
50 # The GKV checksum validation in validate_result() is the primary defence
51 # against false positives; the base confidence is therefore kept low (0.3)
52 # and context words are required for high-confidence matches.
53 # Formal accuracy evaluation has not been performed on a labelled dataset.
54 PATTERNS = [
55 Pattern(
56 "Krankenversicherungsnummer KVNR (letter + 9 digits)",
57 r"\b[A-Z]\d{9}\b",
58 0.3,
59 ),
60 ]
62 CONTEXT = [
63 "krankenversicherungsnummer",
64 "krankenversichertennummer",
65 "versichertennummer",
66 "kvnr",
67 "krankenkasse",
68 "krankenversicherung",
69 "gesundheitskarte",
70 "egk",
71 "elektronische gesundheitskarte",
72 "gkv",
73 "gesetzliche krankenversicherung",
74 "krankenversicherungsausweis",
75 "versichertenausweis",
76 "versichertenkarte",
77 "aok",
78 "tkk",
79 "barmer",
80 "dak",
81 ]
83 def __init__(
84 self,
85 patterns: Optional[List[Pattern]] = None,
86 context: Optional[List[str]] = None,
87 supported_language: str = "de",
88 supported_entity: str = "DE_HEALTH_INSURANCE",
89 name: Optional[str] = None,
90 ):
91 patterns = patterns if patterns else self.PATTERNS
92 context = context if context else self.CONTEXT
93 super().__init__(
94 supported_entity=supported_entity,
95 patterns=patterns,
96 context=context,
97 supported_language=supported_language,
98 name=name,
99 )
101 def validate_result(self, pattern_text: str) -> Optional[bool]:
102 """
103 Validate the KVNR using the GKV-Spitzenverband checksum algorithm.
105 Algorithm source: GKV-Spitzenverband technical specification (§ 290 SGB V).
107 :param pattern_text: the text to validate (10 characters: 1 letter + 9 digits)
108 :return: True if valid, False if invalid
109 """
110 pattern_text = pattern_text.upper().strip()
112 if len(pattern_text) != 10:
113 return False
115 if not re.match(r"^[A-Z]\d{9}$", pattern_text):
116 return False
118 letter = pattern_text[0]
119 letter_val = str(ord(letter) - ord("A") + 1).zfill(2)
121 # Effective 11-digit string: 2 (from letter) + 8 data digits + 1 check digit
122 # We apply weights to the first 10 effective positions (before check digit)
123 effective = letter_val + pattern_text[1:9] # 2 + 8 = 10 digits
125 check_digit = int(pattern_text[9])
126 weights = [2, 9, 8, 7, 6, 5, 4, 3, 2, 1]
128 total = 0
129 for digit_char, weight in zip(effective, weights):
130 product = int(digit_char) * weight
131 if product >= 10:
132 product = (product // 10) + (product % 10)
133 total += product
135 return (total % 10) == check_digit