Skip to content

Commit dcf73fe

Browse files
committed
feat: add french i18n validation
1 parent 15984e8 commit dcf73fe

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

src/validators/i18n/fr.py

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

tests/i18n/test_fr.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Test French validators."""
2+
3+
import pytest
4+
from validators import ValidationError
5+
from validators.i18n.fr import fr_department, fr_ssn
6+
7+
8+
@pytest.mark.parametrize(
9+
("value",),
10+
[
11+
("1 84 12 76 451 089 46",),
12+
("1 84 12 76 451 089",), # control key is optional
13+
("2 99 05 75 202 818 97",),
14+
("2 99 05 75 202 817 01",),
15+
("2 99 05 2A 202 817 58",),
16+
("2 99 05 2B 202 817 85",),
17+
("2 99 05 971 12 817 70",),
18+
],
19+
)
20+
def test_returns_true_on_valid_ssn(value: str):
21+
"""Test returns true on valid ssn."""
22+
assert fr_ssn(value)
23+
24+
25+
@pytest.mark.parametrize(
26+
("value",),
27+
[
28+
(None,),
29+
("",),
30+
("3 84 12 76 451 089 46",), # wrong gender number
31+
("1 84 12 76 451 089 47",), # wrong control key
32+
("1 84 00 76 451 089",), # invalid month
33+
("1 84 13 76 451 089",), # invalid month
34+
("1 84 12 00 451 089",), # invalid department
35+
("1 84 12 2C 451 089",),
36+
("1 84 12 98 451 089",), # invalid department
37+
("1 84 12 971 451 089",),
38+
],
39+
)
40+
def test_returns_failed_validation_on_invalid_ssn(value: str):
41+
"""Test returns failed validation on invalid_ssn."""
42+
assert isinstance(fr_ssn(value), ValidationError)
43+
44+
45+
@pytest.mark.parametrize(
46+
("value",),
47+
[
48+
("01",),
49+
("2A",), # Corsica
50+
("2B",),
51+
(14,),
52+
("95",),
53+
("971",),
54+
(971,),
55+
],
56+
)
57+
def test_returns_true_on_valid_department(value: str | int):
58+
"""Test returns true on valid department."""
59+
assert fr_department(value)
60+
61+
62+
@pytest.mark.parametrize(
63+
("value",),
64+
[
65+
(None,),
66+
("",),
67+
("00",),
68+
(0,),
69+
("2C",),
70+
("97",),
71+
("978",),
72+
("98",),
73+
("96",),
74+
("20",),
75+
(20,),
76+
],
77+
)
78+
def test_returns_failed_validation_on_invalid_department(value: str | int):
79+
"""Test returns failed validation on invalid department."""
80+
assert isinstance(fr_department(value), ValidationError)

0 commit comments

Comments
 (0)