Created
February 14, 2013 13:55
-
-
Save bmispelon/4952993 to your computer and use it in GitHub Desktop.
French social security number validation
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
| from collections import namedtuple | |
| FrenchSSNTuple = namedtuple('FrenchSSNTuple', [ | |
| 'sex', | |
| 'year', | |
| 'month', | |
| 'departement', | |
| 'city', | |
| 'country', | |
| 'id_month_city', | |
| 'control_key', | |
| ]) | |
| def split_SSN(ssn): | |
| """ | |
| Split a French SSN into its parts, according to the rules described on: | |
| https://fr.wikipedia.org/wiki/Num%C3%A9ro_de_s%C3%A9curit%C3%A9_sociale_en_France#Signification_des_chiffres_du_NIR | |
| The order in which fields are returned allows to use ''.join(*t) to get | |
| the SSN back. | |
| """ | |
| assert len(ssn) == 15 | |
| ssb = ssn.lower() | |
| sex = ssn[:1] | |
| year = ssn[1:3] | |
| month = ssn[3:5] | |
| location = ssn[5:10] | |
| id_month_city = ssn[10:13] | |
| control_key = ssn[13:] | |
| dpt = location[:2] | |
| if dpt.isdigit() and 1 <= int(dpt) <= 95 or dpt in ('2a', '2b'): | |
| departement = location[:2] | |
| city = location[2:] | |
| country = '' | |
| elif dpt.isdigit() and 97 <= int(dpt) <= 98: | |
| departement = location[:3] | |
| city = location[3:] | |
| country = '' | |
| elif dpt == '99': | |
| departement = '99' | |
| city = '' | |
| country = location[2:] | |
| else: | |
| raise ValueError('Invalid location for SSN: %s' % location) | |
| return (sex, year, month, departement, city, country, id_month_city, control_key) | |
| class FrenchSSN(FrenchSSNTuple): | |
| @classmethod | |
| def fromstring(cls, ssn): | |
| splitted = split_SSN(ssn) | |
| return cls(*splitted) | |
| def __str__(self): | |
| return ''.join(self) | |
| @property | |
| def valid(self): | |
| return all( | |
| getattr(self, '_validate_%s' % field, lambda: True)() | |
| for field in self._fields | |
| ) | |
| def _validate_sex(self): | |
| return len(self.sex) == 1 and self.sex in '123' | |
| def _validate_year(self): | |
| return len(self.year) == 2 and self.year.isdigit() | |
| def _validate_month(self): | |
| try: | |
| month = int(self.month) | |
| except ValueError: | |
| return False | |
| return ( | |
| ( 1 <= month <= 12) or | |
| (20 <= month <= 42) or | |
| (50 <= month <= 99) | |
| ) | |
| def _validate_departement(self): | |
| try: | |
| dpt = int(self.departement) | |
| except ValueError: | |
| return dpt in ('2a', '2b') # Corsica | |
| return ( | |
| (1 <= dpt <= 95) or # France (mainland) | |
| (970 < dpt <= 989) or # France (outre-mer) | |
| (dpt == 99) # abroad | |
| ) | |
| def _validate_city(self): | |
| return True # TODO | |
| def _validate_country(self): | |
| return True # TODO | |
| def _validate_id_month_city(self): | |
| try: | |
| id_month_city = int(self.id_month_city) | |
| except ValueError: | |
| return False | |
| return 1 <= id_month_city <= 999 | |
| def _validate_control_key(self): | |
| ssn_without_key = self._replace( | |
| departement={'2a': '19', '2b': 18}.get(self.departement, self.departement), | |
| control_key='' | |
| ) | |
| try: | |
| ssn_without_key = int(''.join(ssn_without_key)) | |
| key = int(self.control_key) | |
| except ValueError: | |
| return False | |
| return key == (97 - ssn_without_key % 97) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment