Repository: joke2k/faker
Pull request #596 (2017-09-13 10:30:10)

Date feature parity with datetime
This PR ensure feature parity between `date` and `datetime`


Patch:
diff --git a/faker/providers/date_time/__init__.py b/faker/providers/date_time/__init__.py
index 0d1eb6b0..34885eb3 100644
--- a/faker/providers/date_time/__init__.py
+++ b/faker/providers/date_time/__init__.py
@@ -367,6 +367,24 @@ class Provider(BaseProvider):
             return datetime_to_timestamp(now + timedelta(text))
         raise ParseError("Invalid format for date '{0}'".format(text))
 
+    @classmethod
+    def _parse_date(cls, value):
+        if isinstance(value, (datetime, real_datetime)):
+            return value.date()
+        elif isinstance(value, (date, real_date)):
+            return value
+        today = date.today()
+        if isinstance(value, timedelta):
+            return today - value
+        if is_string(value):
+            if value in ('today', 'now'):
+                return today
+            time_params = cls._parse_date_string(value)
+            return today + timedelta(**time_params)
+        if isinstance(value, int):
+            return today + timedelta(value)
+        raise ParseError("Invalid format for date '{0}'".format(value))
+
     def date_time_between(self, start_date='-30y', end_date='now', tzinfo=None):
         """
         Get a DateTime object based on a random date between two given dates.
@@ -383,6 +401,21 @@ class Provider(BaseProvider):
         timestamp = self.generator.random.randint(start_date, end_date)
         return datetime(1970, 1, 1,tzinfo=tzinfo) + timedelta(seconds=timestamp)
 
+    def date_between(self, start_date='-30y', end_date='today'):
+        """
+        Get a Date object based on a random date between two given dates.
+        Accepts date strings that can be recognized by strtotime().
+
+        :param start_date Defaults to 30 years ago
+        :param end_date Defaults to "today"
+        :example Date('1999-02-02')
+        :return Date
+        """
+
+        start_date = self._parse_date(start_date)
+        end_date = self._parse_date(end_date)
+        return self.date_between_dates(date_start=start_date, date_end=end_date)
+
     def future_datetime(self, end_date='+30d', tzinfo=None):
         """
         Get a DateTime object based on a random date between 1 second form now
@@ -409,9 +442,7 @@ class Provider(BaseProvider):
         :example DateTime('1999-02-02 11:42:52')
         :return DateTime
         """
-        return self.date_time_between(
-            start_date='+1d', end_date=end_date, tzinfo=tzinfo,
-        ).date()
+        return self.date_between(start_date='+1d', end_date=end_date)
 
     def past_datetime(self, start_date='-30d', tzinfo=None):
         """
@@ -439,9 +470,7 @@ class Provider(BaseProvider):
         :example DateTime('1999-02-02 11:42:52')
         :return DateTime
         """
-        return self.date_time_between(
-            start_date=start_date, end_date='-1d', tzinfo=tzinfo,
-        ).date()
+        return self.date_between(start_date=start_date, end_date='-1d')
 
     def date_time_between_dates(self, datetime_start=None, datetime_end=None, tzinfo=None):
         """
@@ -472,6 +501,17 @@ class Provider(BaseProvider):
             pick = datetime.fromtimestamp(timestamp, tzinfo)
         return pick
 
