2
2
# nn.py - function for handling Belgian national numbers
3
3
#
4
4
# Copyright (C) 2021-2022 Cédric Krier
5
+ # Copyright (C) 2023 Jeff Horemans
5
6
#
6
7
# This library is free software; you can redistribute it and/or
7
8
# modify it under the terms of the GNU Lesser General Public
30
31
date, seperated by sex (odd for male and even for females respectively). The
31
32
final 2 digits form a check number based on the 9 preceding digits.
32
33
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
+
33
57
More information:
34
58
35
59
* https://nl.wikipedia.org/wiki/Rijksregisternummer
36
60
* 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
37
62
38
63
>>> compact('85.07.30-033 28')
39
64
'85073003328'
49
74
'85.07.30-033.28'
50
75
>>> get_birth_date('85.07.30-033 28')
51
76
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
52
81
>>> get_gender('85.07.30-033 28')
53
82
'M'
54
83
"""
55
84
85
+ import calendar
56
86
import datetime
57
87
58
88
from stdnum .exceptions import *
@@ -71,10 +101,40 @@ def _checksum(number):
71
101
numbers = [number ]
72
102
if int (number [:2 ]) + 2000 <= datetime .date .today ().year :
73
103
numbers .append ('2' + number )
74
- for century , n in zip ((19 , 20 ), numbers ):
104
+ for century , n in zip ((1900 , 2000 ), numbers ):
75
105
if 97 - (int (n [:- 2 ]) % 97 ) == int (n [- 2 :]):
76
106
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 )
78
138
79
139
80
140
def validate (number ):
@@ -84,8 +144,7 @@ def validate(number):
84
144
raise InvalidFormat ()
85
145
if len (number ) != 11 :
86
146
raise InvalidLength ()
87
- if not _checksum (number ):
88
- raise InvalidChecksum ()
147
+ _get_birth_date_parts (number )
89
148
return number
90
149
91
150
@@ -105,17 +164,23 @@ def format(number):
105
164
'-' + '.' .join ([number [6 :9 ], number [9 :11 ]]))
106
165
107
166
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
+
108
179
def get_birth_date (number ):
109
180
"""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 )
119
184
120
185
121
186
def get_gender (number ):
0 commit comments