diff options
author | Arthur de Jong <arthur@arthurdejong.org> | 2020-01-18 17:53:15 +0100 |
---|---|---|
committer | Arthur de Jong <arthur@arthurdejong.org> | 2020-01-18 17:59:22 +0100 |
commit | f23c54922b24a3a71aba6bc0f11495a924951e40 (patch) | |
tree | 450db127886fe0d8b0ca360fa7f316185065d6b1 | |
parent | 42e096e948e32d387ecdb8bf51abefdf17d21377 (diff) |
Add South African Identity Document number
Closes https://github.com/arthurdejong/python-stdnum/issues/126
-rw-r--r-- | stdnum/za/idnr.py | 133 | ||||
-rw-r--r-- | tests/test_za_idnr.doctest | 90 |
2 files changed, 223 insertions, 0 deletions
diff --git a/stdnum/za/idnr.py b/stdnum/za/idnr.py new file mode 100644 index 0000000..c4cae18 --- /dev/null +++ b/stdnum/za/idnr.py @@ -0,0 +1,133 @@ +# tin.py - functions for handling South Africa ID number +# coding: utf-8 +# +# Copyright (C) 2020 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""ID number (South African Identity Document number). + +The South African ID number is issued to individuals within South Africa. The +number consists of 13 digits and contains information about a person's date +of birth, gender and whether the person is a citizen or permanent resident. + +More information: + +* https://en.wikipedia.org/wiki/South_African_identity_card +* http://www.dha.gov.za/index.php/identity-documents2 + +>>> validate('7503305044089') +'7503305044089' +>>> validate('8503305044089') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> validate('9125568') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> get_gender('7503305044089') +'M' +>>> get_birth_date('7503305044089') +datetime.date(1975, 3, 30) +>>> get_citizenship('7503305044089') +'citizen' +>>> format('750330 5044089') +'750330 5044 08 9' +""" + +import datetime + +from stdnum import luhn +from stdnum.exceptions import * +from stdnum.util import clean, isdigits + + +def compact(number): + """Convert the number to the minimal representation. + + This strips the number of any valid separators and removes surrounding + whitespace. + """ + return clean(number, ' ') + + +def get_birth_date(number): + """Split the date parts from the number and return the date of birth. + + Since the number only uses two digits for the year, the century may be + incorrect. + """ + number = compact(number) + today = datetime.date.today() + year = int(number[0:2]) + (100 * (today.year // 100)) + month = int(number[2:4]) + day = int(number[4:6]) + if year > today.year: + year -= 100 + try: + return datetime.date(year, month, day) + except ValueError: + raise InvalidComponent() + + +def get_gender(number): + """Get the gender (M/F) from the person's ID number.""" + number = compact(number) + if number[6] in '01234': + return 'F' + else: + return 'M' + + +def get_citizenship(number): + """Get the citizenship status (citizen/resident) from the ID number.""" + number = compact(number) + if number[10] == '0': + return 'citizen' + elif number[10] == '1': + return 'resident' + else: + raise InvalidComponent() + + +def validate(number): + """Check if the number is a valid South African ID number. + + This checks the length, formatting and check digit. + """ + number = compact(number) + if not isdigits(number): + raise InvalidFormat() + if len(number) != 13: + raise InvalidLength() + get_birth_date(number) + get_citizenship(number) + return luhn.validate(number) + + +def is_valid(number): + """Check if the number is a valid South African ID number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number): + """Reformat the number to the standard presentation format.""" + number = compact(number) + return ' '.join((number[:6], number[6:10], number[10:12], number[12:])) diff --git a/tests/test_za_idnr.doctest b/tests/test_za_idnr.doctest new file mode 100644 index 0000000..206ff6e --- /dev/null +++ b/tests/test_za_idnr.doctest @@ -0,0 +1,90 @@ +test_za_idnr.doctest - more detailed doctests for stdnum.za.idnr module + +Copyright (C) 2020 Arthur de Jong + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA + + +This file contains more detailed doctests for the stdnum.za.idnr module. It +tries to test more corner cases and detailed functionality that is not really +useful as module documentation. + +>>> from stdnum.za import idnr + + +Tests for some corner cases. + +>>> idnr.validate('7611055077082') +'7611055077082' +>>> idnr.get_gender('7611055077082') +'M' +>>> idnr.get_gender('7209170838080') +'F' +>>> idnr.get_birth_date('7611055077082') +datetime.date(1976, 11, 5) +>>> idnr.get_birth_date('0001015077082') +datetime.date(2000, 1, 1) +>>> idnr.get_birth_date('0099015077086') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> idnr.get_citizenship('9103225261083') +'citizen' +>>> idnr.get_citizenship('5306060050180') +'resident' +>>> idnr.get_citizenship('5306060050883') +Traceback (most recent call last): + ... +InvalidComponent: ... + + +These have been found online and should all be valid numbers. + +>>> numbers = ''' +... +... 5306060050180 +... 6107165087088 +... 6302285896084 +... 6602056061184 +... 6602085002084 +... 6704020865185 +... 6906155141080 +... 7010115159081 +... 7106245929185 +... 7209170838080 +... 7210015101080 +... 7405035028087 +... 7405095437186 +... 7503305044089 +... 7611055077082 +... 7701275868087 +... 7803200018083 +... 7804180106088 +... 7911175459081 +... 8012185201081 +... 8311280061089 +... 8507085951081 +... 8510290194083 +... 8811166068082 +... 8907020111082 +... 9008185655085 +... 9103225261083 +... 9210245029083 +... 9708205014086 +... +... ''' +>>> [x for x in numbers.splitlines() if x and not idnr.is_valid(x)] +[] |