Repository: joke2k/faker

Issue:
tests/test_passport.py::TestEnUS::testDates failure: ValueError: day is out of range for month
* Faker version: 18.10.0
* OS: Gentoo Linux amd64

It's possible that I've been incredibly lucky:

```
>           expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
E           ValueError: day is out of range for month
[…]
expiry_years = 10
issue_date = datetime.datetime(2020, 2, 29, 6, 57, 56)
```

Full traceback below.

### Steps to reproduce

1. `python -m pytest` ;-)

### Expected behavior

Tests passing ;-).

### Actual behavior

```pytb
_________________________________________________________ TestEnUS.testDates __________________________________________________________

self = <tests.test_passport.TestEnUS object at 0x7f6db2c3a920>, faker = <faker.proxy.Faker object at 0x7f6db21159f0>, num_samples = 20

    def testDates(self, faker, num_samples=20):
        age4 = date.today() - timedelta(days=4 * 365)
        age12 = date.today() - timedelta(days=12 * 365)
        age17 = date.today() - timedelta(days=17 * 365)
        age23 = date.today() - timedelta(days=23 * 365)
        age30 = date.today() - timedelta(days=30 * 365)
    
        birthdays = [(age4, 4), (age12, 12), (age17, 17), (age23, 23), (age30, 30)]
        for _ in range(num_samples):
            for birthday in birthdays:
>               birth_date_f, issue_date_f, expiry_date_f = faker.passport_dates(birthday[0])

_          = 4
age12      = datetime.date(2011, 6, 5)
age17      = datetime.date(2006, 6, 6)
age23      = datetime.date(2000, 6, 7)
age30      = datetime.date(1993, 6, 9)
age4       = datetime.date(2019, 6, 3)
birth_date = datetime.date(2006, 6, 6)
birth_date_f = '06 Jun 2006'
birthday   = (datetime.date(2000, 6, 7), 23)
birthdays  = [(datetime.date(2019, 6, 3), 4),
 (datetime.date(2011, 6, 5), 12),
 (datetime.date(2006, 6, 6), 17),
 (datetime.date(2000, 6, 7), 23),
 (datetime.date(1993, 6, 9), 30)]
expiry_date = datetime.date(2025, 4, 8)
expiry_date_f = '08 Apr 2025'
faker      = <faker.proxy.Faker object at 0x7f6db21159f0>
issue_date = datetime.date(2020, 4, 8)
issue_date_f = '08 Apr 2020'
num_samples = 20
self       = <tests.test_passport.TestEnUS object at 0x7f6db2c3a920>

tests/test_passport.py:55: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <faker.providers.passport.en_US.Provider object at 0x7f6db20938b0>, birthday = datetime.date(2000, 6, 7)

    def passport_dates(self, birthday: date = date.today()) -> Tuple[str, str, str]:
        """Generates a formatted date of birth, issue, and expiration dates.
        issue and expiration dates are conditioned to fall within U.S. standards of 5 and 10 year expirations
    
    
        The ``birthday`` argument is a datetime.date object representing a date of birth.
    
        Sources:
    
        -https://travel.state.gov/content/travel/en/passports/passport-help/faqs.html
        """
        birth_date = birthday.strftime("%d ") + birthday.strftime("%b ") + birthday.strftime("%Y")
        today = date.today()
        age = (today - birthday).days // 365
        if age < 16:
            expiry_years = 5
            issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
            # Checks if age is less than 5 so issue date is not before birthdate
            if age < 5:
                issue_date = self.generator.date_time_between(birthday, today)
            expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
    
            issue_date_fromat = issue_date.strftime("%d ") + issue_date.strftime("%b ") + issue_date.strftime("%Y")
            expiry_date_format = expiry_date.strftime("%d ") + expiry_date.strftime("%b ") + expiry_date.strftime("%Y")
            return birth_date, issue_date_fromat, expiry_date_format
        elif age >= 26:
            expiry_years = 10
            issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
            expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
            issue_date_fromat = issue_date.strftime("%d ") + issue_date.strftime("%b ") + issue_date.strftime("%Y")
            expiry_date_format = expiry_date.strftime("%d ") + expiry_date.strftime("%b ") + expiry_date.strftime("%Y")
            return birth_date, issue_date_fromat, expiry_date_format
    
        else:
            # In cases between age 16 and 26, the issue date is 5 years ago, but expiry may be in 10 or 5 years
            expiry_years = 5
            issue_date = self.generator.date_time_between(
                today - timedelta(days=expiry_years * 365 - 1), birthday + timedelta(days=16 * 365 - 1)
            )
            # all people over 21 must have been over 16 when they recieved passport or it will be expired otherwise
            if age >= 21:
                issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
                expiry_years = 10
    
>           expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
E           ValueError: day is out of range for month

age        = 23
birth_date = '07 Jun 2000'
birthday   = datetime.date(2000, 6, 7)
expiry_years = 10
issue_date = datetime.datetime(2020, 2, 29, 6, 57, 56)
self       = <faker.providers.passport.en_US.Provider object at 0x7f6db20938b0>
today      = datetime.date(2023, 6, 2)

faker/providers/passport/en_US/__init__.py:69: ValueError
```



