Repository: joke2k/faker

Issue:
Give the user the ability to generate adult vs. minor dates of birth
Presently, dates of birth in the Profile provider are chosen randomly between unix epoch timestamp 0 and the present date and time. This isn't particularly useful from a testing perspective, as `0 <= age <= 48 years` isn't the most useful range.

### Expected behavior

Users should be able to explicitly generate fake dates of birth for adults or minors.

Discussion:
crd: Any thoughts as to why the specific unit test passes, but the full `coverage run` execution fails?

```
$ python -m unittest tests/providers/test_person.py
...............
----------------------------------------------------------------------
Ran 15 tests in 0.347s

OK
```

Versus:

```
$ coverage run --source=faker setup.py test
...
======================================================================
ERROR: test_documentor (tests.test_factory.FactoryTestCase)
----------------------------------------------------------------------
...
File "/Users/hwv729/workspaces/contrib/faker/faker/providers/person/__init__.py", line 134, in date_of_birth
    return self.generator.date_between(start_date=start_date, end_date=end_date)
AttributeError: 'Generator' object has no attribute 'date_between'

----------------------------------------------------------------------
Ran 246 tests in 15.029s

FAILED (errors=1)
Test failed: <unittest.runner.TextTestResult run=246 errors=1 failures=0>
error: Test failed: <unittest.runner.TextTestResult run=246 errors=1 failures=0>
```
fcurella: @crd I'm suprised it works on the unit test. The `faker.generator.Generator` class does not have that method.

I think I workaround would be move the `date_of_birth` method from the `person` provider to the `date_time` provider. It might be a little weird, but it wouldn't be totally wrong for it to be there. We're generating a `date` object after all.
crd: Please don't merge this quite yet -- I'm wrestling with Windows limitations. I'll let you know when I think it's in a good state.
crd: This is ready for your review, @fcurella -- it took a surprising amount of fiddling to get this to work due to poor Windows support of dates prior to 1970-01-01.
crd: Added a new test to generate a date of birth for an age between 100 and 110 and a note better explaining the `-62135596800` magic number. Should be in a good spot for acceptance now. Thanks for the feedback, @fcurella.

Patch:
diff --git a/faker/providers/date_time/__init__.py b/faker/providers/date_time/__init__.py
index 11c7b8fc..8b10d4c3 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -1203,15 +1203,24 @@ class Provider(BaseProvider):
         return datetime(1970, 1, 1, tzinfo=tzinfo) + \
             timedelta(seconds=self.unix_time(end_datetime=end_datetime))
 
-    def date_time_ad(self, tzinfo=None, end_datetime=None):
+    def date_time_ad(self, tzinfo=None, end_datetime=None, start_datetime=None):
         """
         Get a datetime object for a date between January 1, 001 and now
         :param tzinfo: timezone, instance of datetime.tzinfo subclass
         :example DateTime('1265-03-22 21:15:52')
         :return datetime
         """
+
+        # 1970-01-01 00:00:00 UTC minus 62135596800 seconds is
+        # 0001-01-01 00:00:00 UTC.  Since _parse_end_datetime() is used
+        # elsewhere where a default value of 0 is expected, we can't
+        # simply change that class method to use this magic number as a
+        # default value when None is provided.
+
+        start_time = -62135596800 if start_datetime is None else self._parse_start_datetime(start_datetime)
         end_datetime = self._parse_end_datetime(end_datetime)
-        ts = self.generator.random.randint(-62135596800, end_datetime)
+
+        ts = self.generator.random.randint(start_time, end_datetime)
         # NOTE: using datetime.fromtimestamp(ts) directly will raise
         #       a "ValueError: timestamp out of range for platform time_t"
         #       on some platforms due to system C functions;
@@ -1747,3 +1756,44 @@ class Provider(BaseProvider):
     def timezone(self):
         return self.generator.random.choice(
             self.random_element(self.countries)['timezones'])
