Repository: joke2k/faker (python)

Issue:
Faker missing french licence plate
I noticed that Faker was missing the implementation of French licence plates. 
I would interested in adding this new feature :)

Pull request:
Add generating random french licence plates
* Add french licence plates
* Add corresponding tests

Closes #1118 


Patch:
diff --git a/faker/providers/automotive/fr_FR/__init__.py b/faker/providers/automotive/fr_FR/__init__.py
new file mode 100644
index 00000000..3fc9c7a2
--- /dev/null
+++ b/faker/providers/automotive/fr_FR/__init__.py
@@ -0,0 +1,11 @@
+from .. import Provider as AutomotiveProvider
+
+
+class Provider(AutomotiveProvider):
+    # Source (english): https://en.wikipedia.org/wiki/Vehicle_registration_plates_of_France
+    license_formats = (
+        # New format
+        '??-###-??',
+        # Old format for plates < 2009
+        '###-???-##',
+    )
diff --git a/faker/providers/date_time/__init__.py b/faker/providers/date_time/__init__.py
index 39dab838..d0d33aae 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -29,6 +29,29 @@ def timestamp_to_datetime(timestamp, tzinfo):
     return pick
 
 
+def change_year(current_date, year_diff):
+    """
+    Unless the current_date is February 29th, it is fine to just subtract years.
+    If it is a leap day, and we are rolling back to a non-leap year, it will
+    cause a ValueError.
+    Since this is relatively uncommon, just catch the error and roll forward to
+    March 1
+
+    current_date: date  object
+    year_diff: int year delta value, positive or negative
+    """
+    year = current_date.year + year_diff
+    try:
+        return current_date.replace(year=year)
+    except ValueError as e:
+        # ValueError thrown if trying to move date to a non-leap year if the current
+        # date is February 29th
+        if year != 0 and current_date.month == 2 and current_date.day == 29:
+            return current_date.replace(month=3, day=1, year=year)
+        else:
+            raise e
+
+
 class ParseError(ValueError):
     pass
 
@@ -1996,8 +2019,8 @@ class Provider(BaseProvider):
         # boundary.
 
         now = datetime.now(tzinfo).date()
-        start_date = now.replace(year=now.year - (maximum_age+1))
-        end_date = now.replace(year=now.year - minimum_age)
+        start_date = change_year(now, -(maximum_age + 1))
+        end_date = change_year(now, -minimum_age)
 
         dob = self.date_time_ad(tzinfo=tzinfo, start_datetime=start_date, end_datetime=end_date).date()
 


Test patch:
diff --git a/tests/providers/test_automotive.py b/tests/providers/test_automotive.py
index 7ad2874f..ddffb945 100644
--- a/tests/providers/test_automotive.py
+++ b/tests/providers/test_automotive.py
@@ -133,3 +133,16 @@ class TestRuRU(unittest.TestCase):
     def test_vehicle_category(self):
         category = self.fake.vehicle_category()
         assert isinstance(category, str)
+
+
+class TestFrFR(unittest.TestCase):
+
+    def setUp(self):
+        self.fake = Faker('fr_FR')
+        Faker.seed(0)
+        self.pattern = re.compile(r'^\d{3}-[A-Z]{3}-\d{2}$|^[A-Z]{2}-\d{3}-[A-Z]{2}')
+
+    def test_fr_FR_plate_format(self):
+        plate = self.fake.license_plate()
+        assert isinstance(plate, str)
+        assert self.pattern.match(plate)
diff --git a/tests/providers/test_date_time.py b/tests/providers/test_date_time.py
index 30e9cf09..82c4c8c3 100644
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -10,10 +10,12 @@ from datetime import date, datetime
 from datetime import time as datetime_time
 from datetime import timedelta, tzinfo
 
+import freezegun
 import pytest
 
 from faker import Faker
 from faker.providers.date_time import Provider as DatetimeProvider
+from faker.providers.date_time import change_year
 from faker.providers.date_time.ar_AA import Provider as ArProvider
 from faker.providers.date_time.ar_EG import Provider as EgProvider
 from faker.providers.date_time.hy_AM import Provider as HyAmProvider
@@ -369,8 +371,8 @@ class TestDateTime(unittest.TestCase):
 
     def test_date_time_between(self):
         now = datetime.now()