+    def date_between_dates(self, date_start=None, date_end=None):
+        """
+        Takes two Date objects and returns a random date between the two given dates.
+        Accepts Date or Datetime objects
+
+        :param date_start: Date
+        :param date_end: Date
+        :return Date
+        """
+        return self.date_time_between_dates(date_start, date_end).date()
+
     def date_time_this_century(self, before_now=True, after_now=False, tzinfo=None):
         """
         Gets a DateTime object for the current century.
@@ -564,6 +604,95 @@ class Provider(BaseProvider):
         else:
             return now
 
+    def date_this_century(self, before_today=True, after_today=False):
+        """
+        Gets a Date object for the current century.
+
+        :param before_today: include days in current century before today
+        :param after_today: include days in current century after today
+        :example Date('2012-04-04')
+        :return Date
+        """
+        today = date.today()
+        this_century_start = date(today.year - (today.year % 100), 1, 1)
+        next_century_start = date(this_century_start.year + 100, 1, 1)
+
+        if before_today and after_today:
+            return self.date_between_dates(this_century_start, next_century_start)
+        elif not before_today and after_today:
+            return self.date_between_dates(today, next_century_start)
+        elif not after_today and before_today:
+            return self.date_between_dates(this_century_start, today)
+        else:
+            return today
+
+    def date_this_decade(self, before_today=True, after_today=False):
+        """
+        Gets a Date object for the decade year.
+
+        :param before_today: include days in current decade before today
+        :param after_today: include days in current decade after today
+        :example Date('2012-04-04')
+        :return Date
+        """
+        today = date.today()
+        this_decade_start = date(today.year - (today.year % 10), 1, 1)
+        next_decade_start = date(this_decade_start.year + 10, 1, 1)
+
+        if before_today and after_today:
+            return self.date_between_dates(this_decade_start, next_decade_start)
+        elif not before_today and after_today:
+            return self.date_between_dates(today, next_decade_start)
+        elif not after_today and before_today:
+            return self.date_between_dates(this_decade_start, today)
+        else:
+            return today
+
+    def date_this_year(self, before_today=True, after_today=False):
+        """
+        Gets a Date object for the current year.
+
+        :param before_today: include days in current year before today
+        :param after_today: include days in current year after today
+        :example Date('2012-04-04')
+        :return Date
+        """
+        today = date.today()
+        this_year_start = today.replace(month=1, day=1)
+        next_year_start = date(today.year + 1, 1, 1)
+
+        if before_today and after_today:
+            return self.date_between_dates(this_year_start, next_year_start)
+        elif not before_today and after_today:
+            return self.date_between_dates(today, next_year_start)
+        elif not after_today and before_today:
+            return self.date_between_dates(this_year_start, today)
+        else:
+            return today
+
+    def date_this_month(self, before_today=True, after_today=False):
+        """
+        Gets a Date object for the current month.
+
+        :param before_today: include days in current month before today
+        :param after_today: include days in current month after today
+        :param tzinfo: timezone, instance of datetime.tzinfo subclass
+        :example DateTime('2012-04-04 11:02:02')
+        :return DateTime
+        """
+        today = date.today()
+        this_month_start = today.replace(day=1)
+
+        next_month_start = this_month_start + relativedelta.relativedelta(months=1)
+        if before_today and after_today:
+            return self.date_between_dates(this_month_start, next_month_start)
+        elif not before_today and after_today:
+            return self.date_between_dates(today, next_month_start)
+        elif not after_today and before_today:
+            return self.date_between_dates(this_month_start, today)
+        else:
+            return today
+
     def time_series(self, start_date='-30d', end_date='now', precision=None, distrib=None, tzinfo=None):
         """
         Returns a generator yielding tuples of ``(<datetime>, <value>)``.


Test patch:
diff --git a/tests/providers/test_date_time.py b/tests/providers/test_date_time.py
index b63551c4..15fa6c4e 100644
--- a/tests/providers/test_date_time.py
+++ b/tests/providers/test_date_time.py
@@ -33,6 +33,7 @@ class UTC(tzinfo):
 
 utc = UTC()
 
+
 class TestKoKR(unittest.TestCase):
 
     def setUp(self):
@@ -41,6 +42,7 @@ class TestKoKR(unittest.TestCase):
     def test_day(self):
         day = self.factory.day_of_week()
         assert isinstance(day, string_types)
+
     def test_month(self):
         month = self.factory.month()
         assert isinstance(month, string_types)
@@ -51,6 +53,10 @@ class TestDateTime(unittest.TestCase):
     def setUp(self):
         self.factory = Factory.create()
 
+    def assertBetween(self, date, start_date, end_date):
+        self.assertTrue(date <= end_date)
+        self.assertTrue(date >= start_date)
+
     def test_day(self):
         day = self.factory.day_of_week()
         assert isinstance(day, string_types)
