Repository: identique/idnumbers (language: python)

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


Discussion / hints:
https://archive.org/details/Aadhaar_numbering_scheme/page/n12/mode/1up?view=theater page 10 ~ 14

Fix patch:
diff --git a/README.md b/README.md
index b40dee34..21b81c5a 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,7 @@ print(id_data['citizenship'])
 * HKG - Hong Kong
 * JPN - Japan
 * KOR - South Korea
+* IND - India
 * LKA - Sri Lanka
 * MYS - Malaysia
 * NGA - Nigeria
diff --git a/idnumbers/nationalid/IND.py b/idnumbers/nationalid/IND.py
new file mode 100644
index 00000000..46795367
--- /dev/null
+++ b/idnumbers/nationalid/IND.py
@@ -0,0 +1,37 @@
+import re
+from types import SimpleNamespace
+from .util import validate_regexp, verhoeff_check
+
+
+def normalize(id_number: str) -> str:
+    return re.sub(r'[ -]', '', id_number)
+
+
+class NationalID:
+    """
+    India National ID number format, UID
+    https://en.wikipedia.org/wiki/National_identification_number#India
+    https://archive.org/details/Aadhaar_numbering_scheme/page/n12/mode/1up?view=theater
+    https://en.wikipedia.org/wiki/Verhoeff_algorithm#Table-based_algorithm
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'IN',
+        'min_length': 12,
+        'max_length': 12,
+        'parsable': False,
+        'checksum': True,
+        'regexp': re.compile(r'^[2-9]\d{3}[ -]?\d{4}[ -]?\d{4}$')
+    })
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the id number
+        """
+        return NationalID.checksum(id_number)
+
+    @staticmethod
+    def checksum(id_number) -> bool:
+        if not validate_regexp(id_number, NationalID.METADATA.regexp):
+            return False
+        return verhoeff_check([int(char) for char in normalize(id_number)])
diff --git a/idnumbers/nationalid/util.py b/idnumbers/nationalid/util.py
index dceda596..819746bf 100644
--- a/idnumbers/nationalid/util.py
+++ b/idnumbers/nationalid/util.py
@@ -1,6 +1,32 @@
 from re import Pattern
 from typing import List, Literal, cast
 
+VERHOEFF = {
+    'D_TABLE': [
+        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+        [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
+        [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
+        [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
+        [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
+        [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
+        [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
+        [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
+        [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
+        [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
+    ],
+
+    'P_TABLE': [
+        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+        [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
+        [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
+        [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
+        [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
+        [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
+        [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
+        [7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
+    ]
+}
+
 
 def validate_regexp(id_number: str, regexp: Pattern[str]) -> bool:
     assert isinstance(id_number, str), 'id_number MUST be str'
@@ -25,6 +51,16 @@ def luhn_digit(digits: List[int]) -> Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]:
     return cast(Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], (10 - total_sum % 10) % 10)
 
 
+def verhoeff_check(digits: List[int]) -> bool:
+    rev_digits = list(digits)
+    rev_digits.reverse()
+    c = 0
+    for idx, num in enumerate(rev_digits):
+        p_val = VERHOEFF["P_TABLE"][idx % 8][num]
+        c = VERHOEFF["D_TABLE"][c][p_val]
+    return c == 0
+
+
 def modulus_check(numbers: List[int], weights: List[int], divider: int, modulus_only: bool = False) -> int:
     assert len(numbers) <= len(weights), 'numbers length must be less than or equal to weights length'
     modulus = sum([value * weights[index] for (index, value) in enumerate(numbers)]) % divider


Test patch:
diff --git a/idnumbers/nationalid/test_IND.py b/idnumbers/nationalid/test_IND.py
new file mode 100644
index 00000000..1b29ea42
--- /dev/null
+++ b/idnumbers/nationalid/test_IND.py
@@ -0,0 +1,20 @@
+from unittest import TestCase, main
+
+from idnumbers.nationalid import IND
+
+
+class TestSGPValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(IND.NationalID.validate('8924 7352 8038'))
+        self.assertTrue(IND.NationalID.validate('3977 8800 0234'))
+        self.assertTrue(IND.NationalID.validate('5485-5000-8800'))
+        self.assertTrue(IND.NationalID.validate('475587669949'))
+
+    def test_error_case(self):
+        self.assertFalse(IND.NationalID.validate('47558'))
+        self.assertFalse(IND.NationalID.validate('475587669940'))
+        self.assertFalse(IND.NationalID.validate('175587669949'))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/idnumbers/nationalid/test_SGP.py b/idnumbers/nationalid/test_SGP.py
index 0d753a95..0f91da51 100644
--- a/idnumbers/nationalid/test_SGP.py
+++ b/idnumbers/nationalid/test_SGP.py
@@ -1,7 +1,6 @@
 from unittest import TestCase, main
 
 from idnumbers.nationalid import SGP
-from idnumbers.nationalid.constant import Gender
 
 
 class TestSGPValidation(TestCase):