-        _30_years_ago = now.replace(year=now.year - 30)
-        _20_years_ago = now.replace(year=now.year - 20)
+        _30_years_ago = change_year(now, -30)
+        _20_years_ago = change_year(now, -20)
 
         random_datetime = self.fake.date_time_between(start_date='-30y', end_date='-20y')
 
@@ -379,8 +381,8 @@ class TestDateTime(unittest.TestCase):
 
     def test_date_between(self):
         today = date.today()
-        _30_years_ago = today.replace(year=today.year - 30)
-        _20_years_ago = today.replace(year=today.year - 20)
+        _30_years_ago = change_year(today, -30)
+        _20_years_ago = change_year(today, -20)
 
         random_date = self.fake.date_between(start_date='-30y', end_date='-20y')
 
@@ -501,6 +503,20 @@ class TestDateTime(unittest.TestCase):
             if platform.system() != 'Windows':
                 del os.environ['TZ']
 
+    def test_change_year(self):
+        _2020_06_01 = datetime.strptime("2020-06-01", "%Y-%m-%d")
+        _20_years_ago = change_year(_2020_06_01, -20)
+        self.assertEqual(_20_years_ago.strftime("%Y-%m-%d"), "2000-06-01")
+        # Verify a leap day
+        today = datetime.strptime("2020-02-29", "%Y-%m-%d")
+        with self.assertRaises(ValueError):
+            _11_years_ago = today.replace(year=11)
+        _11_years_ago = change_year(today, -11)
+        self.assertEqual(_11_years_ago.strftime("%Y-%m-%d"), "2009-03-01")
+        # 0 is an invalid year, so it should still raise a ValueError
+        with self.assertRaises(ValueError):
+            change_year(today, -today.year)
+
 
 class TestPlPL(unittest.TestCase):
 
@@ -593,6 +609,15 @@ class DatesOfBirth(unittest.TestCase):
         dob = self.fake.date_of_birth()
         assert isinstance(dob, date)
 
+    @freezegun.freeze_time('2020-02-29')
+    def test_date_of_birth_on_leap_day(self):
+        """
+        Freeze the date to a leap day to verify that the date_of_birth method  does not
+        raise an error
+        """
+        dob = self.fake.date_of_birth()
+        assert isinstance(dob, date)
+
     def test_value_errors(self):
         with self.assertRaises(ValueError):
             self.fake.date_of_birth(minimum_age=-1)
@@ -622,7 +647,7 @@ class DatesOfBirth(unittest.TestCase):
             now = datetime.now(utc).date()
 
             days_since_now = now - now
-            days_since_six_years_ago = now - now.replace(year=now.year-6)
+            days_since_six_years_ago = now - change_year(now, -6)
 
             dob = self.fake.date_of_birth(tzinfo=utc, minimum_age=0, maximum_age=5)
             days_since_dob = now - dob
@@ -635,7 +660,7 @@ class DatesOfBirth(unittest.TestCase):
             now = datetime.now(utc).date()
 
             days_since_now = now - now
-            days_since_nineteen_years_ago = now - now.replace(year=now.year-19)
+            days_since_nineteen_years_ago = now - change_year(now, -19)
 
             dob = self.fake.date_of_birth(tzinfo=utc, minimum_age=0, maximum_age=18)
             days_since_dob = now - dob
@@ -647,8 +672,8 @@ class DatesOfBirth(unittest.TestCase):
         for _ in range(100):
             now = datetime.now(utc).date()
 
-            days_since_five_years_ago = now - now.replace(year=now.year-5)
-            days_since_six_years_ago = now - now.replace(year=now.year-6)
+            days_since_five_years_ago = now - change_year(now, -5)
+            days_since_six_years_ago = now - change_year(now, -6)
 
             dob = self.fake.date_of_birth(tzinfo=utc, minimum_age=5, maximum_age=5)
             days_since_dob = now - dob
@@ -660,8 +685,8 @@ class DatesOfBirth(unittest.TestCase):
         for _ in range(100):
             now = datetime.now(utc).date()
 
-            days_since_one_hundred_years_ago = now - now.replace(year=now.year-100)
-            days_since_one_hundred_eleven_years_ago = now - now.replace(year=now.year-111)
+            days_since_one_hundred_years_ago = now - change_year(now, -100)
+            days_since_one_hundred_eleven_years_ago = now - change_year(now, -111)
 
             dob = self.fake.date_of_birth(minimum_age=100, maximum_age=110)
             days_since_dob = now - dob