-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add french i18n validation (#308)
* feat: add french i18n validation --------- Co-authored-by: Jovial Joe Jayarson <[email protected]>
- Loading branch information
Showing
4 changed files
with
227 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
"""France.""" | ||
|
||
# standard | ||
from functools import lru_cache | ||
import re | ||
import typing | ||
|
||
# local | ||
from validators.utils import validator | ||
|
||
|
||
@lru_cache | ||
def _ssn_pattern(): | ||
"""SSN Pattern.""" | ||
return re.compile( | ||
r"^([1,2])" # gender (1=M, 2=F) | ||
r"\s(\d{2})" # year of birth | ||
r"\s(0[1-9]|1[0-2])" # month of birth | ||
r"\s(\d{2,3}|2[A,B])" # department of birth | ||
r"\s(\d{2,3})" # town of birth | ||
r"\s(\d{3})" # registration number | ||
r"(?:\s(\d{2}))?$", # control key (may or may not be provided) | ||
re.VERBOSE, | ||
) | ||
|
||
|
||
@validator | ||
def fr_department(value: typing.Union[str, int]): | ||
"""Validate a french department number. | ||
Examples: | ||
>>> fr_department(20) # can be an integer | ||
# Output: True | ||
>>> fr_department("20") | ||
# Output: True | ||
>>> fr_department("971") # Guadeloupe | ||
# Output: True | ||
>>> fr_department("00") | ||
# Output: ValidationError(func=fr_department, args=...) | ||
>>> fr_department('2A') # Corsica | ||
# Output: True | ||
>>> fr_department('2B') | ||
# Output: True | ||
>>> fr_department('2C') | ||
# Output: ValidationError(func=fr_department, args=...) | ||
Args: | ||
value: | ||
French department number to validate. | ||
Returns: | ||
(Literal[True]): | ||
If `value` is a valid french department number. | ||
(ValidationError): | ||
If `value` is an invalid french department number. | ||
> *New in version 0.23.0*. | ||
""" | ||
if not value: | ||
return False | ||
if isinstance(value, str): | ||
if value in ("2A", "2B"): # Corsica | ||
return True | ||
try: | ||
value = int(value) | ||
except ValueError: | ||
return False | ||
return 1 <= value <= 19 or 21 <= value <= 95 or 971 <= value <= 976 # Overseas departments | ||
|
||
|
||
@validator | ||
def fr_ssn(value: str): | ||
"""Validate a french Social Security Number. | ||
Each french citizen has a distinct Social Security Number. | ||
For more information see [French Social Security Number][1] (sadly unavailable in english). | ||
[1]: https://fr.wikipedia.org/wiki/Num%C3%A9ro_de_s%C3%A9curit%C3%A9_sociale_en_France | ||
Examples: | ||
>>> fr_ssn('1 84 12 76 451 089 46') | ||
# Output: True | ||
>>> fr_ssn('1 84 12 76 451 089') # control key is optional | ||
# Output: True | ||
>>> fr_ssn('3 84 12 76 451 089 46') # wrong gender number | ||
# Output: ValidationError(func=fr_ssn, args=...) | ||
>>> fr_ssn('1 84 12 76 451 089 47') # wrong control key | ||
# Output: ValidationError(func=fr_ssn, args=...) | ||
Args: | ||
value: | ||
French Social Security Number string to validate. | ||
Returns: | ||
(Literal[True]): | ||
If `value` is a valid french Social Security Number. | ||
(ValidationError): | ||
If `value` is an invalid french Social Security Number. | ||
> *New in version 0.23.0*. | ||
""" | ||
if not value: | ||
return False | ||
matched = re.match(_ssn_pattern(), value) | ||
if not matched: | ||
return False | ||
groups = list(matched.groups()) | ||
control_key = groups[-1] | ||
department = groups[3] | ||
if department != "99" and not fr_department(department): | ||
# 99 stands for foreign born people | ||
return False | ||
if control_key is None: | ||
# no control key provided, no additional check needed | ||
return True | ||
if len(department) == len(groups[4]): | ||
# if the department number is 3 digits long (overseas departments), | ||
# the town number must be 2 digits long | ||
# and vice versa | ||
return False | ||
if department in ("2A", "2B"): | ||
# Corsica's department numbers are not in the same range as the others | ||
# thus 2A and 2B are replaced by 19 and 18 respectively to compute the control key | ||
groups[3] = "19" if department == "2A" else "18" | ||
# the control key is valid if it is equal to 97 - (the first 13 digits modulo 97) | ||
digits = int("".join(groups[:-1])) | ||
return int(control_key) == (97 - (digits % 97)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
"""Test French validators.""" | ||
|
||
# standard | ||
from typing import Union | ||
|
||
# external | ||
import pytest | ||
|
||
# local | ||
from validators import ValidationError | ||
from validators.i18n.fr import fr_department, fr_ssn | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("value",), | ||
[ | ||
("1 84 12 76 451 089 46",), | ||
("1 84 12 76 451 089",), # control key is optional | ||
("2 99 05 75 202 818 97",), | ||
("2 99 05 75 202 817 01",), | ||
("2 99 05 2A 202 817 58",), | ||
("2 99 05 2B 202 817 85",), | ||
("2 99 05 971 12 817 70",), | ||
], | ||
) | ||
def test_returns_true_on_valid_ssn(value: str): | ||
"""Test returns true on valid ssn.""" | ||
assert fr_ssn(value) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("value",), | ||
[ | ||
(None,), | ||
("",), | ||
("3 84 12 76 451 089 46",), # wrong gender number | ||
("1 84 12 76 451 089 47",), # wrong control key | ||
("1 84 00 76 451 089",), # invalid month | ||
("1 84 13 76 451 089",), # invalid month | ||
("1 84 12 00 451 089",), # invalid department | ||
("1 84 12 2C 451 089",), | ||
("1 84 12 98 451 089",), # invalid department | ||
# ("1 84 12 971 451 089",), # ? | ||
], | ||
) | ||
def test_returns_failed_validation_on_invalid_ssn(value: str): | ||
"""Test returns failed validation on invalid_ssn.""" | ||
assert isinstance(fr_ssn(value), ValidationError) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("value",), | ||
[ | ||
("01",), | ||
("2A",), # Corsica | ||
("2B",), | ||
(14,), | ||
("95",), | ||
("971",), | ||
(971,), | ||
], | ||
) | ||
def test_returns_true_on_valid_department(value: Union[str, int]): | ||
"""Test returns true on valid department.""" | ||
assert fr_department(value) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("value",), | ||
[ | ||
(None,), | ||
("",), | ||
("00",), | ||
(0,), | ||
("2C",), | ||
("97",), | ||
("978",), | ||
("98",), | ||
("96",), | ||
("20",), | ||
(20,), | ||
], | ||
) | ||
def test_returns_failed_validation_on_invalid_department(value: Union[str, int]): | ||
"""Test returns failed validation on invalid department.""" | ||
assert isinstance(fr_department(value), ValidationError) |