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.