Repository: joke2k/faker
Pull request #1983 (2024-01-28 17:21:20)

feat(ssn)Add support uk_Ua ssn method birthday and gender optional pa…

### Now the ssn for the uk_Ua locale can generate values for a specific gender and date of birth. 

Add optional params birthdate and gender. 
Customize logic generation and write tests.

### What was wrong

Many validators in Ukraine check the ssn together with the date of birth, the previous implementation does not allow to pass validation, because the method takes a random date of birth. Gender was not taken into account at all.
Now it is possible to generate the desired value more accurately. If this is not necessary, the old logic will work.



Patch:
diff --git a/faker/providers/ssn/uk_UA/__init__.py b/faker/providers/ssn/uk_UA/__init__.py
index 4848d611..e647be2d 100644
--- a/faker/providers/ssn/uk_UA/__init__.py
+++ b/faker/providers/ssn/uk_UA/__init__.py
@@ -1,36 +1,59 @@
-from datetime import date
+import random
 
+from datetime import date, datetime
+from typing import Optional
+
+from ....typing import SexLiteral
 from .. import Provider as SsnProvider
 
 
+def select_gender(gender: SexLiteral) -> int:
+    """Choose an even number for Female and odd number for Male."""
+    gender = 0 if gender.lower() == "f" else 1
+    return random.choice(range(gender, 10, 2))
+
+
+def calculate_day_count(birthday: date) -> int:
+    """Calculate the day count from reference date '31 December 1899'."""
+    ref_date = date(1899, 12, 31)
+    return (birthday - ref_date).days
+
+
+def calculate_check_sum(val: str) -> int:
+    """Calculate checksum using INN calculation method."""
+    weights = [-1, 5, 7, 9, 4, 6, 10, 5, 7]
+    checksum = sum(int(v) * w for v, w in zip(val, weights))
+
+    return checksum % 11 % 10
+
+
 class Provider(SsnProvider):
-    def ssn(self) -> str:
+    def ssn(self, birthday: Optional[str] = None, gender: Optional[SexLiteral] = None) -> str:
         """
         Ukrainian "Реєстраційний номер облікової картки платника податків"
         also known as "Ідентифікаційний номер фізичної особи".
+        @params: birthday: "DD-MM-YYYY" format, default random date
+        @params: gender: "M" or "F" default: random gender
+
+        :sample:
+        :sample: birthday='22-06-1990', gender='F'
         """
-        digits = []
-
-        # Number of days between 1899-12-31 and a birth date
-        for digit in str((self.generator.date_object() - date(1899, 12, 31)).days):
-            digits.append(int(digit))
-
-        # Person's sequence number
-        for _ in range(4):
-            digits.append(self.random_int(0, 9))
-
-        checksum = (
-            digits[0] * -1
-            + digits[1] * 5
-            + digits[2] * 7
-            + digits[3] * 9
-            + digits[4] * 4
-            + digits[5] * 6
-            + digits[6] * 10
-            + digits[7] * 5
-            + digits[8] * 7
-        )
-        # Remainder of a checksum divided by 11 or 1 if it equals to 10
-        digits.append(checksum % 11 % 10)
-
-        return "".join(str(digit) for digit in digits)
+
+        try:
+            # generate day of birthday date object
+            if birthday:
+                dob = datetime.strptime(birthday, "%d-%m-%Y").date()
+            else:
+                dob = self.generator.date_object()
+        except Exception:
+            raise ValueError("Birthday format must be DD-MM-YYYY")
+
+        if gender and gender not in ("M", "F"):
+            raise ValueError('Gender must be "m" or "f" or None')
+
+        day_count = calculate_day_count(dob)
+        people_num = self.random_number(3, fix_len=True)
+        gender_ = select_gender(gender) if gender else random.randint(0, 1)
+        ssn_without_checksum = f"{day_count}{people_num}{gender_}"
+        checksum = calculate_check_sum(ssn_without_checksum)
+        return f"{ssn_without_checksum}{checksum}"


Test patch:
diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py
index b41f08d2..a4955c32 100644
--- a/tests/providers/test_ssn.py
+++ b/tests/providers/test_ssn.py
@@ -34,6 +34,7 @@ from faker.providers.ssn.pl_PL import checksum as pl_checksum
 from faker.providers.ssn.pt_BR import checksum as pt_checksum
 from faker.providers.ssn.ro_RO import ssn_checksum as ro_ssn_checksum
 from faker.providers.ssn.ro_RO import vat_checksum as ro_vat_checksum
+from faker.providers.ssn.uk_UA import Provider as uk_Provider
 from faker.providers.ssn.zh_TW import checksum as tw_checksum
 from faker.utils.checksums import luhn_checksum
 
@@ -1364,3 +1365,30 @@ class TestZhTW(unittest.TestCase):
     def test_checksum(self):
         for sample in self.samples:
             assert tw_checksum(sample) % 10 == 0
+
+
+class TestUkUA(unittest.TestCase):
+    def setUp(self):
+        self.fake = Faker("uk_Ua")
+        Faker.seed(0)
+        self.provider = uk_Provider
+
+    def test_ssn_len(self):
+        assert len(self.fake.ssn()) == 10
+
+    def test_start_ssn(self):
+        assert self.fake.ssn("21-06-1994")[:5] == "34505"
+
+    def test_ssn_gender(self):
+        m = self.fake.ssn(gender="M")
+        w = self.fake.ssn(gender="F")
+        assert int(m[8]) % 2 != 0, "Must be odd for men"
+        assert int(w[8]) % 2 == 0, "Must be even for women"
+
+    def test_incorrect_birthday(self):
+        with pytest.raises(ValueError):
+            self.fake.ssn(birthday="1994-06-01")
+
+    def test_incorrect_gender(self):
+        with pytest.raises(ValueError):
+            self.fake.ssn(gender="f")