From fdd9b217c60b454ac6a82f02c8b0b551caeac88b Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Tue, 24 Oct 2017 13:17:10 -0400 Subject: [PATCH] Closes bpo-28292: Implemented Calendar.itermonthdays3() and itermonthdays4(). (#4079) 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. Thanks @serhiy-storchaka for suggesting the itermonthdays4() method and for the review. --- Doc/library/calendar.rst | 39 ++++++++-- Lib/calendar.py | 76 +++++++++++++------ Lib/test/test_calendar.py | 16 ++-- .../2017-10-23-20-03-36.bpo-28292.1Gkim2.rst | 5 ++ 4 files changed, 101 insertions(+), 35 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2017-10-23-20-03-36.bpo-28292.1Gkim2.rst diff --git a/Doc/library/calendar.rst b/Doc/library/calendar.rst index a415b4792a6..8fe93fd4de3 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:: 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 - number and a week day number. - - .. method:: itermonthdays(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 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`, 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:: 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`, 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) diff --git a/Lib/calendar.py b/Lib/calendar.py index 0218e2d3977..fb594e0f5b0 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,28 +175,8 @@ class Calendar(object): 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 - - def itermonthdays2(self, year, month): - """ - Like itermonthdates(), but will yield (day number, weekday number) - tuples. For days outside the specified month the day number is 0. - """ - for i, d in enumerate(self.itermonthdays(year, month), self.firstweekday): - yield d, i % 7 + for y, m, d in self.itermonthdays3(year, month): + yield datetime.date(y, m, d) def itermonthdays(self, year, month): """ @@ -192,6 +190,40 @@ class Calendar(object): 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) + tuples. For days outside the specified month the day number is 0. + """ + 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 monthdatescalendar(self, year, month): """ Return a matrix (list of lists) representing a month's calendar. diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index c777f648356..ad8b6bb6b93 100644 --- a/Lib/test/test_calendar.py +++ b/Lib/test/test_calendar.py @@ -502,10 +502,15 @@ class CalendarTestCase(unittest.TestCase): 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 @@ class MiscTestCase(unittest.TestCase): 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) 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 00000000000..e0eb53ea51c --- /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.