Repository: identique/idnumbers (language: python)

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


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 21b81c5a..6914857c 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,7 @@ print(id_data['citizenship'])
 * JPN - Japan
 * KOR - South Korea
 * IND - India
+* IRL - Ireland
 * LKA - Sri Lanka
 * MYS - Malaysia
 * NGA - Nigeria
diff --git a/idnumbers/nationalid/IRL.py b/idnumbers/nationalid/IRL.py
new file mode 100644
index 00000000..2098102a
--- /dev/null
+++ b/idnumbers/nationalid/IRL.py
@@ -0,0 +1,48 @@
+import re
+from types import SimpleNamespace
+from .util import validate_regexp, modulus_check, letter_to_number
+
+
+def normalize(id_number: str) -> str:
+    return id_number.replace('/', '')
+
+
+class PersonalPublicServiceNumber:
+    """
+    Ireland Personal Public Service Number
+    https://en.wikipedia.org/wiki/Personal_Public_Service_Number
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'IE',
+        'min_length': 8,
+        'max_length': 10,
+        'parsable': False,
+        'checksum': True,
+        'regexp': re.compile(r'^\d{7}[A-W][A-W\s]?$|'
+                             r'^\d{7}[A-W]/[A-W\s]?$')
+    })
+
+    MAGIC_MULTIPLIER = [8, 7, 6, 5, 4, 3, 2, 9]
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the IRL personal public service number
+        """
+        if not validate_regexp(id_number, PersonalPublicServiceNumber.METADATA.regexp):
+            return False
+        return PersonalPublicServiceNumber.checksum(id_number)
+
+    @staticmethod
+    def checksum(id_number: str) -> bool:
+        normalized = normalize(id_number)
+        number_list = [int(i) for i in normalized[:7]]
+        if len(normalized) == 9 and normalized[-1] not in [' ', 'W']:
+            number_list.append(letter_to_number(normalized[-1]))
+        modulus = modulus_check(numbers=number_list,
+                                weights=PersonalPublicServiceNumber.MAGIC_MULTIPLIER,
+                                divider=23, modulus_only=True)
+        # the last digit is a check_char if the length is 8
+        check_char = normalized[-2] if len(normalized) == 9 else normalized[-1]
+
+        return modulus == letter_to_number(check_char) % 23
diff --git a/idnumbers/nationalid/util.py b/idnumbers/nationalid/util.py
index 819746bf..4aa3b6c2 100644
--- a/idnumbers/nationalid/util.py
+++ b/idnumbers/nationalid/util.py
@@ -69,3 +69,10 @@ def modulus_check(numbers: List[int], weights: List[int], divider: int, modulus_
 
 def modulus_overflow_mod10(modulus: 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], modulus % 10 if modulus > 9 else modulus)
+
+
+def letter_to_number(letter: str, capital: bool = True):
+    assert len(letter) == 1 and letter.isalpha(), 'only allow one alphabet'
+    if capital:
+        return ord(letter) - 64
+    return ord(letter) - 96


Test patch:
diff --git a/idnumbers/nationalid/test_IRL.py b/idnumbers/nationalid/test_IRL.py
new file mode 100644
index 00000000..fcbf925e
--- /dev/null
+++ b/idnumbers/nationalid/test_IRL.py
@@ -0,0 +1,30 @@
+from unittest import TestCase, main
+from idnumbers.nationalid import IRL
+
+
+class TestIRLPPSValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(IRL.PersonalPublicServiceNumber.validate('1234567FA'))
+        self.assertTrue(IRL.PersonalPublicServiceNumber.validate('1234567F/A'))
+        self.assertTrue(IRL.PersonalPublicServiceNumber.validate('1234567T'))
+        self.assertTrue(IRL.PersonalPublicServiceNumber.validate('1234567T '))
+        self.assertTrue(IRL.PersonalPublicServiceNumber.validate('1234567TW'))
+
+    def test_error_case(self):
+        self.assertFalse(IRL.PersonalPublicServiceNumber.validate('1234567AA'))
+        self.assertFalse(IRL.PersonalPublicServiceNumber.validate('1234567AX'))
+        self.assertFalse(IRL.PersonalPublicServiceNumber.validate('1234567XX'))
+        self.assertFalse(IRL.PersonalPublicServiceNumber.validate('1234567FAA'))
+        self.assertFalse(IRL.PersonalPublicServiceNumber.validate('123456F'))
+        self.assertFalse(IRL.PersonalPublicServiceNumber.validate('123456FT'))
+        self.assertFalse(IRL.PersonalPublicServiceNumber.validate('123456F//A'))
+
+    def test_with_regex(self):
+        self.assertRegex('1234567FA', IRL.PersonalPublicServiceNumber.METADATA.regexp)
+
+    def test_with_metadata(self):
+        self.assertIsNotNone(IRL.PersonalPublicServiceNumber.METADATA)
+
+
+if __name__ == '__main__':
+    main()