-
Notifications
You must be signed in to change notification settings - Fork 168
feat: add french i18n validation #308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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.""" | ||
|
|
||
yozachar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # standard | ||
| from typing import Union | ||
|
|
||
| # external | ||
| import pytest | ||
yozachar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # 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) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.