Skip to content

Handle (partially) unknown birthdate of Belgian National Number. #416

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 84 additions & 11 deletions stdnum/be/nn.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,34 @@
date, seperated by sex (odd for male and even for females respectively). The
final 2 digits form a check number based on the 9 preceding digits.

Special cases include:

* Counter exhaustion:
When the even or uneven day counter range for a specific date of birth runs
out, (e.g. from 001 tot 997 for males), the first 2 digits will represent
the birth year as normal, while the next 4 digits (birth month and day) are
taken to be zeroes. The remaining 3 digits still represent a day counter
which will then restart.
When those ranges would run out also, the sixth digit is incremented with 1
and the day counter will restart again.

* Incomplete date of birth
When the exact month or day of the birth date were not known at the time of
assignment, incomplete parts are taken to be zeroes, similarly as with
counter exhaustion.
Note that a month with zeroes can thus both mean the date of birth was not
exactly known, or the person was born on a day were at least 500 persons of
the same gender got a number assigned already.

* Unknown date of birth
When no part of the date of birth was known, a fictitious date is used
depending on the century (i.e. 1900/00/01 or 2000/00/01).

More information:

* https://nl.wikipedia.org/wiki/Rijksregisternummer
* https://fr.wikipedia.org/wiki/Numéro_de_registre_national
* https://www.ibz.rrn.fgov.be/fileadmin/user_upload/nl/rr/instructies/IT-lijst/IT000_Rijksregisternummer.pdf

>>> compact('85.07.30-033 28')
'85073003328'
Expand All @@ -49,11 +73,16 @@
'85.07.30-033.28'
>>> get_birth_date('85.07.30-033 28')
datetime.date(1985, 7, 30)
>>> get_birth_year('85.07.30-033 28')
1985
>>> get_birth_month('85.07.30-033 28')
7
>>> get_gender('85.07.30-033 28')
'M'
"""

import datetime
from calendar import monthrange

from stdnum.exceptions import *
from stdnum.util import clean, isdigits
Expand All @@ -71,21 +100,53 @@ def _checksum(number):
numbers = [number]
if int(number[:2]) + 2000 <= datetime.date.today().year:
numbers.append('2' + number)
for century, n in zip((19, 20), numbers):
for century, n in zip((1900, 2000), numbers):
if 97 - (int(n[:-2]) % 97) == int(n[-2:]):
return century
return False


def _get_birth_date_parts(number):
"""Check if the number's encoded birth date is valid, and return the contained
birth year, month and day of month, accounting for unknown values."""
number = compact(number)

# If the fictitious dates 1900/00/01 or 2000/00/01 are detected,
# the birth date (including the year) was not known when the number
# was issued.
if number[:6] == '000001':
return (None, None, None)

year = int(number[:2])
month, day = int(number[2:4]), int(number[4:6])
# When the month is zero, it was either unknown when the number was issued,
# or the day counter ran out. In both cases, the month and day are not known
# reliably:
if month == 0:
return (year, None, None)

# Verify range of month:
if month > 12:
raise InvalidComponent('month must be in 1..12')

# Case when only the day of the birth date is unknown:
if day == 0 or day > monthrange(year, month)[1]:
return (year, month, None)

return (year, month, day)


def validate(number):
"""Check if the number is a valid National Number."""
number = compact(number)
if not isdigits(number) or int(number) <= 0:
raise InvalidFormat()
if len(number) != 11:
raise InvalidLength()
if not _checksum(number):
century = _checksum(number)
if not century:
raise InvalidChecksum()
_get_birth_date_parts(number)
return number


Expand All @@ -105,17 +166,29 @@ def format(number):
'-' + '.'.join([number[6:9], number[9:11]]))


def get_birth_year(number):
"""Return the year of the birth date."""
number = validate(number)
birth_year = _get_birth_date_parts(number)[0]
if birth_year is not None:
century = _checksum(number)
return century + birth_year


def get_birth_month(number):
"""Return the month of the birth date."""
number = validate(number)
return _get_birth_date_parts(number)[1]


def get_birth_date(number):
"""Return the date of birth."""
number = compact(number)
century = _checksum(number)
if not century:
raise InvalidChecksum()
try:
return datetime.datetime.strptime(
str(century) + number[:6], '%Y%m%d').date()
except ValueError:
raise InvalidComponent()
number = validate(number)
year, month, day = _get_birth_date_parts(number)
if None not in (year, month, day):
century = _checksum(number)
year = century + year
return datetime.date(year, month, day)


def get_gender(number):
Expand Down
48 changes: 46 additions & 2 deletions tests/test_be_nn.doctest
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,62 @@ really useful as module documentation.
>>> from stdnum.be import nn


Extra tests for getting birth date
Extra tests for getting birth date, year and/or month


>>> nn.get_birth_date('85.07.30-033 28')
datetime.date(1985, 7, 30)
>>> nn.get_birth_year('85.07.30-033 28')
1985
>>> nn.get_birth_month('85.07.30-033 28')
7
>>> nn.get_birth_date('17 07 30 033 84')
datetime.date(2017, 7, 30)
>>> nn.get_birth_year('17 07 30 033 84')
2017
>>> nn.get_birth_month('17 07 30 033 84')
7
>>> nn.get_birth_date('12345678901')
Traceback (most recent call last):
...
InvalidChecksum: ...
>>> nn.get_birth_date('00 00 01 003-64') # 2000-00-00 is not a valid date
>>> nn.get_birth_year('12345678901')
Traceback (most recent call last):
...
InvalidChecksum: ...
>>> nn.get_birth_month('12345678901')
Traceback (most recent call last):
...
InvalidChecksum: ...
>>> nn.get_birth_date('00000100166') # Exact date of birth unknown (fictitious date case 1900-00-01)
>>> nn.get_birth_year('00000100166')
>>> nn.get_birth_month('00000100166')
>>> nn.get_birth_date('00000100195') # Exact date of birth unknown (fictitious date case 2000-00-01)
>>> nn.get_birth_year('00000100195')
>>> nn.get_birth_month('00000100195')
>>> nn.get_birth_date('00000000128') # Only birth year known (2000-00-00)
>>> nn.get_birth_year('00000000128')
2000
>>> nn.get_birth_month('00000000128')
>>> nn.get_birth_date('00010000135') # Only birth year and month known (2000-01-00)
>>> nn.get_birth_year('00010000135')
2000
>>> nn.get_birth_month('00010000135')
1
>>> nn.get_birth_date('85073500107') # Unknown day of birth date (35)
>>> nn.get_birth_year('85073500107')
1985
>>> nn.get_birth_month('85073500107')
7
>>> nn.get_birth_date('85133000105') # Invalid month (13)
Traceback (most recent call last):
...
InvalidComponent: ...
>>> nn.get_birth_year('85133000105')
Traceback (most recent call last):
...
InvalidComponent: ...
>>> nn.get_birth_month('85133000105')
Traceback (most recent call last):
...
InvalidComponent: ...
Expand Down