lists.arthurdejong.org
RSS feed

python-stdnum branch master updated. 1.18-22-g311fd56

[Date Prev][Date Next] [Thread Prev][Thread Next]

python-stdnum branch master updated. 1.18-22-g311fd56



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-stdnum".

The branch, master has been updated
       via  311fd569fd9e10b61a0795d21ea84b0e9f1aad63 (commit)
      from  7d3ddab7ed289580401d29e9a79574174e39f56b (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-stdnum/commit/?id=311fd569fd9e10b61a0795d21ea84b0e9f1aad63

commit 311fd569fd9e10b61a0795d21ea84b0e9f1aad63
Author: Jeff Horemans <jeff.horemans@vortex-financials.be>
Date:   Tue Jun 13 14:44:17 2023 +0200

    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 https://github.com/arthurdejong/python-stdnum/pull/416

diff --git a/stdnum/be/nn.py b/stdnum/be/nn.py
index fd6c627..12b05cb 100644
--- a/stdnum/be/nn.py
+++ b/stdnum/be/nn.py
@@ -2,6 +2,7 @@
 # nn.py - function for handling Belgian national numbers
 #
 # Copyright (C) 2021-2022 Cédric Krier
+# Copyright (C) 2023 Jeff Horemans
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -30,10 +31,34 @@ YYMMDD. The following 3 digits represent a counter of 
people born on the same
 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'
@@ -49,10 +74,15 @@ InvalidChecksum: ...
 '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 calendar
 import datetime
 
 from stdnum.exceptions import *
@@ -71,10 +101,40 @@ 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
+    raise InvalidChecksum()
+
+
+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."""
+    century = _checksum(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]) + century
+    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 > calendar.monthrange(year, month)[1]:
+        return (year, month, None)
+
+    return (year, month, day)
 
 
 def validate(number):
@@ -84,8 +144,7 @@ def validate(number):
         raise InvalidFormat()
     if len(number) != 11:
         raise InvalidLength()
-    if not _checksum(number):
-        raise InvalidChecksum()
+    _get_birth_date_parts(number)
     return number
 
 
@@ -105,17 +164,23 @@ def format(number):
         '-' + '.'.join([number[6:9], number[9:11]]))
 
 
+def get_birth_year(number):
+    """Return the year of the birth date."""
+    year, month, day = _get_birth_date_parts(compact(number))
+    return year
+
+
+def get_birth_month(number):
+    """Return the month of the birth date."""
+    year, month, day = _get_birth_date_parts(compact(number))
+    return month
+
+
 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()
+    year, month, day = _get_birth_date_parts(compact(number))
+    if None not in (year, month, day):
+        return datetime.date(year, month, day)
 
 
 def get_gender(number):
diff --git a/tests/test_be_nn.doctest b/tests/test_be_nn.doctest
index 492218d..cada6b5 100644
--- a/tests/test_be_nn.doctest
+++ b/tests/test_be_nn.doctest
@@ -1,6 +1,7 @@
 test_be_nn.doctest - more detailed doctests for stdnum.be.nn module
 
 Copyright (C) 2022 Arthur de Jong
+Copyright (C) 2023 Jeff Horemans
 
 This library is free software; you can redistribute it and/or
 modify it under the terms of the GNU Lesser General Public
@@ -25,18 +26,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: ...

-----------------------------------------------------------------------

Summary of changes:
 stdnum/be/nn.py          | 91 +++++++++++++++++++++++++++++++++++++++++-------
 tests/test_be_nn.doctest | 49 ++++++++++++++++++++++++--
 2 files changed, 125 insertions(+), 15 deletions(-)


hooks/post-receive
-- 
python-stdnum