Repository: identique/idnumbers (language: python)

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


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 36c21390..709f0e22 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@ print(id_data['citizenship'])
 * NZL - New Zealand
 * PHL - Philippine
 * SGP - Singapore
+* SWE - Sweden
 * THA - Thailand
 * TWN - Taiwan
 * UKR - Ukraine
diff --git a/idnumbers/nationalid/SWE.py b/idnumbers/nationalid/SWE.py
new file mode 100644
index 00000000..93f37293
--- /dev/null
+++ b/idnumbers/nationalid/SWE.py
@@ -0,0 +1,78 @@
+import datetime
+import re
+from types import SimpleNamespace
+from .util import validate_regexp, luhn_digit
+from typing import Optional, TypedDict
+from .constant import Gender
+
+
+def normalize(id_number):
+    return re.sub(r'[+-]', '', id_number)
+
+
+class ParseResult(TypedDict):
+    gender: Gender
+    yyyymmdd: datetime.date
+    checksum: str
+
+
+class PersonalIdentityNumber:
+    """
+    Sweden Personal Identity number
+    https://en.wikipedia.org/wiki/National_identification_number#Sweden
+    https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden)
+    https://swedish.identityinfo.net/
+    https://personnummer.dev/
+    """
+    METADATA = SimpleNamespace(**{
+        'iso3166_alpha2': 'SE',
+        # length without insignificant chars
+        'min_length': 10,
+        'max_length': 10,
+        'parsable': True,
+        'checksum': True,
+        'regexp': re.compile(r'^(?P<yy>\d{2})(?P<mm>\d{2})(?P<dd>\d{2})'
+                             r'(?P<sep>[+|-])'
+                             r'(?!000)(?P<birth_number>\d{3})'
+                             r'(?P<checksum>\d)$')
+
+    })
+
+    @staticmethod
+    def validate(id_number: str) -> bool:
+        """
+        Validate the SWE id number
+        """
+        if not validate_regexp(id_number, PersonalIdentityNumber.METADATA.regexp):
+            return False
+        return PersonalIdentityNumber.parse(id_number) is not None
+
+    @staticmethod
+    def parse(id_number: str) -> Optional[ParseResult]:
+        match_obj = PersonalIdentityNumber.METADATA.regexp.match(id_number)
+        if not match_obj:
+            return None
+        yy = match_obj.group('yy')
+        mm = match_obj.group('mm')
+        dd = match_obj.group('dd')
+        birth_number = match_obj.group('birth_number')
+        sep = match_obj.group('sep')
+        base_year = datetime.datetime.now().year if sep == '-' else datetime.datetime.now().year - 100
+        yyyy = int((base_year - ((base_year - int(yy)) % 100)) / 100) * 100 + int(yy)
+        checksum = match_obj.group('checksum')
+        if PersonalIdentityNumber.checksum(id_number) != int(checksum):
+            return None
+        return {
+            "gender": Gender.FEMALE if int(birth_number) % 2 == 0 else Gender.MALE,
+            "yyyymmdd": datetime.date(yyyy, int(mm), int(dd)),
+            'checksum': match_obj.group('checksum')
+        }
+
+    @staticmethod
+    def checksum(id_number: str) -> int:
+        normalized = normalize(id_number)
+        '''
+        https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden)#Checksum
+        Multiplier start with 2, so we add a 0 in the start of the normalized digits
+        '''
+        return luhn_digit([0, *[int(char) for char in normalized[:-1]]])


Test patch:
diff --git a/idnumbers/nationalid/test_SWE.py b/idnumbers/nationalid/test_SWE.py
new file mode 100644
index 00000000..831961fb
--- /dev/null
+++ b/idnumbers/nationalid/test_SWE.py
@@ -0,0 +1,28 @@
+from unittest import TestCase, main
+from .constant import Gender
+
+from idnumbers.nationalid import SWE
+
+
+class TestSWEValidation(TestCase):
+    def test_normal_case(self):
+        self.assertTrue(SWE.PersonalIdentityNumber.validate('850709-9805'))
+        self.assertTrue(SWE.PersonalIdentityNumber.validate('191231+2392'))
+
+    def test_error_case(self):
+        self.assertFalse(SWE.PersonalIdentityNumber.validate('850709-9802'))
+        self.assertFalse(SWE.PersonalIdentityNumber.validate('191231+2391'))
+        self.assertFalse(SWE.PersonalIdentityNumber.validate('850709_9805'))
+        self.assertFalse(SWE.PersonalIdentityNumber.validate('850709 _ 9805'))
+
+    def test_parse(self):
+        result = SWE.PersonalIdentityNumber.parse('850709-9805')
+        self.assertEqual(1985, result['yyyymmdd'].year)
+        self.assertEqual(7, result['yyyymmdd'].month)
+        self.assertEqual(9, result['yyyymmdd'].day)
+        self.assertEqual(Gender.FEMALE, result['gender'])
+        self.assertEqual('5', result['checksum'])
+
+
+if __name__ == '__main__':
+    main()