+
+    def date_of_birth(self, tzinfo=None, minimum_age=0, maximum_age=115):
+        """
+        Generate a random date of birth represented as a Date object,
+        constrained by optional miminimum_age and maximum_age
+        parameters.
+
+        :param tzinfo Defaults to None.
+        :param minimum_age Defaults to 0.
+        :param maximum_age Defaults to 115.
+        
+        :example Date('1979-02-02')
+        :return Date
+        """
+
+        if not isinstance(minimum_age, int):
+            raise TypeError("minimum_age must be an integer.")
+
+        if not isinstance(maximum_age, int):
+            raise TypeError("maximum_age must be an integer.")
+
+        if (maximum_age < 0):
+            raise ValueError("maximum_age must be greater than or equal to zero.")
+
+        if (minimum_age < 0):
+            raise ValueError("minimum_age must be greater than or equal to zero.")
+
+        if (minimum_age > maximum_age):
+            raise ValueError("minimum_age must be less than or equal to maximum_age.")
+
+        # In order to return the full range of possible dates of birth, add one
+        # year to the potential age cap and subtract one day if we land on the 
+        # 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)
+
+        dob = self.date_time_ad(tzinfo=tzinfo, start_datetime=start_date, end_datetime=end_date).date()
+
+        return dob if dob != start_date else dob + timedelta(days=1)
diff --git a/faker/providers/profile/__init__.py b/faker/providers/profile/__init__.py
index bcf5b5e5..a43622fd 100644
--- a/faker/providers/profile/__init__.py
+++ b/faker/providers/profile/__init__.py
@@ -29,7 +29,7 @@ class Provider(BaseProvider):
             "mail": self.generator.free_email(),
 
             #"password":self.generator.password()
-            "birthdate": self.generator.date(),
+            "birthdate": self.generator.date_of_birth(),
 
         }
 


Test patch:
diff --git a/tests/providers/test_date_time.py b/tests/providers/test_date_time.py
index 6b912b96..3f011a79 100644
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -471,3 +471,95 @@ class TestAr(unittest.TestCase):
             factory.month_name(),
             ArProvider.MONTH_NAMES.values()
         )
+
+
+class DatesOfBirth(unittest.TestCase):
+    from faker.providers.date_time import datetime_to_timestamp
+
+    """
+    Test Dates of Birth
+    """
+
+    def setUp(self):
+        self.factory = Faker()
+        self.factory.seed(0)
+
+    def test_date_of_birth(self):
+        dob = self.factory.date_of_birth()
+        assert isinstance(dob, date)
+
+    def test_value_errors(self):
+        with self.assertRaises(ValueError):
+            self.factory.date_of_birth(minimum_age=-1)
+
+        with self.assertRaises(ValueError):
+            self.factory.date_of_birth(maximum_age=-1)
+
+        with self.assertRaises(ValueError):
+            self.factory.date_of_birth(minimum_age=-2, maximum_age=-1)
+
+        with self.assertRaises(ValueError):
+            self.factory.date_of_birth(minimum_age=5, maximum_age=4)
+
+    def test_type_errors(self):
+        with self.assertRaises(TypeError):
+            self.factory.date_of_birth(minimum_age=0.5)
+
+        with self.assertRaises(TypeError):
+            self.factory.date_of_birth(maximum_age='hello')
+
+    def test_bad_age_range(self):
+        with self.assertRaises(ValueError):
+            self.factory.date_of_birth(minimum_age=5, maximum_age=0)
+
+    def test_acceptable_age_range_five_years(self):
+        for _ in range(100):
+            now = datetime.now(utc).date()
+
+            days_since_now = now - now
+            days_since_six_years_ago = now - now.replace(year=now.year-6)
+            
+            dob = self.factory.date_of_birth(tzinfo=utc, minimum_age=0, maximum_age=5)
+            days_since_dob = now - dob
+
+            assert isinstance(dob, date)
+            assert days_since_six_years_ago > days_since_dob >= days_since_now 
+
+    def test_acceptable_age_range_eighteen_years(self):
+        for _ in range(100):
+            now = datetime.now(utc).date()
+
+            days_since_now = now - now
+            days_since_nineteen_years_ago = now - now.replace(year=now.year-19)
+            
+            dob = self.factory.date_of_birth(tzinfo=utc, minimum_age=0, maximum_age=18)
+            days_since_dob = now - dob
+
+            assert isinstance(dob, date)
+            assert days_since_nineteen_years_ago > days_since_dob >= days_since_now
+    
+    def test_identical_age_range(self):
+        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)
+
+            dob = self.factory.date_of_birth(minimum_age=5, maximum_age=5)
+            days_since_dob = now - dob
+
+            assert isinstance(dob, date)
+            assert days_since_six_years_ago > days_since_dob >= days_since_five_years_ago
+
+    def test_distant_age_range(self):
+        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)
+
+            dob = self.factory.date_of_birth(minimum_age=100, maximum_age=110)
+            days_since_dob = now - dob
+
+            assert isinstance(dob, date)
+            assert days_since_one_hundred_eleven_years_ago > days_since_dob >= days_since_one_hundred_years_ago