@@ -79,6 +85,30 @@ class TestDateTime(unittest.TestCase):
         timestamp = DatetimeProvider._parse_date_time('+30d')
         now = DatetimeProvider._parse_date_time('now')
         self.assertTrue(timestamp > now)
+        delta = timedelta(days=-30)
+        from_delta = DatetimeProvider._parse_date_time(delta)
+        from_int = DatetimeProvider._parse_date_time(30)
+
+        self.assertEqual(datetime.fromtimestamp(from_delta).date(),
+                         datetime.fromtimestamp(timestamp).date())
+
+        self.assertEqual(datetime.fromtimestamp(from_int).date(),
+                         datetime.fromtimestamp(timestamp).date())
+
+    def test_parse_date(self):
+        parsed = DatetimeProvider._parse_date('+30d')
+        now = DatetimeProvider._parse_date('now')
+        today = DatetimeProvider._parse_date('today')
+        self.assertIsInstance(parsed, date)
+        self.assertIsInstance(now, date)
+        self.assertIsInstance(today, date)
+        self.assertEqual(today, date.today())
+        self.assertEqual(now, today)
+        self.assertEqual(parsed, today + timedelta(days=30))
+        self.assertEqual(DatetimeProvider._parse_date(datetime.now()), today)
+        self.assertEqual(DatetimeProvider._parse_date(parsed), parsed)
+        self.assertEqual(DatetimeProvider._parse_date(30), parsed)
+        self.assertEqual(DatetimeProvider._parse_date(timedelta(days=-30)), parsed)
 
     def test_timezone_conversion(self):
         from faker.providers.date_time import datetime_to_timestamp
@@ -158,6 +188,14 @@ class TestDateTime(unittest.TestCase):
         self.assertTrue(datetime_start <= random_date)
         self.assertTrue(datetime_end >= random_date)
 
+    def test_date_between_dates(self):
+        date_end = date.today()
+        date_start = date_end - timedelta(days=10)
+
+        random_date = self.factory.date_between_dates(date_start, date_end)
+        self.assertTrue(date_start <= random_date)
+        self.assertTrue(date_end >= random_date)
+
     def _datetime_to_time(self, value):
         return int(time.mktime(value.timetuple()))
 
@@ -224,6 +262,52 @@ class TestDateTime(unittest.TestCase):
             datetime.now(utc).replace(second=0, microsecond=0)
         )
 
+    def test_date_this_period(self):
+        # test century
+        self.assertTrue(self.factory.date_this_century(after_today=False) <= date.today())
+        self.assertTrue(self.factory.date_this_century(before_today=False, after_today=True) >= date.today())
+        # test decade
+        self.assertTrue(self.factory.date_this_decade(after_today=False) <= date.today())
+        self.assertTrue(self.factory.date_this_decade(before_today=False, after_today=True) >= date.today())
+        self.assertEqual(
+            self.factory.date_this_decade(before_today=False, after_today=False),
+            date.today()
+        )
+        # test year
+        self.assertTrue(self.factory.date_this_year(after_today=False) <= date.today())
+        self.assertTrue(self.factory.date_this_year(before_today=False, after_today=True) >= date.today())
+        self.assertEqual(
+            self.factory.date_this_year(before_today=False, after_today=False),
+            date.today()
+        )
+        # test month
+        self.assertTrue(self.factory.date_this_month(after_today=False) <= date.today())
+        self.assertTrue(self.factory.date_this_month(before_today=False, after_today=True) >= date.today())
+        self.assertEqual(
+            self.factory.date_this_month(before_today=False, after_today=False),
+            datetime.now()
+        )
+
+    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)
+
+        random_datetime = self.factory.date_time_between(start_date='-30y', end_date='-20y')
+
+        self.assertIsInstance(random_datetime, datetime)
+        self.assertBetween(random_datetime, _30_years_ago, _20_years_ago)
+
+    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)
+
+        random_date = self.factory.date_between(start_date='-30y', end_date='-20y')
+
+        self.assertIsInstance(random_date, date)
+        self.assertBetween(random_date, _30_years_ago, _20_years_ago)
+
     def test_parse_timedelta(self):
         from faker.providers.date_time import Provider