Repository: identique/idnumbers (language: python)

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


Discussion / hints:

Fix patch:
diff --git a/idnumbers/nationalid/BGR.py b/idnumbers/nationalid/BGR.py
new file mode 100644
index 00000000..63769387
--- /dev/null
+++ b/idnumbers/nationalid/BGR.py
@@ -0,0 +1,83 @@
+import re
+from datetime import date
+from types import SimpleNamespace
+from typing import TypedDict, Optional
+
+from .util import validate_regexp, CHECK_DIGIT, weighted_modulus_digit
+from .constant import Gender
+
+
+class ParseResult(TypedDict):
+    """Parse result of UniformCivilNumber"""
+    yyyymmdd: date
+    """year of birth"""
+    checksum: CHECK_DIGIT
+    """check digits"""
+    gender: Gender
+
+
+class UniformCivilNumber:
+    """
+    Bulgaria Uniform civil number
+    https://en.wikipedia.org/wiki/National_identification_number#Bulgaria
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'BG',
+        # length without insignificant chars
+        'min_length': 10,
+        'max_length': 10,
+        'parsable': True,
+        'checksum': True,
+        'regexp': re.compile(r'^(?P<yy>\d{2})'
+                             r'(?P<mm>\d{2})'
+                             r'(?P<dd>\d{2})'
+                             r'\d{2}'
+                             r'(?P<gender>\d)'
+                             r'(?P<checksum>\d)$')
+    })
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the BGR id number
+        """
+        if not validate_regexp(id_number, UniformCivilNumber.METADATA.regexp):
+            return False
+        return UniformCivilNumber.parse(id_number) is not None
+
+    @staticmethod
+    def parse(id_number: str) -> Optional[ParseResult]:
+        """
+        Parse the result
+        """
+        match_obj = UniformCivilNumber.METADATA.regexp.match(id_number)
+        checksum = int(match_obj.group("checksum"))
+        yy = int(match_obj.group("yy"))
+        mm = int(match_obj.group("mm"))
+        dd = int(match_obj.group("dd"))
+        if UniformCivilNumber.checksum(id_number) != checksum:
+            return None
+        if mm > 40:
+            mm -= 40
+            yyyy = yy + 2000
+        elif mm > 20:
+            mm -= 20
+            yyyy = yy + 1800
+        else:
+            yyyy = yy + 1900
+        return {
+            'yyyymmdd': date(yyyy, mm, dd),
+            "checksum": int(checksum),
+            'gender': Gender.MALE if int(match_obj.group("gender")) % 2 == 0 else Gender.FEMALE
+        }
+
+    MULTIPLIER = [2, 4, 8, 5, 10, 9, 7, 3, 6]
+
+    @staticmethod
+    def checksum(id_number: str) -> CHECK_DIGIT:
+        """
+        Get the checksum digit
+        https://en.wikipedia.org/wiki/Unique_citizenship_number
+        """
+        digits_numbers = [int(i) for i in id_number[:-1]]
+        return weighted_modulus_digit(digits_numbers, UniformCivilNumber.MULTIPLIER, 11, True)
diff --git a/idnumbers/nationalid/util.py b/idnumbers/nationalid/util.py
index d25acd68..d8740799 100644
--- a/idnumbers/nationalid/util.py
+++ b/idnumbers/nationalid/util.py
@@ -1,5 +1,5 @@
 from re import Pattern
-from typing import List, Literal, cast
+from typing import List, Literal, cast, Type
 
 VERHOEFF = {
     'D_TABLE': [
@@ -28,7 +28,7 @@
 }
 """[Table](https://en.wikipedia.org/wiki/Verhoeff_algorithm#Table-based_algorithm) for the Verhoeff algorithm"""
 
-CHECK_DIGIT = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+CHECK_DIGIT: Type[int] = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 """Check digit type. Numeric check digits are only allowed in 0 to 9"""
 
 CHECK_ALPHA = Literal['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',


Test patch:
diff --git a/tests/nationalid/test_BGR.py b/tests/nationalid/test_BGR.py
new file mode 100644
index 00000000..7c560388
--- /dev/null
+++ b/tests/nationalid/test_BGR.py
@@ -0,0 +1,30 @@
+from unittest import TestCase, main
+from datetime import date
+
+from idnumbers.nationalid import BGR
+from idnumbers.nationalid.constant import Gender
+
+
+class TestBGRValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(BGR.UniformCivilNumber.validate('7501020018'))
+        self.assertTrue(BGR.UniformCivilNumber.validate('7542011030'))
+
+    def test_error_case(self):
+        self.assertFalse(BGR.UniformCivilNumber.validate('7501020011'))
+        self.assertFalse(BGR.UniformCivilNumber.validate('750102 0018'))
+
+    def test_parse(self):
+        result = BGR.UniformCivilNumber.parse('7501020018')
+        self.assertEqual(date(1975, 1, 2), result['yyyymmdd'])
+        self.assertEqual(Gender.FEMALE, result['gender'])
+        self.assertEqual(8, result['checksum'])
+
+        result = BGR.UniformCivilNumber.parse('7552010005')
+        self.assertEqual(date(2075, 12, 1), result['yyyymmdd'])
+        self.assertEqual(Gender.MALE, result['gender'])
+        self.assertEqual(5, result['checksum'])
+
+
+if __name__ == '__main__':
+    main()