Repository: identique/idnumbers (language: python)

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


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 491e4e6c..9be62611 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,7 @@ print(id_data['citizenship'])
 * MYS - Malaysia
 * NGA - Nigeria
 * NLD - Netherlands
+* NOR - Norway
 * NZL - New Zealand
 * PHL - Philippine
 * SGP - Singapore
diff --git a/idnumbers/nationalid/NOR.py b/idnumbers/nationalid/NOR.py
new file mode 100644
index 00000000..43b4172a
--- /dev/null
+++ b/idnumbers/nationalid/NOR.py
@@ -0,0 +1,83 @@
+import re
+from types import SimpleNamespace
+from typing import Optional, TypedDict
+from datetime import date
+
+from .util import validate_regexp
+from .constant import Gender
+
+
+class ParseResult(TypedDict):
+    gender: Gender
+    yyyymmdd: date
+    checksum: str
+
+
+class NationalID:
+    """
+    Norway National ID number
+    https://en.wikipedia.org/wiki/National_identification_number#Norway
+    https://en.wikipedia.org/wiki/National_identity_number_(Norway)
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'NO',
+        # length without insignificant chars
+        'min_length': 11,
+        'max_length': 11,
+        'parsable': True,
+        'checksum': True,
+        'regexp': re.compile(r'^(?P<dd>\d{2})'
+                             r'(?P<mm>\d{2})'
+                             r'(?P<yy>\d{2})'
+                             r'(?P<individual_number>\d{3})'
+                             r'(?P<checksum>\d{2})$')
+    })
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the NOR id number
+        """
+        if not validate_regexp(id_number, NationalID.METADATA.regexp):
+            return False
+        if not NationalID.parse(id_number):
+            return False
+        return NationalID.checksum(id_number)
+
+    @staticmethod
+    def parse(id_number: str) -> Optional[ParseResult]:
+        match_obj = NationalID.METADATA.regexp.match(id_number)
+        if not match_obj:
+            return None
+
+        individual_code = match_obj.group('individual_number')
+        yy = match_obj.group('yy')
+        mm = match_obj.group('mm')
+        dd = match_obj.group('dd')
+
+        birth_century = '20'
+        individual_num = int(individual_code)
+        if 0 <= individual_num < 500:
+            birth_century = 19
+        elif 500 <= individual_num < 750 and int(yy) >= 54:
+            birth_century = 18
+        elif 900 <= individual_num < 1000 and int(yy) >= 40:
+            birth_century = 19
+
+        return {
+            "gender": Gender.FEMALE if int(individual_code[2]) % 2 == 0 else Gender.MALE,
+            'yyyymmdd': date(int(f'{birth_century}{yy}'), int(mm), int(dd)),
+            "checksum": match_obj.group('checksum')
+        }
+
+    FIRST_MAGIC_MULTIPLIER = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1]
+    SECOND_MAGIC_MULTIPLIER = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1]
+
+    @staticmethod
+    def checksum(id_number: str) -> bool:
+        number_list = [int(char) for char in id_number]
+        # Digit 10th
+        first_total = sum([value * number_list[idx] for (idx, value) in enumerate(NationalID.FIRST_MAGIC_MULTIPLIER)])
+        # Digit 11th
+        second_total = sum([value * number_list[idx] for (idx, value) in enumerate(NationalID.SECOND_MAGIC_MULTIPLIER)])
+        return first_total % 11 == 0 and second_total % 11 == 0


Test patch:
diff --git a/idnumbers/nationalid/test_NOR.py b/idnumbers/nationalid/test_NOR.py
new file mode 100644
index 00000000..c9cc6586
--- /dev/null
+++ b/idnumbers/nationalid/test_NOR.py
@@ -0,0 +1,23 @@
+from unittest import TestCase, main
+from datetime import date
+
+from idnumbers.nationalid import NOR
+from .constant import Gender
+
+
+class TestNORValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(NOR.NationalID.validate('29029600013'))
+
+    def test_error_case(self):
+        self.assertFalse(NOR.NationalID.validate('29029600012'))
+
+    def test_parse(self):
+        result = NOR.NationalID.parse('29029600013')
+        self.assertEqual(Gender.FEMALE, result['gender'])
+        self.assertEqual(date(1996, 2, 29), result['yyyymmdd'])
+        self.assertEqual('13', result['checksum'])
+
+
+if __name__ == '__main__':
+    main()