Discussion:
Thanks for the report. Forced date replacement with leap years is always tricky (I remember a similar issue in another library: https://github.com/FactoryBoy/factory_boy/issues/969).

Would you be interested in submitting a corresponding PR to fix this?
I suppose I could do that. Should the date move to the next day or the previous day?
CC @llw2128 
let's the previous day, but on the issue date. IE: let's special-case issue date of Feb 29th, and make it Feb 28th

Resolving patch:
diff --git a/faker/providers/passport/en_US/__init__.py b/faker/providers/passport/en_US/__init__.py
--- a/faker/providers/passport/en_US/__init__.py
+++ b/faker/providers/passport/en_US/__init__.py
@@ -42,19 +42,9 @@ def passport_dates(self, birthday: date = date.today()) -> Tuple[str, str, str]:
             # Checks if age is less than 5 so issue date is not before birthdate
             if age < 5:
                 issue_date = self.generator.date_time_between(birthday, today)
-            expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
-
-            issue_date_fromat = issue_date.strftime("%d ") + issue_date.strftime("%b ") + issue_date.strftime("%Y")
-            expiry_date_format = expiry_date.strftime("%d ") + expiry_date.strftime("%b ") + expiry_date.strftime("%Y")
-            return birth_date, issue_date_fromat, expiry_date_format
         elif age >= 26:
             expiry_years = 10
             issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
-            expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
-            issue_date_fromat = issue_date.strftime("%d ") + issue_date.strftime("%b ") + issue_date.strftime("%Y")
-            expiry_date_format = expiry_date.strftime("%d ") + expiry_date.strftime("%b ") + expiry_date.strftime("%Y")
-            return birth_date, issue_date_fromat, expiry_date_format
-
         else:
             # In cases between age 16 and 26, the issue date is 5 years ago, but expiry may be in 10 or 5 years
             expiry_years = 5
@@ -66,11 +56,13 @@ def passport_dates(self, birthday: date = date.today()) -> Tuple[str, str, str]:
                 issue_date = self.generator.date_time_between(today - timedelta(days=expiry_years * 365 - 1), today)
                 expiry_years = 10
 
-            expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
+        if issue_date.day == 29 and issue_date.month == 2:
+            issue_date -= timedelta(days=1)
+        expiry_date = issue_date.replace(year=issue_date.year + expiry_years)
 
-            issue_date_fromat = issue_date.strftime("%d ") + issue_date.strftime("%b ") + issue_date.strftime("%Y")
-            expiry_date_format = expiry_date.strftime("%d ") + expiry_date.strftime("%b ") + expiry_date.strftime("%Y")
-            return birth_date, issue_date_fromat, expiry_date_format
+        issue_date_format = issue_date.strftime("%d ") + issue_date.strftime("%b ") + issue_date.strftime("%Y")
+        expiry_date_format = expiry_date.strftime("%d ") + expiry_date.strftime("%b ") + expiry_date.strftime("%Y")
+        return birth_date, issue_date_format, expiry_date_format
 
     def passport_gender(self, seed: int = 0) -> str:
         """Generates a string representing the gender displayed on a passport


Test patch: