From cf3a675018c4b68a50df017dd5299900028e70da Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Sun, 22 Oct 2017 23:32:34 -0400 Subject: [PATCH 1/4] Closes bpo-28292: Implemented Calendar.itermonthdays3() and itermonthdays4(). --- Lib/calendar.py | 60 ++++++++++++++++++++++++++++++--------- Lib/test/test_calendar.py | 16 +++++++---- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/Lib/calendar.py b/Lib/calendar.py index 0218e2d39770fac..73715fb05bb6fed 100644 --- a/Lib/calendar.py +++ b/Lib/calendar.py @@ -126,6 +126,24 @@ def monthrange(year, month): return day1, ndays +def monthlen(year, month): + return mdays[month] + (month == February and isleap(year)) + + +def prevmonth(year, month): + if month == 1: + return year-1, 12 + else: + return year, month-1 + + +def nextmonth(year, month): + if month == 12: + return year+1, 1 + else: + return year, month+1 + + class Calendar(object): """ Base calendar class. This class doesn't do any formatting. It simply @@ -157,20 +175,8 @@ def itermonthdates(self, year, month): values and will always iterate through complete weeks, so it will yield dates outside the specified month. """ - date = datetime.date(year, month, 1) - # Go back to the beginning of the week - days = (date.weekday() - self.firstweekday) % 7 - date -= datetime.timedelta(days=days) - oneday = datetime.timedelta(days=1) - while True: - yield date - try: - date += oneday - except OverflowError: - # Adding one day could fail after datetime.MAXYEAR - break - if date.month != month and date.weekday() == self.firstweekday: - break + for y, m, d in self.itermonthdays3(year, month): + yield datetime.date(y, m, d) def itermonthdays2(self, year, month): """ @@ -180,6 +186,32 @@ def itermonthdays2(self, year, month): for i, d in enumerate(self.itermonthdays(year, month), self.firstweekday): yield d, i % 7 + def itermonthdays3(self, year, month): + """ + Like itermonthdates(), but will yield (year, month, day) tuples. Can be + used for dates outside of datetime.date range. + """ + day1, ndays = monthrange(year, month) + days_before = (day1 - self.firstweekday) % 7 + days_after = (self.firstweekday - day1 - ndays) % 7 + y, m = prevmonth(year, month) + end = monthlen(y, m) + 1 + for d in range(end-days_before, end): + yield y, m, d + for d in range(1, ndays + 1): + yield year, month, d + y, m = nextmonth(year, month) + for d in range(1, days_after + 1): + yield y, m, d + + def itermonthdays4(self, year, month): + """ + Like itermonthdates(), but will yield (year, month, day, day_of_week) tuples. + Can be used for dates outside of datetime.date range. + """ + for i, (y, m, d) in enumerate(self.itermonthdays3(year, month)): + yield y, m, d, (self.firstweekday + i) % 7 + def itermonthdays(self, year, month): """ Like itermonthdates(), but will yield day numbers. For days outside diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index c777f6483567ddb..ad8b6bb6b93e348 100644 --- a/Lib/test/test_calendar.py +++ b/Lib/test/test_calendar.py @@ -502,10 +502,15 @@ def test_locale_calendars(self): new_october = calendar.TextCalendar().formatmonthname(2010, 10, 10) self.assertEqual(old_october, new_october) - def test_itermonthdates(self): - # ensure itermonthdates doesn't overflow after datetime.MAXYEAR - # see #15421 - list(calendar.Calendar().itermonthdates(datetime.MAXYEAR, 12)) + def test_itermonthdays3(self): + # ensure itermonthdays3 doesn't overflow after datetime.MAXYEAR + list(calendar.Calendar().itermonthdays3(datetime.MAXYEAR, 12)) + + def test_itermonthdays4(self): + cal = calendar.Calendar(firstweekday=3) + days = list(cal.itermonthdays4(2001, 2)) + self.assertEqual(days[0], (2001, 2, 1, 3)) + self.assertEqual(days[-1], (2001, 2, 28, 2)) def test_itermonthdays(self): for firstweekday in range(7): @@ -846,7 +851,8 @@ def test__all__(self): blacklist = {'mdays', 'January', 'February', 'EPOCH', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY', 'different_locale', 'c', - 'prweek', 'week', 'format', 'formatstring', 'main'} + 'prweek', 'week', 'format', 'formatstring', 'main', + 'monthlen', 'prevmonth', 'nextmonth'} support.check__all__(self, calendar, blacklist=blacklist) From e794c0637a5911d8cde3165c8f243fe86f2da846 Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Mon, 23 Oct 2017 19:52:37 -0400 Subject: [PATCH 2/4] Documented the new calendar methods. --- Doc/library/calendar.rst | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/Doc/library/calendar.rst b/Doc/library/calendar.rst index a415b4792a6bd6b..8fe93fd4de3fa48 100644 --- a/Doc/library/calendar.rst +++ b/Doc/library/calendar.rst @@ -53,17 +53,40 @@ it's the base calendar for all computations. month that are required to get a complete week. + .. method:: itermonthdays(year, month) + + Return an iterator for the month *month* in the year *year* similar to + :meth:`itermonthdates`, but not restricted by the :class:`datetime.date` + range. Days returned will simply be day of the month numbers. For the + days outside of the specified month, the day number is ``0``. + + .. method:: itermonthdays2(year, month) Return an iterator for the month *month* in the year *year* similar to - :meth:`itermonthdates`. Days returned will be tuples consisting of a day + :meth:`itermonthdates`, but not restricted by the :class:`datetime.date` + range. Days returned will be tuples consisting of a day of the month number and a week day number. - .. method:: itermonthdays(year, month) + .. method:: itermonthdays3(year, month) + + Return an iterator for the month *month* in the year *year* similar to + :meth:`itermonthdates`, but not restricted by the :class:`datetime.date` + range. Days returned will be tuples consisting of a year, a month and a day + of the month numbers. + + .. versionadded:: 3.7 + + + .. method:: itermonthdays4(year, month) Return an iterator for the month *month* in the year *year* similar to - :meth:`itermonthdates`. Days returned will simply be day numbers. + :meth:`itermonthdates`, but not restricted by the :class:`datetime.date` + range. Days returned will be tuples consisting of a year, a month, a day + of the month, and a day of the week numbers. + + .. versionadded:: 3.7 .. method:: monthdatescalendar(year, month) From 3862fec4110f238fffc9c5206590489b93e71858 Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Mon, 23 Oct 2017 19:54:03 -0400 Subject: [PATCH 3/4] Changed the order of Calendar method definitions. --- Lib/calendar.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Lib/calendar.py b/Lib/calendar.py index 73715fb05bb6fed..fb594e0f5b0e1b4 100644 --- a/Lib/calendar.py +++ b/Lib/calendar.py @@ -178,6 +178,18 @@ def itermonthdates(self, year, month): for y, m, d in self.itermonthdays3(year, month): yield datetime.date(y, m, d) + def itermonthdays(self, year, month): + """ + Like itermonthdates(), but will yield day numbers. For days outside + the specified month the day number is 0. + """ + day1, ndays = monthrange(year, month) + days_before = (day1 - self.firstweekday) % 7 + yield from repeat(0, days_before) + yield from range(1, ndays + 1) + days_after = (self.firstweekday - day1 - ndays) % 7 + yield from repeat(0, days_after) + def itermonthdays2(self, year, month): """ Like itermonthdates(), but will yield (day number, weekday number) @@ -212,18 +224,6 @@ def itermonthdays4(self, year, month): for i, (y, m, d) in enumerate(self.itermonthdays3(year, month)): yield y, m, d, (self.firstweekday + i) % 7 - def itermonthdays(self, year, month): - """ - Like itermonthdates(), but will yield day numbers. For days outside - the specified month the day number is 0. - """ - day1, ndays = monthrange(year, month) - days_before = (day1 - self.firstweekday) % 7 - yield from repeat(0, days_before) - yield from range(1, ndays + 1) - days_after = (self.firstweekday - day1 - ndays) % 7 - yield from repeat(0, days_after) - def monthdatescalendar(self, year, month): """ Return a matrix (list of lists) representing a month's calendar. From b18ed1aacefd505f6e503ccb0d528e551cf06d02 Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Mon, 23 Oct 2017 20:03:53 -0400 Subject: [PATCH 4/4] Added a NEWS entry. --- .../next/Library/2017-10-23-20-03-36.bpo-28292.1Gkim2.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2017-10-23-20-03-36.bpo-28292.1Gkim2.rst diff --git a/Misc/NEWS.d/next/Library/2017-10-23-20-03-36.bpo-28292.1Gkim2.rst b/Misc/NEWS.d/next/Library/2017-10-23-20-03-36.bpo-28292.1Gkim2.rst new file mode 100644 index 000000000000000..e0eb53ea51c2974 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-10-23-20-03-36.bpo-28292.1Gkim2.rst @@ -0,0 +1,5 @@ +Calendar.itermonthdates() will now consistently raise an exception when a +date falls outside of the 0001-01-01 through 9999-12-31 range. To support +applications that cannot tolerate such exceptions, the new methods +itermonthdays3() and itermonthdays4() are added. The new methods return +tuples and are not restricted by the range supported by datetime.date.