diff -r b98ed69cb04c Doc/c-api/datetime.rst --- Doc/c-api/datetime.rst Wed Apr 22 19:50:21 2009 +0200 +++ Doc/c-api/datetime.rst Wed Apr 22 22:57:39 2009 -0500 @@ -62,6 +62,18 @@ *NULL*. +.. cfunction:: int PyMonth_Check(PyObject *ob) + + Return true if *ob* is of type :cdata:`PyDateTime_MonthType` or a subtype + of :cdata:`PyDateTime_MonthType`. *ob* must not be *NULL*. + + +.. cfunction:: int PyMonth_CheckExact(PyObject *ob) + + Return true if *ob* is of type :cdata:`PyDateTime_MonthType`. *ob* must not + be *NULL*. + + .. cfunction:: int PyTZInfo_Check(PyObject *ob) Return true if *ob* is of type :cdata:`PyDateTime_TZInfoType` or a subtype of @@ -100,6 +112,10 @@ number of microseconds and seconds lie in the ranges documented for ``datetime.timedelta`` objects. +.. cfunction:: PyObject* PyMonth_FromMonths(int months) + + Return a :class:`datetime.monthdelta` object representing the given number + of months. Macros to extract fields from date objects. The argument must be an instance of :cdata:`PyDateTime_Date`, including subclasses (such as @@ -181,3 +197,18 @@ Create and return a new ``datetime.date`` object given an argument tuple suitable for passing to ``datetime.date.fromtimestamp()``. + +Macro for module-level function: + +.. cfunction:: int PyMonth_Mod(PyObject *start, PyObject *end, PyObject **md, PyObject **td) + + Create (and return by reference) a :cdata:`PyDateTime_MonthType` and a + :cdata:`PyDateTime_DeltaType` from two :cdata:`PyDateTime_DateTypes` or + :cdata:`PyDateTime_DatetimeTypes`. After :cfunc:`PyMonth_Mod` returns zero, + ``md`` and ``td`` will be addresses of, respectively, a + :class:`~datetime.monthdelta` object and a :class:`~datetime.timedelta` + object, which together represent the interim between ``start`` and ``end``. + In case of error this function will return -1 and will set an appropriate + exception. See documentation for :func:`~datetime.monthmod` for more + information. + diff -r b98ed69cb04c Doc/library/datetime.rst --- Doc/library/datetime.rst Wed Apr 22 19:50:21 2009 +0200 +++ Doc/library/datetime.rst Wed Apr 22 22:57:39 2009 -0500 @@ -6,6 +6,7 @@ .. moduleauthor:: Tim Peters .. sectionauthor:: Tim Peters .. sectionauthor:: A.M. Kuchling +.. sectionauthor:: Jess Austin .. XXX what order should the types be discussed in? @@ -90,6 +91,12 @@ or :class:`datetime` instances to microsecond resolution. +.. class:: monthdelta + :noindex: + + A quantity of months offset from a :class:`date` or :class:`datetime`. + + .. class:: tzinfo An abstract base class for time zone information objects. These are used by the @@ -107,12 +114,13 @@ ``None`` but ``d.tzinfo.utcoffset(d)`` returns ``None``, *d* is naive. The distinction between naive and aware doesn't apply to :class:`timedelta` -objects. +or :class:`monthdelta` objects. Subclass relationships:: object timedelta + monthdelta tzinfo time date @@ -284,6 +292,145 @@ True +.. _datetime-monthdelta: + +:class:`monthdelta` Objects +--------------------------- + +.. class:: monthdelta([months=1]) + + The class constructor takes one optional integer argument, *months*. + + :param months: between -99999999 and 99999999 + :type months: :class:`integer ` + +A :class:`monthdelta` object represents a quantity of months offset from a +:class:`date` or :class:`datetime`. :class:`monthdelta` allows date +calculations without regard to the different lengths of different months. A +:class:`monthdelta` object added to a :class:`date` object produces another +:class:`date` that has the same :attr:`~date.day`, with :attr:`~date.year` +and :attr:`~date.month` offset by :attr:`monthdelta.months`. If the resulting +:attr:`~date.day` would be too large for the resulting :attr:`~date.month`, +the last day in that month is used instead: + + >>> date(2008, 1, 30) + monthdelta(1) + datetime.date(2008, 2, 29) + >>> date(2008, 1, 30) + monthdelta(2) + datetime.date(2008, 3, 30) + +Adding a :class:`monthdelta` object to a :class:`date` or :class:`datetime` +object differs from adding a :class:`timedelta` object in that a +:class:`timedelta` object represents a fixed number of +:attr:`~timedelta.days`, while the number of days that a :class:`monthdelta` +object represents depends on the actual months that it spans when added to the +:class:`date` or :class:`datetime` object. + +:class:`monthdelta` objects may be added, subtracted, multiplied, and +floor-divided similarly to :class:`timedelta` objects. They may not be added +to :class:`timedelta` objects directly, as both classes are intended to be +used directly with :class:`date` and :class:`datetime` objects. + + +Class attributes: + + +.. attribute:: monthdelta.min + + The most negative :class:`monthdelta` object, ``monthdelta(-99999999)``. + +.. attribute:: monthdelta.max + + The most positive :class:`monthdelta` object, ``monthdelta(99999999)``. + + +Instance attribute: + +.. attribute:: monthdelta.months + + Between -99999999 and 99999999 inclusive, read-only. + +Supported operations: + ++-------------------------+-------------------------------------------------+ +| Operation | Result | ++=========================+=================================================+ +| ``m1 = m2 + m3`` | Sum of *m2* and *m3*. Afterwards ``m1-m2 == m3``| +| | and ``m1-m3 == m2`` are true. (1) | ++-------------------------+-------------------------------------------------+ +| ``m1 = m2 - m3`` | Difference of *m2* and *m3*. Afterwards ``m1 == | +| | m2 - m3`` and ``m2 == m1 + m3`` are true. (1) | ++-------------------------+-------------------------------------------------+ +| ``m1 = m2 * i`` or | :class:`monthdelta` multiplied by an | +| ``m1 = i * m2`` | :class:`integer `. Afterwards | +| | ``m1 // i == m2`` is true, provided ``i != 0``. | +| | Also, ``m1 // m2 == i`` is true, provided | +| | ``m2.months != 0``. (1) | ++-------------------------+-------------------------------------------------+ +| ``m1 = m2 // i`` | The floor is computed and the remainder (if any)| +| | is thrown away. Division of a | +| | :class:`monthdelta` by an :class:`integer `| +| | produces a :class:`monthdelta`. (3) | ++-------------------------+-------------------------------------------------+ +| ``i = m2 // m3`` | The floor is computed and the remainder (if any)| +| | is thrown away. Division of a | +| | :class:`monthdelta` by a :class:`monthdelta` | +| | produces an :class:`integer `. (3) | ++-------------------------+-------------------------------------------------+ +| ``+m1`` | Returns a :class:`monthdelta` object with the | +| | same value. (2) | ++-------------------------+-------------------------------------------------+ +| ``-m1`` | equivalent to ``monthdelta(-m1.months)``, and | +| | to ``m1 * -1``. (2) | ++-------------------------+-------------------------------------------------+ +| ``abs(m1)`` | equivalent to ``+m1`` when ``m1.months >= 0``, | +| | and to ``-m1`` when ``m1.months < 0``. (2) | ++-------------------------+-------------------------------------------------+ + +Notes: + +(1) + This is exact, but may overflow. + +(2) + This is exact, and cannot overflow. + +(3) + Division by 0 raises :exc:`ZeroDivisionError`. + +In addition to the operations listed above :class:`monthdelta` objects support +certain additions and subtractions with :class:`date` and :class:`datetime` +objects (:ref:`see below `): + +Comparisons of :class:`monthdelta` objects are supported; the object with the +smaller :attr:`~monthdelta.months` attribute is considered the smaller +:class:`monthdelta`. In order to stop mixed-type comparisons from falling +back to the default comparison by object address, when a :class:`monthdelta` +object is compared to an object of a different type, :exc:`TypeError` is +raised unless the comparison is ``==`` or ``!=``. The latter cases return +:const:`False` or :const:`True`, respectively. + +:class:`monthdelta` objects are :term:`hashable` and support efficient +pickling. In Boolean contexts, a :class:`monthdelta` object is considered to +be :const:`True` if and only if it isn't equal to ``monthdelta(0)``. + +Example usage: + + >>> from datetime import date, monthdelta + >>> date(2008, 1, 1) + monthdelta(1) + datetime.date(2008, 2, 1) + >>> date(2008, 1, 30) + monthdelta(1) + datetime.date(2008, 2, 29) + >>> date(2008, 1, 31) + monthdelta(1) + datetime.date(2008, 2, 29) + >>> date(2008, 1, 31) + monthdelta(6) + datetime.date(2008, 7, 31) + >>> year = monthdelta(12) + >>> date(2008, 2, 29) + year + datetime.date(2009, 2, 28) + >>> date(2008, 2, 29) + 4*year + datetime.date(2012, 2, 29) + + .. _datetime-date: :class:`date` Objects @@ -383,6 +530,16 @@ | | timedelta == date1``. (2) | +-------------------------------+----------------------------------------------+ | ``timedelta = date1 - date2`` | \(3) | ++-------------------------------+----------------------------------------------+ +| ``date2 = date1 + | *date2* has the same :attr:`~date.day` | +| monthdelta`` | attribute as *date1*, | +| | :attr:`monthdelta.months` months later than | +| | *date1*. (5) | ++-------------------------------+----------------------------------------------+ +| ``date2 = date1 - | *date2* has the same :attr:`~date.day` | +| monthdelta`` | attribute as *date1*, | +| | :attr:`monthdelta.months` months earlier | +| | than *date1*. (5) | +-------------------------------+----------------------------------------------+ | ``date1 < date2`` | *date1* is considered less than *date2* when | | | *date1* precedes *date2* in time. (4) | @@ -417,6 +574,27 @@ object is compared to an object of a different type, :exc:`TypeError` is raised unless the comparison is ``==`` or ``!=``. The latter cases return :const:`False` or :const:`True`, respectively. + +.. _date-operations-note5: + +(5) + When the resulting :class:`date` would have too large a :attr:`~date.day` + for its :attr:`~date.month`, it has the last day of that month: + + >>> date(2008,1,30) + monthdelta(1) + date(2008,2,29) + + :class:`monthdelta` calculations involving the 29th, 30th, and 31st days + of the month are not necessarily invertible: + + >>> date(2008,2,29) - monthdelta(1) + date(2008,1,29) + + Adding or subtracting a :class:`date` object and a :class:`monthdelta` + object produces another :class:`date` object. Use the :func:`monthmod` + function in order to produce a :class:`monthdelta` object from two + :class:`date` objects. + Dates can be used as dictionary keys. In Boolean contexts, all :class:`date` objects are considered to be true. @@ -555,6 +733,37 @@ >>> d.strftime("%A %d. %B %Y") 'Monday 11. March 2002' +Example of working with :class:`date` and :class:`monthdelta`. We have a +dictionary of accounts associated with sorted lists of their invoice dates, +and we're looking for missing invoices: + + >>> from datetime import date, monthdelta + >>> invoices = {123: [date(2008, 1, 31), + ... date(2008, 2, 29), + ... date(2008, 3, 31), + ... date(2008, 4, 30), + ... date(2008, 5, 31), + ... date(2008, 6, 30), + ... date(2008, 7, 31), + ... date(2008, 12, 31)], + ... 456: [date(2008, 1, 1), + ... date(2008, 5, 1), + ... date(2008, 6, 1), + ... date(2008, 7, 1), + ... date(2008, 8, 1), + ... date(2008, 11, 1), + ... date(2008, 12, 1)]} + >>> for account, dates in invoices.items(): + ... a = dates[0] + ... for b in dates[1:]: + ... if b - monthdelta(1) > a: + ... print('account', account, 'missing between', a, 'and', b) + ... a = b + ... + account 456 missing between 2008-01-01 and 2008-05-01 + account 456 missing between 2008-08-01 and 2008-11-01 + account 123 missing between 2008-07-31 and 2008-12-31 + .. _datetime-datetime: @@ -746,6 +955,24 @@ | ``datetime2 = datetime1 - timedelta`` | \(2) | +---------------------------------------+-------------------------------+ | ``timedelta = datetime1 - datetime2`` | \(3) | ++---------------------------------------+-------------------------------+ +| ``datetime2 = datetime1 + | *datetime2* has all attributes| +| monthdelta`` | other than | +| | :attr:`~datetime.year` and | +| | :attr:`~datetime.month` equal | +| | to those of *datetime1*, | +| | :attr:`monthdelta.months` | +| | months later than *datetime1*.| +| | (5) | ++---------------------------------------+-------------------------------+ +| ``datetime2 = datetime1 - | *datetime2* has all attributes| +| monthdelta`` | other than | +| | :attr:`~datetime.year` and | +| | :attr:`~datetime.month` equal | +| | to those of *datetime1*, | +| | :attr:`monthdelta.months` | +| | months earlier than | +| | *datetime1*. (5) | +---------------------------------------+-------------------------------+ | ``datetime1 < datetime2`` | Compares :class:`datetime` to | | | :class:`datetime`. (4) | @@ -803,6 +1030,23 @@ object is compared to an object of a different type, :exc:`TypeError` is raised unless the comparison is ``==`` or ``!=``. The latter cases return :const:`False` or :const:`True`, respectively. + +(5) + As for :class:`date`, addition of a :class:`monthdelta` will produce a + :class:`datetime` with a different :attr:`~datetime.day` attribute in + certain situations near the end of the month. When this happens, the + :attr:`~datetime.hour`, :attr:`~datetime.minute`, :attr:`~datetime.second`, + :attr:`~datetime.microsecond`, and :attr:`~datetime.tzinfo` attributes are + not changed: + + >>> from datetime import datetime, monthdelta + >>> datetime(2008, 1, 30, 12, 30, 13) + monthdelta(1) + datetime.datetime(2008, 2, 29, 12, 30, 13) + + Adding or subtracting a :class:`datetime` object and a :class:`monthdelta` + object produces another :class:`datetime`. Use the :func:`monthmod` + function in order to produce a :class:`monthdelta` object from two + :class:`datetime` objects. :class:`datetime` objects can be used as dictionary keys. In Boolean contexts, all :class:`datetime` objects are considered to be true. @@ -1635,3 +1879,45 @@ (5) For example, if :meth:`utcoffset` returns ``timedelta(hours=-3, minutes=-30)``, ``%z`` is replaced with the string ``'-0330'``. + + +.. _datetime-module-function: + +Module Function +--------------- + +.. function:: monthmod(start, end) + + Return the interim between ``start`` and ``end``, distributed into a + "months" portion and a remainder. + + :param start: :class:`date` + :param end: :class:`date` + :rtype: (:class:`monthdelta`, :class:`timedelta`) tuple + + ``start`` and ``end`` must support mutual subtraction. For this reason, + passing a :class:`date` object and a :class:`datetime` object together will + raise a :exc:`TypeError`. Subclasses that override :func:`__sub__` could + work, however. + + If and only if ``start`` is greater than ``end``, returned + :class:`monthdelta` is negative. Returned :class:`timedelta` is never + negative, and its :attr:`~timedelta.days` attribute is always less than the + number of days in ``end.month``. + + **Invariant:** ``dt + monthmod(dt, dt+td)[0] + monthmod(dt, dt+td)[1] + == dt + td`` is :const:`True`. + + :func:`monthmod` allows round-trip :class:`date` calculations involving + :class:`monthdelta` and :class:`timedelta` objects: + + >>> from datetime import date, monthmod + >>> monthmod(date(2008, 1, 14), date(2009, 4, 2)) + (datetime.monthdelta(14), datetime.timedelta(19)) + >>> date(2008, 1, 14) + _[0] + _[1] + datetime.date(2009, 4, 2) + >>> monthmod(date(2009, 4, 2), date(2008, 1, 14)) + (datetime.monthdelta(-15), datetime.timedelta(12)) + >>> date(2009, 4, 2) + _[0] + _[1] + datetime.date(2008, 1, 14) + diff -r b98ed69cb04c Include/datetime.h --- Include/datetime.h Wed Apr 22 19:50:21 2009 +0200 +++ Include/datetime.h Wed Apr 22 22:57:39 2009 -0500 @@ -39,6 +39,13 @@ int seconds; /* 0 <= seconds < 24*3600 is invariant */ int microseconds; /* 0 <= microseconds < 1000000 is invariant */ } PyDateTime_Delta; + +typedef struct +{ + PyObject_HEAD + long hashcode; /* -1 when unknown */ + int months; /* -MAX_DELTA_MONTHS <= months <= MAX_DELTA_MONTHS */ +} PyDateTime_Month; typedef struct { @@ -143,6 +150,7 @@ PyTypeObject *DateTimeType; PyTypeObject *TimeType; PyTypeObject *DeltaType; + PyTypeObject *MonthType; PyTypeObject *TZInfoType; /* constructors */ @@ -151,10 +159,14 @@ PyObject*, PyTypeObject*); PyObject *(*Time_FromTime)(int, int, int, int, PyObject*, PyTypeObject*); PyObject *(*Delta_FromDelta)(int, int, int, int, PyTypeObject*); + PyObject *(*Month_FromMonth)(int, PyTypeObject*); /* constructors for the DB API */ PyObject *(*DateTime_FromTimestamp)(PyObject*, PyObject*, PyObject*); PyObject *(*Date_FromTimestamp)(PyObject*, PyObject*); + + /* module-level function */ + int (*Month_Mod)(PyObject*, PyObject*, PyObject**, PyObject**); } PyDateTime_CAPI; @@ -176,6 +188,9 @@ #define PyDelta_Check(op) PyObject_TypeCheck(op, &PyDateTime_DeltaType) #define PyDelta_CheckExact(op) (Py_TYPE(op) == &PyDateTime_DeltaType) + +#define PyMonth_Check(op) PyObject_TypeCheck(op, &PyDateTime_MonthType) +#define PyMonth_CheckExact(op) (Py_TYPE(op) == &PyDateTime_MonthType) #define PyTZInfo_Check(op) PyObject_TypeCheck(op, &PyDateTime_TZInfoType) #define PyTZInfo_CheckExact(op) (Py_TYPE(op) == &PyDateTime_TZInfoType) @@ -209,6 +224,9 @@ #define PyDelta_Check(op) PyObject_TypeCheck(op, PyDateTimeAPI->DeltaType) #define PyDelta_CheckExact(op) (Py_TYPE(op) == PyDateTimeAPI->DeltaType) +#define PyMonth_Check(op) PyObject_TypeCheck(op, PyDateTimeAPI->MonthType) +#define PyMonth_CheckExact(op) (Py_TYPE(op) == PyDateTimeAPI->MonthType) + #define PyTZInfo_Check(op) PyObject_TypeCheck(op, PyDateTimeAPI->TZInfoType) #define PyTZInfo_CheckExact(op) (Py_TYPE(op) == PyDateTimeAPI->TZInfoType) @@ -228,6 +246,9 @@ PyDateTimeAPI->Delta_FromDelta(days, seconds, useconds, 1, \ PyDateTimeAPI->DeltaType) +#define PyMonth_FromMonths(months) \ + PyDateTimeAPI->Month_FromMonth(months, PyDateTimeAPI->MonthType) + /* Macros supporting the DB API. */ #define PyDateTime_FromTimestamp(args) \ PyDateTimeAPI->DateTime_FromTimestamp( \ @@ -237,6 +258,10 @@ PyDateTimeAPI->Date_FromTimestamp( \ (PyObject*) (PyDateTimeAPI->DateType), args) +/* Macro for module-level function. */ +#define PyMonth_Mod(start, end, md, td) \ + PyDateTimeAPI->Month_Mod(start, end, md, td) + #endif /* Py_BUILD_CORE */ #ifdef __cplusplus diff -r b98ed69cb04c Lib/test/test_datetime.py --- Lib/test/test_datetime.py Wed Apr 22 19:50:21 2009 +0200 +++ Lib/test/test_datetime.py Wed Apr 22 22:57:39 2009 -0500 @@ -10,12 +10,14 @@ from operator import lt, le, gt, ge, eq, ne from test import support +from itertools import combinations, permutations from datetime import MINYEAR, MAXYEAR from datetime import timedelta from datetime import tzinfo from datetime import time from datetime import date, datetime +from datetime import monthdelta, monthmod pickle_choices = [(pickle, pickle, proto) for proto in range(3)] assert len(pickle_choices) == 3 @@ -3295,6 +3297,220 @@ start += HOUR fstart += HOUR +class TestMonthDelta(unittest.TestCase): + expectations = ( + (date(2006,12,31), monthdelta(6), date(2007,6,30),date(2006,6,30)), + (date(2007,1,1), monthdelta(6), date(2007,7,1), date(2006,7,1)), + (date(2007,1,2), monthdelta(6), date(2007,7,2), date(2006,7,2)), + (date(2006,12,31), monthdelta(12),date(2007,12,31),date(2005,12,31)), + (date(2007,1,1), monthdelta(12), date(2008,1,1),date(2006,1,1)), + (date(2007,1,2), monthdelta(12), date(2008,1,2),date(2006,1,2)), + (date(2006,12,31), monthdelta(60),date(2011,12,31),date(2001,12,31)), + (date(2007,1,1), monthdelta(60), date(2012,1,1),date(2002,1,1)), + (date(2007,1,2), monthdelta(60), date(2012,1,2),date(2002,1,2)), + (date(2006,12,31),monthdelta(600),date(2056,12,31),date(1956,12,31)), + (date(2007,1,1), monthdelta(600), date(2057,1,1),date(1957,1,1)), + (date(2007,1,2), monthdelta(600), date(2057,1,2),date(1957,1,2)), + (date(2007,2,27), monthdelta(1), date(2007, 3, 27),date(2007,1, 27)), + (date(2007,2,28), monthdelta(1), date(2007, 3, 28),date(2007,1, 28)), + (date(2007,3,1), monthdelta(1), date(2007, 4, 1), date(2007, 2, 1)), + (date(2007,3,30), monthdelta(1), date(2007, 4, 30),date(2007,2, 28)), + (date(2007,3,31), monthdelta(1), date(2007, 4, 30),date(2007,2, 28)), + (date(2007,4,1), monthdelta(1), date(2007, 5, 1), date(2007, 3, 1)), + (date(2008,2,27), monthdelta(1), date(2008, 3, 27),date(2008,1, 27)), + (date(2008,2,28), monthdelta(1), date(2008, 3, 28),date(2008,1, 28)), + (date(2008,2,29), monthdelta(1), date(2008, 3, 29),date(2008,1, 29)), + (date(2008,3,1), monthdelta(1), date(2008, 4, 1), date(2008, 2, 1)), + (date(2008,3,30), monthdelta(1), date(2008, 4, 30),date(2008,2, 29)), + (date(2008,3,31), monthdelta(1), date(2008, 4, 30),date(2008,2, 29)), + (date(2008,4,1), monthdelta(1), date(2008, 5, 1), date(2008, 3, 1)), + (date(2100,2,27), monthdelta(1), date(2100, 3, 27),date(2100,1, 27)), + (date(2100,2,28), monthdelta(1), date(2100, 3, 28),date(2100,1, 28)), + (date(2100,3,1), monthdelta(1), date(2100, 4, 1), date(2100, 2, 1)), + (date(2100,3,30), monthdelta(1), date(2100, 4, 30),date(2100,2, 28)), + (date(2100,3,31), monthdelta(1), date(2100, 4, 30),date(2100,2, 28)), + (date(2100,4,1), monthdelta(1), date(2100, 5, 1), date(2100, 3, 1)), + (date(2000,2,27), monthdelta(1), date(2000, 3, 27),date(2000,1, 27)), + (date(2000,2,28), monthdelta(1), date(2000, 3, 28),date(2000,1, 28)), + (date(2000,2,29), monthdelta(1), date(2000, 3, 29),date(2000,1, 29)), + (date(2000,3,1), monthdelta(1), date(2000, 4, 1), date(2000, 2, 1)), + (date(2000,3,30), monthdelta(1), date(2000, 4, 30),date(2000,2, 29)), + (date(2000,3,31), monthdelta(1), date(2000, 4, 30),date(2000,2, 29)), + (date(2000,4,1), monthdelta(1), date(2000, 5, 1), date(2000, 3, 1))) + + def test_calc(self): + for dt, md, sub, prev in self.expectations: + self.assertEqual(dt + md, sub) + self.assertEqual(dt - md, prev) + def test_math(self): + for x, y in permutations(range(26),2): + self.assertEqual(monthdelta(x) + monthdelta(y), monthdelta(x +y)) + self.assertEqual(monthdelta(x) - monthdelta(y), monthdelta(x -y)) + self.assertEqual(monthdelta(x) * y, monthdelta(x * y)) + for x, y in combinations(range(26),2): + self.assertEqual(monthdelta(x) // y, monthdelta(x // y)) + self.assertEqual(monthdelta(x) // monthdelta(y), x // y) + def test_comp(self): + for x, y in combinations(range(26),2): + self.assert_(monthdelta(x) < monthdelta(y)) + self.assert_(monthdelta(x) <= monthdelta(y)) + self.assert_(monthdelta(x) != monthdelta(y)) + self.assert_(monthdelta(y) > monthdelta(x)) + self.assert_(monthdelta(y) >= monthdelta(x)) + for x in range(26): + self.assert_(monthdelta(x) <= monthdelta(x)) + self.assert_(monthdelta(x) == monthdelta(x)) + self.assert_(monthdelta(x) >= monthdelta(x)) + def test_bool(self): + self.assert_(monthdelta()) + self.assert_(monthdelta(-1)) + self.assertFalse(monthdelta(0)) + def test_class(self): + self.assertEqual(monthdelta.min, monthdelta(-99999999)) + self.assertEqual(monthdelta.max, monthdelta(99999999)) + def test_subclass(self): + class M(monthdelta): + @staticmethod + def from_md(md): + return M(md.months) + def as_years(self): + return round(self.months / 12) + + m1 = M() + self.assert_(type(m1) is M) + self.assertEqual(m1.as_years(), 0) + m2 = M(-24) + self.assert_(type(m2) is M) + self.assertEqual(m2.as_years(), -2) + m3 = m1 + m2 + self.assert_(type(m3) is monthdelta) + m4 = M.from_md(m3) + self.assert_(type(m4) is M) + self.assertEqual(m3.months, m4.months) + self.assertEqual(str(m3), str(m4)) + self.assertEqual(m4.as_years(), -2) + def test_str(self): + self.assertEqual(str(monthdelta()), '1 month') + self.assertEqual(str(monthdelta(-1)), '-1 month') + self.assertEqual(str(monthdelta(3)), '3 months') + self.assertEqual(str(monthdelta(-17)), '-17 months') + def test_pickling(self): + orig = monthdelta(42) + green = pickle.dumps(orig) + derived = pickle.loads(green) + self.assertEqual(orig, derived) + def test_disallowed(self): + a = monthdelta(42) + for i in 1, 1.0: + self.assertRaises(TypeError, lambda: a+i) + self.assertRaises(TypeError, lambda: a-i) + self.assertRaises(TypeError, lambda: i+a) + self.assertRaises(TypeError, lambda: i-a) + self.assertRaises(TypeError, lambda: a/i) + self.assertRaises(TypeError, lambda: i/a) + self.assertRaises(TypeError, lambda: a/a) + self.assertRaises(ZeroDivisionError, lambda: a // 0) + def inplace_fail(): + b = monthdelta(12) + b //= monthdelta(3) + self.assertRaises(TypeError, inplace_fail) + x = 2.3 + self.assertRaises(TypeError, lambda: a*x) + self.assertRaises(TypeError, lambda: x*a) + self.assertRaises(TypeError, lambda: a // x) + self.assertRaises(TypeError, lambda: x // a) + self.assertRaises(OverflowError, monthdelta, -100000000) + self.assertRaises(OverflowError, monthdelta, 100000000) + +class TestMonthMod(unittest.TestCase): + md_zero, td_zero = monthdelta(0), timedelta(0) + expectations = ( + (date(2007,1,1), date(2007,1,1), md_zero, td_zero), + (date(2007,2,28), date(2007,2,28), md_zero, td_zero), + (date(2007,3,1), date(2007,3,1), md_zero, td_zero), + (date(2008,2,28), date(2008,2,28), md_zero, td_zero), + (date(2008,2,29), date(2008,2,29), md_zero, td_zero), + (date(2008,3,1), date(2008,3,1), md_zero, td_zero), + (date(2007,1,1), date(2007,2,27), monthdelta(1), timedelta(26)), + (date(2007,1,1), date(2007,2,28), monthdelta(1), timedelta(27)), + (date(2007,1,1), date(2007,3,1), monthdelta(2), timedelta(0)), + (date(2007,1,1), date(2007,3,30), monthdelta(2), timedelta(29)), + (date(2007,1,1), date(2007,3,31), monthdelta(2), timedelta(30)), + (date(2007,1,1), date(2007,4,1), monthdelta(3), timedelta(0)), + (date(2008,1,1), date(2008,2,27), monthdelta(1), timedelta(26)), + (date(2008,1,1), date(2008,2,28), monthdelta(1), timedelta(27)), + (date(2008,1,1), date(2008,2,29), monthdelta(1), timedelta(28)), + (date(2008,1,1), date(2008,3,1), monthdelta(2), timedelta(0)), + (date(2008,1,1), date(2008,3,30), monthdelta(2), timedelta(29)), + (date(2008,1,1), date(2008,3,31), monthdelta(2), timedelta(30)), + (date(2008,1,1), date(2008,4,1), monthdelta(3), timedelta(0)), + (date(2006,1,1), date(2007,2,27), monthdelta(13), timedelta(26)), + (date(2006,1,1), date(2007,2,28), monthdelta(13), timedelta(27)), + (date(2006,1,1), date(2007,3,1), monthdelta(14), timedelta(0)), + (date(2006,1,1), date(2007,3,30), monthdelta(14), timedelta(29)), + (date(2006,1,1), date(2007,3,31), monthdelta(14), timedelta(30)), + (date(2006,1,1), date(2007,4,1), monthdelta(15), timedelta(0)), + (date(2006,1,1), date(2008,2,27), monthdelta(25), timedelta(26)), + (date(2006,1,1), date(2008,2,28), monthdelta(25), timedelta(27)), + (date(2006,1,1), date(2008,2,29), monthdelta(25), timedelta(28)), + (date(2006,1,1), date(2008,3,1), monthdelta(26), timedelta(0)), + (date(2006,1,1), date(2008,3,30), monthdelta(26), timedelta(29)), + (date(2006,1,1), date(2008,3,31), monthdelta(26), timedelta(30)), + (date(2006,1,1), date(2008,4,1), monthdelta(27), timedelta(0)), + (date(2007,2,27), date(2007,1,1), monthdelta(-2), timedelta(5)), + (date(2007,2,28), date(2007,1,1), monthdelta(-2), timedelta(4)), + (date(2007,3,1), date(2007,1,1), monthdelta(-2), timedelta(0)), + (date(2007,3,30), date(2007,1,1), monthdelta(-3), timedelta(2)), + (date(2007,3,31), date(2007,1,1), monthdelta(-3), timedelta(1)), + (date(2007,4,1), date(2007,1,1), monthdelta(-3), timedelta(0)), + (date(2008,2,27), date(2008,1,1), monthdelta(-2), timedelta(5)), + (date(2008,2,28), date(2008,1,1), monthdelta(-2), timedelta(4)), + (date(2008,2,29), date(2008,1,1), monthdelta(-2), timedelta(3)), + (date(2008,3,1), date(2008,1,1), monthdelta(-2), timedelta(0)), + (date(2008,3,30), date(2008,1,1), monthdelta(-3), timedelta(2)), + (date(2008,3,31), date(2008,1,1), monthdelta(-3), timedelta(1)), + (date(2008,4,1), date(2008,1,1), monthdelta(-3), timedelta(0)), + (date(2007,2,27), date(2006,1,1), monthdelta(-14), timedelta(5)), + (date(2007,2,28), date(2006,1,1), monthdelta(-14), timedelta(4)), + (date(2007,3,1), date(2006,1,1), monthdelta(-14), timedelta(0)), + (date(2007,3,30), date(2006,1,1), monthdelta(-15), timedelta(2)), + (date(2007,3,31), date(2006,1,1), monthdelta(-15), timedelta(1)), + (date(2007,4,1), date(2006,1,1), monthdelta(-15), timedelta(0)), + (date(2008,2,27), date(2006,1,1), monthdelta(-26), timedelta(5)), + (date(2008,2,28), date(2006,1,1), monthdelta(-26), timedelta(4)), + (date(2008,2,29), date(2006,1,1), monthdelta(-26), timedelta(3)), + (date(2008,3,1), date(2006,1,1), monthdelta(-26), timedelta(0)), + (date(2008,3,30), date(2006,1,1), monthdelta(-27), timedelta(2)), + (date(2008,3,31), date(2006,1,1), monthdelta(-27), timedelta(1)), + (date(2008,4,1), date(2006,1,1), monthdelta(-27), timedelta(0)), + (date.min, date.max-timedelta(365), monthdelta(119975), timedelta(30))) + + def test_calc(self): + for start, end, md, td in self.expectations: + self.assertEqual(monthmod(start, end), (md, td)) + self.assert_((start > end and md < self.md_zero) or + (start <= end and md >= self.md_zero)) + self.assert_(td >= self.td_zero) + self.assert_(td < end.replace(end.year+end.month//12, + end.month%12+1, 1) - + end.replace(day=1)) + def test_invariant(self): + for start, end, md, td in self.expectations: + self.assertEqual(sum(monthmod(start, start + td), start), + start + td) + self.assertEqual(sum(monthmod(end, end + td), end), + end + td) + + def test_error_handling(self): + self.assertRaises(TypeError, monthmod, date.min) + self.assertRaises(TypeError, monthmod, 123, 'abc') + self.assertRaises(TypeError, monthmod, end=date.max) + self.assertRaises(TypeError, monthmod, date.min, datetime.max) + self.assertRaises(TypeError, monthmod, datetime.min, date.max) + # perhaps it would be better not to overflow for this, but we rely on + # the addition defined by the type of the arguments + self.assertRaises(OverflowError, monthmod, date.min+timedelta(1), + date.min) ############################################################################# # oddballs diff -r b98ed69cb04c Misc/NEWS --- Misc/NEWS Wed Apr 22 19:50:21 2009 +0200 +++ Misc/NEWS Wed Apr 22 22:57:39 2009 -0500 @@ -798,6 +798,9 @@ protocol numbers are supplied outside the allowed 0-65536 range on bind() and getservbyport(). +- Issue #5434: added monthdelta class and monthmod function to datetime + module. + Tools/Demos ----------- diff -r b98ed69cb04c Modules/datetimemodule.c --- Modules/datetimemodule.c Wed Apr 22 19:50:21 2009 +0200 +++ Modules/datetimemodule.c Wed Apr 22 22:57:39 2009 -0500 @@ -37,6 +37,10 @@ * carries from adding seconds. */ #define MAX_DELTA_DAYS 999999999 + +/* Just a bit smaller. + */ +#define MAX_DELTA_MONTHS 99999999 /* Rename the long macros in datetime.h to more reasonable short names. */ #define GET_YEAR PyDateTime_GET_YEAR @@ -84,6 +88,10 @@ #define SET_TD_SECONDS(o, v) ((o)->seconds = (v)) #define SET_TD_MICROSECONDS(o, v) ((o)->microseconds = (v)) +/* accessors for monthdelta. */ +#define GET_MD_MONTHS(o) (((PyDateTime_Month *)(o))->months) +#define SET_MD_MONTHS(o, v) ((o)->months = (v)) + /* p is a pointer to a time or a datetime object; HASTZINFO(p) returns * p->hastzinfo. */ @@ -101,6 +109,7 @@ static PyTypeObject PyDateTime_DeltaType; static PyTypeObject PyDateTime_TimeType; static PyTypeObject PyDateTime_TZInfoType; +static PyTypeObject PyDateTime_MonthType; /* --------------------------------------------------------------------------- * Math utilities. @@ -369,6 +378,20 @@ return -1; } +/* Check that -MAX_DELTA_MONTHS <= days <= MAX_DELTA_MONTHS. If so, return 0. + * If not, raise OverflowError and return -1. + */ +static int +check_monthdelta_range(int months) +{ + if (-MAX_DELTA_MONTHS <= months && months <= MAX_DELTA_MONTHS) + return 0; + PyErr_Format(PyExc_OverflowError, + "months=%d; must have magnitude <= %d", + months, MAX_DELTA_MONTHS); + return -1; +} + /* Check that date arguments are in range. Return 0 if they are. If they * aren't, raise ValueError and return -1. */ @@ -574,6 +597,34 @@ normalize_pair(hour, minute, 60); normalize_pair(day, hour, 24); return normalize_date(year, month, day); +} + +/* Force the month to make sense, and then if the day is too large, set it + * to the end of the month. + */ + +static int +normalize_month_end(int *year, int *month, int *day) +{ + int days, result; + + if (*month < 1 || *month > 12) { + --*month; + normalize_pair(year, month, 12); + ++*month; + } + assert(1 <= *month && *month <= 12); + days = days_in_month(*year, *month); + if (*day > days) + *day = days; + if (MINYEAR <= *year && *year <= MAXYEAR) + result = 0; + else { + PyErr_SetString(PyExc_OverflowError, + "date value out of range"); + result = -1; + } + return result; } /* --------------------------------------------------------------------------- @@ -748,6 +799,24 @@ #define new_delta(d, s, us, normalize) \ new_delta_ex(d, s, us, normalize, &PyDateTime_DeltaType) + +static PyObject * +new_month_ex(int months, PyTypeObject *type) +{ + PyDateTime_Month *self = NULL; + + if (! check_monthdelta_range(months)) { + self = (PyDateTime_Month *)(type->tp_alloc(type, 0)); + if (self != NULL) { + self->hashcode = -1; + SET_MD_MONTHS(self, months); + } + } + return (PyObject *) self; +} + +#define new_month(months) \ + new_month_ex(months, &PyDateTime_MonthType) /* --------------------------------------------------------------------------- * tzinfo helpers. @@ -2171,6 +2240,383 @@ 0, /* tp_free */ }; + +/* + * PyDateTime_Month implementation. Kind of a cut-n-paste from timedelta. + */ + +static PyObject * +multiply_int_monthdelta(PyObject *intobj, PyDateTime_Month *month) +{ + PyObject *in; + PyObject *out; + PyObject *result; + + in = PyLong_FromLong(GET_MD_MONTHS(month)); + if (in == NULL) + return NULL; + out = PyNumber_Multiply(in, intobj); + Py_DECREF(in); + if (out == NULL) + return NULL; + result = new_month(PyLong_AsLong(out)); + Py_DECREF(out); + return result; +} + +static PyObject * +divide_monthdelta_int(PyDateTime_Month *month, PyObject *intobj) +{ + PyObject *in; + PyObject *out; + PyObject *result; + + in = PyLong_FromLong(GET_MD_MONTHS(month)); + if (in == NULL) + return NULL; + out = PyNumber_FloorDivide(in, intobj); + Py_DECREF(in); + if (out == NULL) + return NULL; + result = new_month(PyLong_AsLong(out)); + Py_DECREF(out); + return result; +} + +static PyObject * +divide_monthdelta(PyDateTime_Month *left, PyDateTime_Month *right) +{ + PyObject *lft; + PyObject *rgt; + PyObject *result; + + lft = PyLong_FromLong(GET_MD_MONTHS(left)); + if (lft == NULL) + return NULL; + rgt = PyLong_FromLong(GET_MD_MONTHS(right)); + if (rgt == NULL) { + Py_DECREF(lft); + return NULL; + } + result = PyNumber_FloorDivide(lft, rgt); + Py_DECREF(lft); + Py_DECREF(rgt); + return result; +} + +static PyObject * +month_add(PyObject *left, PyObject *right) +{ + PyObject *result = Py_NotImplemented; + + if (PyMonth_Check(left) && PyMonth_Check(right)) { + /* monthdelta + monthdelta */ + /* The C-level additions can't overflow because of the + * invariant bounds. + */ + int months = GET_MD_MONTHS(left) + GET_MD_MONTHS(right); + result = new_month(months); + } + + if (result == Py_NotImplemented) + Py_INCREF(result); + return result; +} + +static PyObject * +month_negative(PyDateTime_Month *self) +{ + return new_month(-GET_MD_MONTHS(self)); +} + +static PyObject * +month_positive(PyDateTime_Month *self) +{ + return new_month(GET_MD_MONTHS(self)); +} + +static PyObject * +month_abs(PyDateTime_Month *self) +{ + PyObject *result; + + if (GET_MD_MONTHS(self) < 0) + result = month_negative(self); + else + result = month_positive(self); + + return result; +} + +static PyObject * +month_subtract(PyObject *left, PyObject *right) +{ + PyObject *result = Py_NotImplemented; + + if (PyMonth_Check(left) && PyMonth_Check(right)) { + /* monthdelta - monthdelta */ + PyObject *minus_right = PyNumber_Negative(right); + if (minus_right) { + result = month_add(left, minus_right); + Py_DECREF(minus_right); + } + else + result = NULL; + } + + if (result == Py_NotImplemented) + Py_INCREF(result); + return result; +} + +static PyObject * +month_richcompare(PyObject *self, PyObject *other, int op) +{ + if (PyMonth_Check(other)) { + int diff = GET_MD_MONTHS(self) - GET_MD_MONTHS(other); + return diff_to_bool(diff, op); + } + else { + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } +} + +static PyObject *month_getstate(PyDateTime_Month *self); + +static long +month_hash(PyDateTime_Month *self) +{ + if (self->hashcode == -1) { + PyObject *temp = month_getstate(self); + if (temp != NULL) { + self->hashcode = PyObject_Hash(temp); + Py_DECREF(temp); + } + } + return self->hashcode; +} + +static PyObject * +month_multiply(PyObject *left, PyObject *right) +{ + PyObject *result = Py_NotImplemented; + + if (PyMonth_Check(left)) { + if (PyLong_Check(right)) + result = multiply_int_monthdelta(right, + (PyDateTime_Month *)left); + } + else if (PyLong_Check(left)) + result = multiply_int_monthdelta(left, + (PyDateTime_Month *)right); + if (result == Py_NotImplemented) + Py_INCREF(result); + return result; +} + +static PyObject * +month_divide(PyObject *left, PyObject *right) +{ + PyObject *result = Py_NotImplemented; + + if (PyMonth_Check(left)) { + if (PyLong_Check(right)) + result = divide_monthdelta_int( + (PyDateTime_Month *)left, right); + else if (PyMonth_Check(right) && PyMonth_Check(left)) + result = divide_monthdelta((PyDateTime_Month *)left, + (PyDateTime_Month *)right); + } + if (result == Py_NotImplemented) + Py_INCREF(result); + return result; +} + +/* in-place division by a monthdelta (which will change the variable's type) + * is almost certainly a bug -- raising this error is the reason we don't + * just fall back on __floordiv__ + */ +static PyObject * +month_idivide(PyObject *left, PyObject *right) +{ + PyObject *result = Py_NotImplemented; + + if (PyMonth_Check(left)) { + if (PyLong_Check(right)) + result = divide_monthdelta_int( + (PyDateTime_Month *)left, right); + else if (PyMonth_Check(right) && PyMonth_Check(left)) { + result = NULL; + PyErr_SetString(PyExc_TypeError, + "in-place division of a monthdelta " + "requires an integer divisor"); + } + } + if (result == Py_NotImplemented) + Py_INCREF(result); + return result; +} + +static PyObject * +month_new(PyTypeObject *type, PyObject *args, PyObject *kw) +{ + int months = 1; + PyObject *self = NULL; + static char *keyword[] = {"months", NULL}; + + if (PyArg_ParseTupleAndKeywords(args, kw, "|i:monthdelta.__new__", + keyword, &months)) + self = new_month_ex(months, type); + return self; +} + +static int +month_bool(PyDateTime_Month *self) +{ + return (GET_MD_MONTHS(self) != 0); +} + +static PyObject * +month_repr(PyDateTime_Month *self) +{ + return PyUnicode_FromFormat("%s(%d)", Py_TYPE(self)->tp_name, + GET_MD_MONTHS(self)); +} + +static PyObject * +month_str(PyDateTime_Month *self) +{ + int months = GET_MD_MONTHS(self); + + return PyUnicode_FromFormat("%d month%s", months, + (months == 1 || months == -1) ? "" : "s"); +} + +/* Pickle support, a simple use of __reduce__. */ +/* __getstate__ isn't exposed */ +static PyObject * +month_getstate(PyDateTime_Month *self) +{ + return Py_BuildValue("(i)", GET_MD_MONTHS(self)); +} + +static PyObject * +month_reduce(PyDateTime_Month* self) +{ + return Py_BuildValue("ON", Py_TYPE(self), month_getstate(self)); +} + +static PyMemberDef month_members[] = { + {"months", T_INT, offsetof(PyDateTime_Month, months), READONLY, + PyDoc_STR("Number of months.")}, + {NULL} +}; + +static PyMethodDef month_methods[] = { + {"__reduce__", (PyCFunction)month_reduce, METH_NOARGS, + PyDoc_STR("__reduce__() -> (cls, state)")}, + {NULL, NULL}, +}; + +static char month_doc[] = +PyDoc_STR("Months offset from a date or datetime.\n\n" + "monthdeltas allow date calculation without regard to the different" + " lengths\nof different months. A monthdelta value added to a date " + "produces another\ndate that has the same day-of-the-month, " + "regardless of the lengths of the\nintervening months. If the " + "resulting date is in too short a month, the\nlast day in that " + "month will result:\n\n" + "date(2008,1,30) + monthdelta(1) -> date(2008,2,29)\n\n" + "monthdeltas may be added, subtracted, multiplied, and floor-" + "divided\nsimilarly to timedeltas. They may not be added to " + "timedeltas directly, as\nboth classes are intended to be used " + "directly with dates and datetimes.\nOnly ints may be passed to " + "the constructor, the default argument of which\nis 1 (one). " + "monthdeltas are immutable.\n\n" + "NOTE: in calculations involving the 29th, 30th, and 31st days of " + "the\nmonth, monthdeltas are not necessarily invertible [i.e., the " + "result above\nwould NOT imply that date(2008,2,29) - monthdelta(1)" + " -> date(2008,1,30)]."); + +static PyNumberMethods month_as_number = { + month_add, /* nb_add */ + month_subtract, /* nb_subtract */ + month_multiply, /* nb_multiply */ + 0, /* nb_remainder */ + 0, /* nb_divmod */ + 0, /* nb_power */ + (unaryfunc)month_negative, /* nb_negative */ + (unaryfunc)month_positive, /* nb_positive */ + (unaryfunc)month_abs, /* nb_absolute */ + (inquiry)month_bool, /* nb_bool */ + 0, /*nb_invert*/ + 0, /*nb_lshift*/ + 0, /*nb_rshift*/ + 0, /*nb_and*/ + 0, /*nb_xor*/ + 0, /*nb_or*/ + 0, /*nb_int*/ + 0, /*nb_reserved*/ + 0, /*nb_float*/ + 0, /*nb_inplace_add*/ + 0, /*nb_inplace_subtract*/ + 0, /*nb_inplace_multiply*/ + 0, /*nb_inplace_remainder*/ + 0, /*nb_inplace_power*/ + 0, /*nb_inplace_lshift*/ + 0, /*nb_inplace_rshift*/ + 0, /*nb_inplace_and*/ + 0, /*nb_inplace_xor*/ + 0, /*nb_inplace_or*/ + month_divide, /* nb_floor_divide */ + 0, /* nb_true_divide */ + month_idivide, /* nb_inplace_floor_divide */ + 0, /* nb_inplace_true_divide */ +}; + +static PyTypeObject PyDateTime_MonthType = { + PyVarObject_HEAD_INIT(NULL, 0) + "datetime.monthdelta", /* tp_name */ + sizeof(PyDateTime_Month), /* tp_basicsize */ + 0, /* tp_itemsize */ + 0, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + (reprfunc)month_repr, /* tp_repr */ + &month_as_number, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + (hashfunc)month_hash, /* tp_hash */ + 0, /* tp_call */ + (reprfunc)month_str, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + month_doc, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + month_richcompare, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + month_methods, /* tp_methods */ + month_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + month_new, /* tp_new */ + 0, /* tp_free */ +}; + /* * PyDateTime_Date implementation. */ @@ -2351,6 +2797,24 @@ return result; } +/* date + monthdelta -> date. If arg negate is true, subtract the monthdelta + * instead. + */ +static PyObject * +add_date_monthdelta(PyDateTime_Date *date, PyDateTime_Month *monthdelta, + int negate) +{ + PyObject *result = NULL; + int year = GET_YEAR(date); + int deltamonths = GET_MD_MONTHS(monthdelta); + int month = GET_MONTH(date) + (negate ? -deltamonths : deltamonths); + int day = GET_DAY(date); + + if (normalize_month_end(&year, &month, &day) >= 0) + result = new_date(year, month, day); + return result; +} + static PyObject * date_add(PyObject *left, PyObject *right) { @@ -2365,6 +2829,11 @@ return add_date_timedelta((PyDateTime_Date *) left, (PyDateTime_Delta *) right, 0); + if (PyMonth_Check(right)) + /* date + monthdelta */ + return add_date_monthdelta((PyDateTime_Date *) left, + (PyDateTime_Month *) right, + 0); } else { /* ??? + date @@ -2375,6 +2844,11 @@ return add_date_timedelta((PyDateTime_Date *) right, (PyDateTime_Delta *) left, 0); + if (PyMonth_Check(left)) + /* delta + monthdate */ + return add_date_monthdelta((PyDateTime_Date *) right, + (PyDateTime_Month *) left, + 0); } Py_INCREF(Py_NotImplemented); return Py_NotImplemented; @@ -2403,6 +2877,12 @@ return add_date_timedelta((PyDateTime_Date *) left, (PyDateTime_Delta *) right, 1); + } + if (PyMonth_Check(right)) { + /* date - monthdelta */ + return add_date_monthdelta((PyDateTime_Date *) left, + (PyDateTime_Month *) right, + 1); } } Py_INCREF(Py_NotImplemented); @@ -4005,6 +4485,30 @@ } static PyObject * +add_datetime_monthdelta(PyDateTime_DateTime *date, + PyDateTime_Month *monthdelta, int factor) +{ + int year = GET_YEAR(date); + int month = GET_MONTH(date) + GET_MD_MONTHS(monthdelta) * factor; + int day = GET_DAY(date); + int hour = DATE_GET_HOUR(date); + int minute = DATE_GET_MINUTE(date); + int second = DATE_GET_SECOND(date); + int microsecond = DATE_GET_MICROSECOND(date); + + assert(factor == 1 || factor == -1); + /* this might change the day, but we're intentionally leaving hours, + * etc. untouched + */ + if (normalize_month_end(&year, &month, &day) < 0) + return NULL; + else + return new_datetime(year, month, day, + hour, minute, second, microsecond, + HASTZINFO(date) ? date->tzinfo : Py_None); +} + +static PyObject * datetime_add(PyObject *left, PyObject *right) { if (PyDateTime_Check(left)) { @@ -4015,12 +4519,24 @@ (PyDateTime_DateTime *)left, (PyDateTime_Delta *)right, 1); + if (PyMonth_Check(right)) + /* datetime + monthdelta */ + return add_datetime_monthdelta( + (PyDateTime_DateTime *)left, + (PyDateTime_Month *)right, + 1); } else if (PyDelta_Check(left)) { /* delta + datetime */ return add_datetime_timedelta((PyDateTime_DateTime *) right, (PyDateTime_Delta *) left, 1); + } + else if (PyMonth_Check(left)) { + /* monthdelta + datetime */ + return add_datetime_monthdelta((PyDateTime_DateTime *) right, + (PyDateTime_Month *) left, + 1); } Py_INCREF(Py_NotImplemented); return Py_NotImplemented; @@ -4079,6 +4595,13 @@ result = add_datetime_timedelta( (PyDateTime_DateTime *)left, (PyDateTime_Delta *)right, + -1); + } + else if (PyMonth_Check(right)) { + /* datetime - monthdelta */ + result = add_datetime_monthdelta( + (PyDateTime_DateTime *)left, + (PyDateTime_Month *)right, -1); } } @@ -4643,7 +5166,77 @@ * Module methods and initialization. */ +/* let PyInit_datetime() initialize these + */ +static PyObject *add_unicode, *sub_unicode; + +/* monthmod allows round-tripping of calculations with dates and monthdeltas + * month_mod_ex returns 0 if OK; if not set appropriate exception and return + * -1. md and td are returned by reference. + */ +static int +month_mod_ex(PyObject *start, PyObject *end, PyObject **md, PyObject **td) +{ + PyObject *sum; + + *td = NULL; + *md = new_month(12*(GET_YEAR(end) - GET_YEAR(start)) + + GET_MONTH(end) - GET_MONTH(start) - + (GET_DAY(start) > GET_DAY(end) ? 1 : 0)); + if (*md != NULL) { + sum = PyObject_CallMethodObjArgs(start, add_unicode, *md, + NULL); + if (sum != NULL) { + /* td = end - (start + md) */ + *td = PyObject_CallMethodObjArgs(end, sub_unicode, + sum, NULL); + Py_DECREF(sum); + if (*td == Py_NotImplemented) { + Py_DECREF(*td); + *td = NULL; + PyErr_Format(PyExc_TypeError, + "arguments for monthmod are " + "incompatible: %s and %s", + Py_TYPE(start)->tp_name, + Py_TYPE(end)->tp_name); + } else if (*td != NULL) + goto Success; + } + Py_DECREF(*md); + *md = NULL; + } + return -1; +Success: + return 0; +} + +static PyObject * +monthmod(PyObject *self, PyObject *args, PyObject *kw) +{ + PyObject *start, *end, *md = NULL, *td = NULL; + static char *keywords[] = {"start", "end", NULL}; + + if (PyArg_ParseTupleAndKeywords(args, kw, "O!O!:monthmod", keywords, + &PyDateTime_DateType, &start, + &PyDateTime_DateType, &end)) + month_mod_ex(start, end, &md, &td); + + return Py_BuildValue("OO", md, td); +} + +PyDoc_STRVAR(monthmod_doc, + "monthmod(start, end) -> (monthdelta, timedelta)\n\n" + "Distribute the interim between start and end dates into " + "monthdelta and\ntimedelta portions. If and only if start is " + "after end, returned monthdelta\nwill be negative. Returned " + "timedelta is never negative, and is always\nsmaller than the " + "month in which end occurs.\n\n" + "Invariant: dt + monthmod(dt, dt+td)[0] + monthmod(dt, dt+td)[1]" + " = dt + td"); + static PyMethodDef module_methods[] = { + {"monthmod", (PyCFunction)monthmod, METH_VARARGS|METH_KEYWORDS, + monthmod_doc}, {NULL, NULL} }; @@ -4655,13 +5248,16 @@ &PyDateTime_DateTimeType, &PyDateTime_TimeType, &PyDateTime_DeltaType, + &PyDateTime_MonthType, &PyDateTime_TZInfoType, new_date_ex, new_datetime_ex, new_time_ex, new_delta_ex, + new_month_ex, datetime_fromtimestamp, - date_fromtimestamp + date_fromtimestamp, + month_mod_ex }; @@ -4695,6 +5291,8 @@ return NULL; if (PyType_Ready(&PyDateTime_DeltaType) < 0) return NULL; + if (PyType_Ready(&PyDateTime_MonthType) < 0) + return NULL; if (PyType_Ready(&PyDateTime_TimeType) < 0) return NULL; if (PyType_Ready(&PyDateTime_TZInfoType) < 0) @@ -4718,6 +5316,19 @@ return NULL; Py_DECREF(x); + /* monthdelta values */ + d = PyDateTime_MonthType.tp_dict; + + x = new_month(-MAX_DELTA_MONTHS); + if (x == NULL || PyDict_SetItemString(d, "min", x) < 0) + return NULL; + Py_DECREF(x); + + x = new_month(MAX_DELTA_MONTHS); + if (x == NULL || PyDict_SetItemString(d, "max", x) < 0) + return NULL; + Py_DECREF(x); + /* date values */ d = PyDateTime_DateType.tp_dict; @@ -4771,6 +5382,13 @@ if (x == NULL || PyDict_SetItemString(d, "resolution", x) < 0) return NULL; Py_DECREF(x); + + /* initialize values that month_mod_ex uses + */ + add_unicode = PyUnicode_InternFromString("__add__"); + sub_unicode = PyUnicode_InternFromString("__sub__"); + if (add_unicode == NULL || sub_unicode == NULL) + return NULL; /* module initialization */ PyModule_AddIntConstant(m, "MINYEAR", MINYEAR); @@ -4788,6 +5406,10 @@ Py_INCREF(&PyDateTime_DeltaType); PyModule_AddObject(m, "timedelta", (PyObject *) &PyDateTime_DeltaType); + + Py_INCREF(&PyDateTime_MonthType); + PyModule_AddObject(m, "monthdelta", + (PyObject *) &PyDateTime_MonthType); Py_INCREF(&PyDateTime_TZInfoType); PyModule_AddObject(m, "tzinfo", (PyObject *) &PyDateTime_TZInfoType);