Repository: identique/idnumbers (language: python)

Issue:
id number for Belgium
https://en.wikipedia.org/wiki/National_identification_number#Belgium


Discussion / hints:
https://en.wikipedia.org/wiki/Belgian_identity_card#:~:text=The%20National%20Register%20number%20is,and%20cd%20a%20check%2Ddigit.
check digit:

https://dodona.ugent.be/en/activities/394799899/

A third group of two digits is a check number that is calculated from the number  formed by the nine preceding digits. For residents born from the year 2000 onward, an extra digit 2 must be prepended to the nine digits (+ 2000000000) to obtain the number . The calculation of the check number then consists of dividing  by 97 (integer division) and subtracting the remainder of that division (modulo) from 97. That difference is the check number. The check number is represented with two digits by adding a leading zero if necessary.

Fix patch:
diff --git a/README.md b/README.md
index fcc13538..46b8eaad 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ print(id_data['citizenship'])
 * ARE - United Arab Emirates
 * ARG - Argentina 
 * AUS - Australia
+* BEL - Belgium
 * BRA - Brazil
 * CAN - Canada
 * CHL - Chile
@@ -58,10 +59,10 @@ print(id_data['citizenship'])
 * GBR - United Kingdom of Great Britain and Northern Ireland
 * HKG - Hong Kong
 * IDN - Indonesia
-* JPN - Japan
-* KOR - South Korea
 * IND - India
 * IRL - Ireland
+* JPN - Japan
+* KOR - South Korea
 * LKA - Sri Lanka
 * MEX - Mexico
 * MYS - Malaysia
diff --git a/idnumbers/nationalid/BEL.py b/idnumbers/nationalid/BEL.py
new file mode 100644
index 00000000..2d08f4bb
--- /dev/null
+++ b/idnumbers/nationalid/BEL.py
@@ -0,0 +1,76 @@
+import re
+from datetime import date
+from types import SimpleNamespace
+from typing import Optional, TypedDict
+from .util import validate_regexp
+from .constant import Gender
+
+
+def normalize(id_number: str) -> str:
+    return re.sub(r'[.-]', '', id_number)
+
+
+class ParseResult(TypedDict):
+    yyyymmdd: date
+    gender: Gender
+    sn: str
+    checksum: int
+
+
+class NationalID:
+    """
+    Belgium National register number format
+    https://en.wikipedia.org/wiki/Belgian_identity_card
+
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'BE',
+        'min_length': 11,
+        'max_length': 11,
+        'parsable': True,
+        'checksum': True,
+        'regexp': re.compile(r'^(?P<yy>\d{2})\.?(?P<mm>\d{2})\.?(?P<dd>\d{2})-?'
+                             r'(?P<sn>\d{3})\.?'
+                             r'(?P<checksum>\d{2})$')
+    })
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        if not validate_regexp(id_number, NationalID.METADATA.regexp):
+            return False
+        return NationalID.parse(id_number) is not None
+
+    @staticmethod
+    def parse(id_number: str) -> Optional[ParseResult]:
+        match_obj = NationalID.METADATA.regexp.match(id_number)
+        if not match_obj:
+            return None
+        checksum = NationalID.checksum(id_number)
+        if not checksum:
+            return None
+        else:
+            yy = int(match_obj.group('yy'))
+            mm = int(match_obj.group('mm'))
+            dd = int(match_obj.group('dd'))
+            sn = match_obj.group('sn')
+            year_base = 1900 if yy > 50 else 2000
+            return {
+                'yyyymmdd': date(yy + year_base, mm, dd),
+                'gender': Gender.MALE if int(sn) % 2 == 1 else Gender.FEMALE,
+                'sn': sn,
+                'checksum': int(match_obj.group('checksum'))
+            }
+
+    @staticmethod
+    def checksum(id_number) -> bool:
+        if not validate_regexp(id_number, NationalID.METADATA.regexp):
+            return False
+        normalized = normalize(id_number)
+        check_digits = normalized[-2:]
+        if int(normalized[0:2]) < 50:
+            # the person should be born after 2000
+            check = 97 - (2000000000 + int(normalized[:-2])) % 97
+        else:
+            check = 97 - int(normalized[:-2]) % 97
+
+        return int(check_digits) == check


Test patch:
diff --git a/idnumbers/nationalid/test_BEL.py b/idnumbers/nationalid/test_BEL.py
new file mode 100644
index 00000000..13d914d8
--- /dev/null
+++ b/idnumbers/nationalid/test_BEL.py
@@ -0,0 +1,27 @@
+from unittest import TestCase, main
+
+from idnumbers.nationalid import BEL
+from idnumbers.nationalid.constant import Gender
+
+
+class TestBELValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(BEL.NationalID.validate('93051822361'))
+        self.assertTrue(BEL.NationalID.validate('93.05.18-223.61'))
+        self.assertTrue(BEL.NationalID.validate('84122031560'))
+
+    def test_error_case(self):
+        self.assertFalse(BEL.NationalID.validate('930518 223 61'))
+
+    def test_parse(self):
+        result = BEL.NationalID.parse('93051822361')
+        self.assertEqual(1993, result['yyyymmdd'].year)
+        self.assertEqual(5, result['yyyymmdd'].month)
+        self.assertEqual(18, result['yyyymmdd'].day)
+        self.assertEqual(Gender.MALE, result['gender'])
+        self.assertEqual('223', result['sn'])
+        self.assertEqual(61, result['checksum'])
+
+
+if __name__ == '__main__':
+    main()