Repository: identique/idnumbers (language: python)

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


Discussion / hints:

Fix patch:
diff --git a/idnumbers/nationalid/KWT.py b/idnumbers/nationalid/KWT.py
new file mode 100644
index 00000000..488abb07
--- /dev/null
+++ b/idnumbers/nationalid/KWT.py
@@ -0,0 +1,97 @@
+import re
+from datetime import date
+from types import SimpleNamespace
+from typing import Optional, TypedDict
+from .util import CHECK_DIGIT, weighted_modulus_digit, validate_regexp
+
+
+class ParseResult(TypedDict):
+    yyyymmdd: date
+    """birthday of this ID"""
+    sn: str
+    """serial number"""
+    checksum: CHECK_DIGIT
+    """checksum"""
+
+
+class CivilNumber:
+    """
+    Kuwait Civil Number, Arabic: الرقم المدني
+    https://en.wikipedia.org/wiki/National_identification_number#Kuwait
+    https://prakhar.me/articles/kuwait-civil-id-checksum/
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'KW',
+        'min_length': 12,
+        'max_length': 12,
+        'parsable': True,
+        'checksum': True,
+        'regexp': re.compile(r'^(?P<century>\d)'
+                             r'(?P<yy>\d{2})'
+                             r'(?P<mm>\d{2})'
+                             r'(?P<dd>\d{2})'
+                             r'(?P<sn>\d{4})'
+                             r'(?P<checksum>\d)$')
+    })
+
+    MULTIPLIER = [2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the civil number
+        """
+        if not id_number:
+            return False
+
+        if not isinstance(id_number, str):
+            id_number = repr(id_number)
+        return CivilNumber.parse(id_number) is not None
+
+    @staticmethod
+    def parse(id_number: str) -> Optional[ParseResult]:
+        """
+        parse the id number
+        """
+        match_obj = CivilNumber.METADATA.regexp.match(id_number)
+        if not match_obj:
+            return None
+
+        checksum = CivilNumber.checksum(id_number)
+        if checksum is None or str(checksum) != match_obj.group('checksum'):
+            return None
+
+        century = match_obj.group('century')
+        if century == '2':
+            year_base = 1900
+        elif century == '3':
+            year_base = 2000
+        else:
+            return None
+        yy = int(match_obj.group('yy'))
+        mm = int(match_obj.group('mm'))
+        dd = int(match_obj.group('dd'))
+        return {
+            'yyyymmdd': date(year_base + yy, mm, dd),
+            'sn': match_obj.group('sn'),
+            'checksum': checksum
+        }
+
+    @staticmethod
+    def checksum(id_number) -> Optional[CHECK_DIGIT]:
+        """
+        https://prakhar.me/articles/kuwait-civil-id-checksum/
+        """
+        if not validate_regexp(id_number, CivilNumber.METADATA.regexp):
+            return None
+
+        numbers = [int(char) for char in id_number]
+        modulus = weighted_modulus_digit(numbers[:-1], CivilNumber.MULTIPLIER, 11)
+        if modulus > 10:
+            # according to the algorithm, it will not be greater than 10
+            return None
+        return modulus
+
+
+NationalID = CivilNumber
+"""alias of CivilNumber"""


Test patch:
diff --git a/tests/nationalid/test_KWT.py b/tests/nationalid/test_KWT.py
new file mode 100644
index 00000000..054a25af
--- /dev/null
+++ b/tests/nationalid/test_KWT.py
@@ -0,0 +1,21 @@
+from unittest import TestCase
+
+from idnumbers.nationalid import KWT
+
+
+class TestKWTValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(KWT.CivilNumber.validate('291030104196'))
+        self.assertTrue(KWT.CivilNumber.validate('279040907388'))
+        self.assertTrue(KWT.CivilNumber.validate('288070804106'))
+
+    def test_error_case(self):
+        self.assertFalse(KWT.CivilNumber.validate('291030104197'))
+
+    def test_parse(self):
+        result = KWT.CivilNumber.parse('291030104196')
+        self.assertEqual(1991, result['yyyymmdd'].year)
+        self.assertEqual(3, result['yyyymmdd'].month)
+        self.assertEqual(1, result['yyyymmdd'].day)
+        self.assertEqual('0419', result['sn'])
+        self.assertEqual(6, result['checksum'])