1"""France."""
2
3# standard
4from functools import lru_cache
5import re
6import typing
7
8# local
9from validators.utils import validator
10
11
12@lru_cache
13def _ssn_pattern():
14 """SSN Pattern."""
15 return re.compile(
16 r"^([1,2])" # gender (1=M, 2=F)
17 r"\s(\d{2})" # year of birth
18 r"\s(0[1-9]|1[0-2])" # month of birth
19 r"\s(\d{2,3}|2[A,B])" # department of birth
20 r"\s(\d{2,3})" # town of birth
21 r"\s(\d{3})" # registration number
22 r"(?:\s(\d{2}))?$", # control key (may or may not be provided)
23 re.VERBOSE,
24 )
25
26
27@validator
28def fr_department(value: typing.Union[str, int]):
29 """Validate a french department number.
30
31 Examples:
32 >>> fr_department(20) # can be an integer
33 ValidationError(func=fr_department, args={'value': 20})
34 >>> fr_department("20")
35 ValidationError(func=fr_department, args={'value': '20'})
36 >>> fr_department("971") # Guadeloupe
37 True
38 >>> fr_department("00")
39 ValidationError(func=fr_department, args={'value': '00'})
40 >>> fr_department('2A') # Corsica
41 True
42 >>> fr_department('2B')
43 True
44 >>> fr_department('2C')
45 ValidationError(func=fr_department, args={'value': '2C'})
46
47 Args:
48 value:
49 French department number to validate.
50
51 Returns:
52 (Literal[True]): If `value` is a valid french department number.
53 (ValidationError): If `value` is an invalid french department number.
54 """
55 if not value:
56 return False
57 if isinstance(value, str):
58 if value in ("2A", "2B"): # Corsica
59 return True
60 try:
61 value = int(value)
62 except ValueError:
63 return False
64 return 1 <= value <= 19 or 21 <= value <= 95 or 971 <= value <= 976 # Overseas departments
65
66
67@validator
68def fr_ssn(value: str):
69 """Validate a french Social Security Number.
70
71 Each french citizen has a distinct Social Security Number.
72 For more information see [French Social Security Number][1] (sadly unavailable in english).
73
74 [1]: https://fr.wikipedia.org/wiki/Num%C3%A9ro_de_s%C3%A9curit%C3%A9_sociale_en_France
75
76 Examples:
77 >>> fr_ssn('1 84 12 76 451 089 46')
78 True
79 >>> fr_ssn('1 84 12 76 451 089') # control key is optional
80 True
81 >>> fr_ssn('3 84 12 76 451 089 46') # wrong gender number
82 ValidationError(func=fr_ssn, args={'value': '3 84 12 76 451 089 46'})
83 >>> fr_ssn('1 84 12 76 451 089 47') # wrong control key
84 ValidationError(func=fr_ssn, args={'value': '1 84 12 76 451 089 47'})
85
86 Args:
87 value:
88 French Social Security Number string to validate.
89
90 Returns:
91 (Literal[True]): If `value` is a valid french Social Security Number.
92 (ValidationError): If `value` is an invalid french Social Security Number.
93 """
94 if not value:
95 return False
96 matched = re.match(_ssn_pattern(), value)
97 if not matched:
98 return False
99 groups = list(matched.groups())
100 control_key = groups[-1]
101 department = groups[3]
102 if department != "99" and not fr_department(department):
103 # 99 stands for foreign born people
104 return False
105 if control_key is None:
106 # no control key provided, no additional check needed
107 return True
108 if len(department) == len(groups[4]):
109 # if the department number is 3 digits long (overseas departments),
110 # the town number must be 2 digits long
111 # and vice versa
112 return False
113 if department in ("2A", "2B"):
114 # Corsica's department numbers are not in the same range as the others
115 # thus 2A and 2B are replaced by 19 and 18 respectively to compute the control key
116 groups[3] = "19" if department == "2A" else "18"
117 # the control key is valid if it is equal to 97 - (the first 13 digits modulo 97)
118 digits = int("".join(groups[:-1]))
119 return int(control_key) == (97 - (digits % 97))