Skip to content
This repository was archived by the owner on Jan 8, 2025. It is now read-only.

Commit b1d2238

Browse files
authored
Fix #21 - implement Kuwait Civil Number/ID (#214)
1 parent 47e72da commit b1d2238

File tree

2 files changed

+118
-0
lines changed

2 files changed

+118
-0
lines changed

idnumbers/nationalid/KWT.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import re
2+
from datetime import date
3+
from types import SimpleNamespace
4+
from typing import Optional, TypedDict
5+
from .util import CHECK_DIGIT, weighted_modulus_digit, validate_regexp
6+
7+
8+
class ParseResult(TypedDict):
9+
yyyymmdd: date
10+
"""birthday of this ID"""
11+
sn: str
12+
"""serial number"""
13+
checksum: CHECK_DIGIT
14+
"""checksum"""
15+
16+
17+
class CivilNumber:
18+
"""
19+
Kuwait Civil Number, Arabic: الرقم المدني
20+
https://en.wikipedia.org/wiki/National_identification_number#Kuwait
21+
https://prakhar.me/articles/kuwait-civil-id-checksum/
22+
"""
23+
METADATA = SimpleNamespace(**{
24+
'iso3166_alpha2': 'KW',
25+
'min_length': 12,
26+
'max_length': 12,
27+
'parsable': True,
28+
'checksum': True,
29+
'regexp': re.compile(r'^(?P<century>\d)'
30+
r'(?P<yy>\d{2})'
31+
r'(?P<mm>\d{2})'
32+
r'(?P<dd>\d{2})'
33+
r'(?P<sn>\d{4})'
34+
r'(?P<checksum>\d)$')
35+
})
36+
37+
MULTIPLIER = [2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
38+
39+
@staticmethod
40+
def validate(id_number: str) -> bool:
41+
"""
42+
Validate the civil number
43+
"""
44+
if not id_number:
45+
return False
46+
47+
if not isinstance(id_number, str):
48+
id_number = repr(id_number)
49+
return CivilNumber.parse(id_number) is not None
50+
51+
@staticmethod
52+
def parse(id_number: str) -> Optional[ParseResult]:
53+
"""
54+
parse the id number
55+
"""
56+
match_obj = CivilNumber.METADATA.regexp.match(id_number)
57+
if not match_obj:
58+
return None
59+
60+
checksum = CivilNumber.checksum(id_number)
61+
if checksum is None or str(checksum) != match_obj.group('checksum'):
62+
return None
63+
64+
century = match_obj.group('century')
65+
if century == '2':
66+
year_base = 1900
67+
elif century == '3':
68+
year_base = 2000
69+
else:
70+
return None
71+
yy = int(match_obj.group('yy'))
72+
mm = int(match_obj.group('mm'))
73+
dd = int(match_obj.group('dd'))
74+
return {
75+
'yyyymmdd': date(year_base + yy, mm, dd),
76+
'sn': match_obj.group('sn'),
77+
'checksum': checksum
78+
}
79+
80+
@staticmethod
81+
def checksum(id_number) -> Optional[CHECK_DIGIT]:
82+
"""
83+
https://prakhar.me/articles/kuwait-civil-id-checksum/
84+
"""
85+
if not validate_regexp(id_number, CivilNumber.METADATA.regexp):
86+
return None
87+
88+
numbers = [int(char) for char in id_number]
89+
modulus = weighted_modulus_digit(numbers[:-1], CivilNumber.MULTIPLIER, 11)
90+
if modulus > 10:
91+
# according to the algorithm, it will not be greater than 10
92+
return None
93+
return modulus
94+
95+
96+
NationalID = CivilNumber
97+
"""alias of CivilNumber"""

tests/nationalid/test_KWT.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from unittest import TestCase
2+
3+
from idnumbers.nationalid import KWT
4+
5+
6+
class TestKWTValidation(TestCase):
7+
def test_normal_case(self):
8+
self.assertTrue(KWT.CivilNumber.validate('291030104196'))
9+
self.assertTrue(KWT.CivilNumber.validate('279040907388'))
10+
self.assertTrue(KWT.CivilNumber.validate('288070804106'))
11+
12+
def test_error_case(self):
13+
self.assertFalse(KWT.CivilNumber.validate('291030104197'))
14+
15+
def test_parse(self):
16+
result = KWT.CivilNumber.parse('291030104196')
17+
self.assertEqual(1991, result['yyyymmdd'].year)
18+
self.assertEqual(3, result['yyyymmdd'].month)
19+
self.assertEqual(1, result['yyyymmdd'].day)
20+
self.assertEqual('0419', result['sn'])
21+
self.assertEqual(6, result['checksum'])

0 commit comments

Comments
 (0)