Repository: identique/idnumbers (language: python)

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


Discussion / hints:

Fix patch:
diff --git a/idnumbers/nationalid/PRT.py b/idnumbers/nationalid/PRT.py
new file mode 100644
index 00000000..652e7126
--- /dev/null
+++ b/idnumbers/nationalid/PRT.py
@@ -0,0 +1,72 @@
+import re
+from types import SimpleNamespace
+from .util import validate_regexp, weighted_modulus_digit
+
+
+class CivilIDNumber:
+    """
+    Portugal Civil ID number
+    Número de identificação civil or NIC or BI
+    https://en.wikipedia.org/wiki/National_identification_number#Portugal
+    https://www.atractor.pt/mat/alg_controlo/bifm2-_en.html
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'PT',
+        # length without insignificant chars
+        'min_length': 9,
+        'max_length': 9,
+        'parsable': False,
+        'checksum': True,
+        'regexp': re.compile(r'^(\d{9})$')
+    })
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the PRT civil id number
+        """
+        if not validate_regexp(id_number, CivilIDNumber.METADATA.regexp):
+            return False
+        return CivilIDNumber.checksum(id_number)
+
+    @staticmethod
+    def checksum(id_number: str) -> bool:
+        multipliers = [9, 8, 7, 6, 5, 4, 3, 2]
+        mod = weighted_modulus_digit([int(i) for i in id_number[:-1]], multipliers, 11, True)
+        calculated_checksum = 0 if mod == 0 or mod == 1 else (11 - mod)
+        return calculated_checksum == int(id_number[-1])
+
+
+class TaxIDNumber:
+    """
+    Portugal Tax ID number
+    Número de identificação fiscal or NIF
+    https://en.wikipedia.org/wiki/National_identification_number#Portugal
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'PT',
+        # length without insignificant chars
+        'min_length': 9,
+        'max_length': 9,
+        'parsable': False,
+        'checksum': True,
+        'regexp': re.compile(r'^([12356][0-9]|45|7[012]|9[0189])\d{7}$')
+    })
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the Tax ID number
+        https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Portugal-TIN.pdf
+        """
+        if not validate_regexp(id_number, TaxIDNumber.METADATA.regexp):
+            return False
+        return TaxIDNumber.checksum(id_number)
+
+    @staticmethod
+    def checksum(id_number: str) -> bool:
+        multipliers = [9, 8, 7, 6, 5, 4, 3, 2]
+        mod = weighted_modulus_digit([int(i) for i in id_number[:-1]], multipliers, 11, True)
+        calculated_checksum = 0 if mod == 0 or mod == 1 else (11 - mod)
+        print(calculated_checksum)
+        return calculated_checksum == int(id_number[-1])


Test patch:
diff --git a/idnumbers/nationalid/test_PRT.py b/idnumbers/nationalid/test_PRT.py
new file mode 100644
index 00000000..101531c2
--- /dev/null
+++ b/idnumbers/nationalid/test_PRT.py
@@ -0,0 +1,19 @@
+from unittest import TestCase, main
+
+from idnumbers.nationalid import PRT
+
+
+class TestPRTCivilIDNumberValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(PRT.CivilIDNumber.validate('900000007'))
+        self.assertTrue(PRT.CivilIDNumber.validate('118666070'))
+        self.assertTrue(PRT.TaxIDNumber.validate('100000002'))
+
+    def test_error_case(self):
+        self.assertFalse(PRT.CivilIDNumber.validate('900000017'))
+        self.assertFalse(PRT.CivilIDNumber.validate('11866607-0'))
+        self.assertFalse(PRT.TaxIDNumber.validate('950000002'))
+
+
+if __name__ == '__main__':
+    main()