8000 Handle (partially) unknown birthdate of Belgian National Number · arthurdejong/python-stdnum@311fd56 · GitHub
[go: up one dir, main page]

Skip to content

Commit 311fd56

Browse files
jeffh92arthurdejong
authored andcommitted
Handle (partially) unknown birthdate of Belgian National Number
This adds documentation for the special cases regarding birth dates embedded in the number, allows for date parts to be unknown and adds functions for getting the year and month. Closes #416
1 parent 7d3ddab commit 311fd56

File tree

2 files changed

+125
-15
lines changed

2 files changed

+125
-15
lines changed

stdnum/be/nn.py

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# nn.py - function for handling Belgian national numbers
33
#
44
# Copyright (C) 2021-2022 Cédric Krier
5+
# Copyright (C) 2023 Jeff Horemans
56
#
67
# This library is free software; you can redistribute it and/or
78
# modify it under the terms of the GNU Lesser General Public
@@ -30,10 +31,34 @@
3031
date, seperated by sex (odd for male and even for females respectively). The
3132
final 2 digits form a check number based on the 9 preceding digits.
3233
34+
Special cases include:
35+
36+
* Counter exhaustion:
37+
When the even or uneven day counter range for a specific date of birth runs
38+
out, (e.g. from 001 tot 997 for males), the first 2 digits will represent
39+
the birth year as normal, while the next 4 digits (birth month and day) are
40+
taken to be zeroes. The remaining 3 digits still represent a day counter
41+
which will then restart.
42+
When those ranges would run out also, the sixth digit is incremented with 1
43+
and the day counter will restart again.
44+
45+
* Incomplete date of birth
46+
When the exact month or day of the birth date were not known at the time of
47+
assignment, incomplete parts are taken to be zeroes, similarly as with
48+
counter exhaustion.
49+
Note that a month with zeroes can thus both mean the date of birth was not
50+
exactly known, or the person was born on a day were at least 500 persons of
51+
the same gender got a number assigned already.
52+
53+
* Unknown date of birth
54+
When no part of the date of birth was known, a fictitious date is used
55+
depending on the century (i.e. 1900/00/01 or 2000/00/01).
56+
3357
More information:
3458
3559
* https://nl.wikipedia.org/wiki/Rijksregisternummer
3660
* https://fr.wikipedia.org/wiki/Numéro_de_registre_national
61+
* https://www.ibz.rrn.fgov.be/fileadmin/user_upload/nl/rr/instructies/IT-lijst/IT000_Rijksregisternummer.pdf
3762
3863
>>> compact('85.07.30-033 28')
3964
'85073003328'
@@ -49,10 +74,15 @@
4974
'85.07.30-033.28'
5075
>>> get_birth_date('85.07.30-033 28')
5176
datetime.date(1985, 7, 30)
77+
>>> get_birth_year('85.07.30-033 28')
78+
1985
79+
>>> get_birth_month('85.07.30-033 28')
80+
7
5281
>>> get_gender('85.07.30-033 28')
5382
'M'
5483
"""
5584

85+
import calendar
5686
import datetime
5787

5888
from stdnum.exceptions import *
@@ -71,10 +101,40 @@ def _checksum(number):
71101
numbers = [number]
72102
if int(number[:2]) + 2000 <= datetime.date.today().year:
73103
numbers.append('2' + number)
74-
for century, n in zip((19, 20), numbers):
104+
for century, n in zip((1900, 2000), numbers):
75105
if 97 - (int(n[:-2]) % 97) == int(n[-2:]):
76106
return century
77-
return False
107+
raise InvalidChecksum()
108+
109+
110+
def _get_birth_date_parts(number):
111+
"""Check if the number's encoded birth date is valid, and return the contained
112+
birth year, month and day of month, accounting for unknown values."""
113+
century = _checksum(number)
114+
115+
# If the fictitious dates 1900/00/01 or 2000/00/01 are detected,
116+
# the birth date (including the year) was not known when the number
117+
# was issued.
118+
if number[:6] == '000001':
119+
return (None, None, None)
120+
121+
year = int(number[:2]) + century
122+
month, day = int(number[2:4]), int(number[4:6])
123+
# When the month is zero, it was either unknown when the number was issued,
124+
# or the day counter ran out. In both cases, the month and day are not known
125+
# reliably.
126+
if month == 0:
127+
return (year, None, None)
128+
129+
# Verify range of month
130+
if month > 12:
131+
raise InvalidComponent('month must be in 1..12')
132+
133+
# Case when only the day of the birth date is unknown
134+
if day == 0 or day > calendar.monthrange(year, month)[1]:
135+
return (year, month, None)
136+
137+
return (year, month, day)
78138

79139

80140
def validate(number):
@@ -84,8 +144,7 @@ def validate(number):
84144
raise InvalidFormat()
85145
if len(number) != 11:
86146
raise InvalidLength()
87-
if not _checksum(number):
88-
raise InvalidChecksum()
147+
_get_birth_date_parts(number)
89148
return number
90149

91150

@@ -105,17 +164,23 @@ def format(number):
105164
'-' + '.'.join([number[6:9], number[9:11]]))
106165

107166

167+
def get_birth_year(number):
168+
"""Return the year of the birth date."""
169+
year, month, day = _get_birth_date_parts(compact(number))
170+
return year
171+
172+
173+
def get_birth_month(number):
174+
"""Return the month of the birth date."""
175+
year, month, day = _get_birth_date_parts(compact(number))
176+
return month
177+
178+
108179
def get_birth_date(number):
109180
"""Return the date of birth."""
110-
number = compact(number)
111-
century = _checksum(number)
112-
if not century:
113-
raise InvalidChecksum()
114-
try:
115-
return datetime.datetime.strptime(
116-
str(century) + number[:6], '%Y%m%d').date()
117-
except ValueError:
118-
raise InvalidComponent()
181+
year, month, day = _get_birth_date_parts(compact(number))
182+
if None not in (year, month, day):
183+
return datetime.date(year, month, day)
119184

120185

121186
def get_gender(number):
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
test_be_nn.doctest - more detailed doctests for stdnum.be.nn module
22

33
Copyright (C) 2022 Arthur de Jong
4+
Copyright (C) 2023 Jeff Horemans
45

56
This library is free software; you can redistribute it and/or
67
modify it under the terms of the GNU Lesser General Public
@@ -25,18 +26,62 @@ really useful as module documentation.
2526
>>> from stdnum.be import nn
2627

2728

28-
Extra tests for getting birth date
29+
Extra tests for getting birth date, year and/or month
2930

3031

3132
>>> nn.get_birth_date('85.07.30-033 28')
3233
datetime.date(1985, 7, 30)
34+
>>> nn.get_birth_year('85.07.30-033 28')
35+
1985
36+
>>> nn.get_birth_month('85.07.30-033 28')
37+
7
3338
>>> nn.get_birth_date('17 07 30 033 84')
3439
datetime.date(2017, 7, 30)
40+
>>> nn.get_birth_year('17 07 30 033 84')
41+
2017
42+
>>> nn.get_birth_month('17 07 30 033 84')
43+
7
3544
>>> nn.get_birth_date('12345678901')
3645
Traceback (most recent call last):
3746
...
3847
InvalidChecksum: ...
39-
>>> nn.get_birth_date('00 00 01 003-64') # 2000-00-00 is not a valid date
48+
>>> nn.get_birth_year('12345678901')
49+
Traceback (most recent call last):
50+
...
51+
InvalidChecksum: ...
52+
>>> nn.get_birth_month('12345678901')
53+
Traceback (most recent call last):
54+
...
55+
InvalidChecksum: ...
56+
>>> nn.get_birth_date('00000100166') # Exact date of birth unknown (fictitious date case 1900-00-01)
57+
>>> nn.get_birth_year('00000100166')
58+
>>> nn.get_birth_month('00000100166')
59+
>>> nn.get_birth_date('00000100195') # Exact date of birth unknown (fictitious date case 2000-00-01)
60+
>>> nn.get_birth_year('00000100195')
61+
>>> nn.get_birth_month('00000100195')
62+
>>> nn.get_birth_date('00000000128') # Only birth year known (2000-00-00)
63+
>>> nn.get_birth_year('00000000128')
64+
2000
65+
>>> nn.get_birth_month('00000000128')
66+
>>> nn.get_birth_date('00010000135') # Only birth year and month known (2000-01-00)
67+
>>> nn.get_birth_year('00010000135')
68+
2000
69+
>>> nn.get_birth_month('00010000135')
70+
1
71+
>>> nn.get_birth_date('85073500107') # Unknown day of birth date (35)
72+
>>> nn.get_birth_year('85073500107')
73+
1985
74+
>>> nn.get_birth_month('85073500107')
75+
7
76+
>>> nn.get_birth_date('85133000105') # Invalid month (13)
77+
Traceback (most recent call last):
78+
...
79+
InvalidComponent: ...
80+
>>> nn.get_birth_year('85133000105')
81+
Traceback (most recent call last):
82+
...
83+
InvalidComponent: ...
84+
>>> nn.get_birth_month('85133000105')
4085
Traceback (most recent call last):
4186
...
4287
InvalidComponent: ...
0