Welcome, guest | Sign In | My Account | Store | Cart

This pure Python module defines a class Date and several methods to deal with it, including conversions to some other formats.

Needs Python 2.2

Python, 351 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
"""
The module defines a class Date and several methods to deal with it, including conversions.

The "format" of the Date class is as follows: Each instance has three attributes,
year, month and day, all represented as integers and writable. Although no constraints are
enforced, the intended range of values is:

1 <= day <= 31 (more precisely 1 <= day <= NumberDaysMonth(month, year))
1 <= month <= 12 (1 is January and 12 is December)

It is up to the client of this class to make sure that all assignments are correct.

In making conversions with the time module (wether in seconds or in a 9-tuple) local time
is always used.

History of changes:
version 2.0.1:
 - Added docstring to the module.
 - Changed implementation of next() and previous() to take advantage of NumberDaysMonth().

version 2.0: Complete rewrite of the module.
 - Removed weekday as instance attribute of the class.
 - Added conversion to and from Julian Day number. Added NumberDaysMonth function. Added
   __sub__ and __add__. Made the class hashable.
 - Added some (still insuficient and completely ad-hoc) test code when run as __main__.
"""

__version__ = 2.01
__author__ = "G. Rodrigues"

import time

#Needed for conversion to COM dates.
import pythoncom

def IsLeapYear(year):
    """Returns 1 if year is a leap year, zero otherwise."""
    if year%4 == 0:
        if year%100 == 0:
            if year%400 == 0:
                return 1
            else:
                return 0
        else:
            return 1
    else:
        return 0

def NumberDaysYear(year):
    """Returns the number of days in the year."""
    return 365 + IsLeapYear(year)

def NumberDaysMonth(month = None, year = None):
    """Returns the number of days in the month.

    If any of the arguments is missing (month or year) the current month/year is assumed."""
    if month is None:
        m = time.localtime()[1]
    else:
        m = month

    if year is None:
        y = time.localtime()[0]
    else:
        y = year
    
    if m == 2:
        if IsLeapYear(y):
            return 29
        else:
            return 28
    elif m in (1, 3, 5, 7, 8, 10, 12):
        return 31
    else:
        return 30


class Date(object):
    """The Date class."""
    
    Weekdays = ["Monday",
                "Tuesday",
                "Wednesday",
                "Thursday",
                "Friday",
                "Saturday",
                "Sunday"]

    Months = ["January",
              "February",
              "March",
              "April",
              "May",
              "June",
              "July",
              "August",
              "September",
              "October",
              "November",
              "December"]

    #The slots in a Date object are constrained to allow more efficient operations.
    __slots__ = ["year", "month", "day"]

    def __init__(self, tm = None):
        """The initializer has an optional argument, time, in the time module format,
        wether as in seconds since the epoch (Unix time) wether as a tuple (time tuple).
        If it is not provided, then it returns the current date."""
        if tm is None:
            t = time.localtime()
        else:
            if isinstance(tm, int):
                t = time.localtime(tm)
            else:
                t = tm
                
        self.year, self.month, self.day = t[:3]

    def weekday(self):
        """Returns the weekday of the date.

        The format is as in the time module: Monday is 0 and sunday is 6."""
        a = (14 - self.month)//12
        y = self.year - a
        m = self.month + 12*a -2
        d = (self.day + y + y//4 - y//100 + y//400 + (31*m//12))%7
        if d:
            ret = d - 1
        else:
            ret = 6
        return ret

    def __str__(self):
        return "%s, %d-%s-%d" % (Date.Weekdays[self.weekday()],
                                 self.day,
                                 Date.Months[self.month - 1],
                                 self.year)

    def copy(self):
        """Deep copy of Date objects."""
        ret = Date()
        ret.year, ret.month, ret.day = self.year, self.month, self.day
        return ret

    #The iterator protocol. The iteration is "destructive", like in files.
    def __iter__(self):
        return self

    def next(self):
        #Last day of the month.
        if self.day == NumberDaysMonth(self.month, self.year):
            self.day = 1
            #December case.
            if self.month == 12:
                self.month = 1
                self.year += 1
            else:
                self.month += 1
        else:
            self.day += 1

    #Extended iterator protocol. One can go backwards.
    def previous(self):
        #First day of the month.
        if self.day == 1:
            #January case.
            if self.month == 1:
                self.month = 12
                self.year -= 1
            else:
                self.month -= 1
            self.day = NumberDaysMonth(self.month, self.year)
        else:
            self.day -= 1

    #Comparison methods.
    def __eq__(self, date):
        return self.year == date.year and self.month == date.month and\
               self.day == date.day

    def __lt__(self, other):
        return (self.year, self.month, self.day) < (other.year, other.month, other.dy)

    def __le__(self, other):
        return (self.year, self.month, self.day) <= (other.year, other.month, other.dy)

    #Dates can be used as keys in dictionaries.
    def __hash__(self):
        return hash((self.year, self.month, self.day))

    #Some useful methods.
    def GetYearDay(self):
        """Returns the year day of a date."""
        ret = self.day
        for month in range(1, self.month):
            ret += NumberDaysMonth(month, self.year)
        return ret

    def DaysToEndYear(self):
        """Returns the number of days until the end of the year."""
        ret = NumberDaysMonth(self.month, self.year) - self.day
        for i in range(self.month + 1, 13):
            ret += NumberDaysMonth(i, self.year)
        return ret

    def GetWeekday(self):
        """Returns the weekday of the date in string format."""
        return Date.Weekdays[self.weekday()]

    def GetMonth(self):
        """Returns the month of the date in string format."""
        return Date.Months[self.month - 1]

    def ToJDNumber(self):
        """Returns the Julian day number of a date."""
        a = (14 - self.month)//12
        y = self.year + 4800 - a
        m = self.month + 12*a - 3
        return self.day + ((153*m + 2)//5) + 365*y + y//4 - y//100 + y//400 - 32045

    #Binary operations.
    def __add__(self, n):
        """Adds a (signed) number of days to the date."""
        if isinstance(n, int):
            #Calculate julian day number and add n.
            temp = self.ToJDNumber() + n
            #Convert back to date format.
            return DateFromJDNumber(temp)
        else:
            raise TypeError, "%s is not an integer." % str(n)

    def __sub__(self, date):
        """Returns the (signed) difference of days between the dates."""
        #If it is an integer defer calculation to the __add__ method.
        if isinstance(date, int):
            return self.__add__(-date)
        elif isinstance(date, Date):
            #Case: The years are equal.
            if self.year == date.year:
                return self.GetYearDay() - date.GetYearDay()
            else:
                if self < date:
                    ret = self.DaysToEndYear() + date.GetYearDay()
                    for year in range(self.year + 1, date.year):
                        ret += NumberDaysYear(year)
                    return -ret
                else:
                    ret = date.DaysToEndYear() + self.GetYearDay()
                    for year in range(date.year + 1, self.year):
                        ret += NumberDaysYear(year)
                    return ret
        else:
            raise TypeError, "%s is neither an integer nor a Date." % str(date)

    #Adding an integer is "commutative".
    def __radd__(self, n):
        return self.__add__(n)

    #Conversion methods.
    def ToTimeTuple(self):
        """Convert a date into a time tuple (time module) corresponding to the
        same day with the midnight hour."""
        ret = [self.year, self.month, self.day]
        ret.extend([0, 0, 0])
        ret.append(self.weekday())
        ret.extend([self.GetYearDay(), 0])
        return tuple(ret)

    def ToUnixTime(self):
        """Convert a date into Unix time (seconds since the epoch) corresponding
        to the same day with the midnight hour."""
        return time.mktime(self.ToTimeTuple())

    def ToCOMTime(self):
        """Convert a date into COM format."""
        return pythoncom.MakeTime(self.ToUnixTime())


#More conversion functions.
def DateFromJDNumber(n):
    """Returns a date corresponding to the given Julian day number."""
    if not isinstance(n, int):
        raise TypeError, "%s is not an integer." % str(n)

    a = n + 32044
    b = (4*a + 3)//146097
    c = a - (146097*b)//4
    d = (4*c + 3)//1461
    e = c - (1461*d)//4
    m = (5*e + 2)//153

    ret = Date()
    ret.day = e + 1 - (153*m + 2)//5
    ret.month = m + 3 - 12*(m//10)
    ret.year = 100*b + d - 4800 + m/10
    return ret

def DateFromCOM(t):
    """Converts a COM time directly into the Date format."""
    return Date(int(t))

def strpdate(s):
    """This function reads a string in the standard date representation
    format and returns a date object."""
    ret = Date()
    temp = s.split(", ")
    temp = temp[1].split("-")
    ret.year, ret.month, ret.day = (int(temp[2]),
                                    Date.Months.index(temp[1]) + 1,
                                    int(temp[0]))
    return ret


#Some test code.
if __name__ == "__main__":
    #Print the days still left in the month.
    temp = Date()
    curr_month = temp.month
    while temp.month == curr_month:
        print temp
        temp.next()

    print "\n"

    #How many days until the end of the year?
    temp = Date()
    temp.day, temp.month = 1, 1
    curr_year = temp.year
    while temp.year == curr_year:
        print "%s is %d days away from the end of the year." % (str(temp),
                                                                temp.DaysToEndYear())
        temp += NumberDaysMonth(temp.month)

    print "\n"

    #Playing with __sub__.
    temp = Date()
    temp_list = []
    curr_year = temp.year
    while temp.year == curr_year:
        temp_list.append(temp)
        temp += NumberDaysMonth(temp.month)
    for elem in temp_list:
        print "%s differs %d days from current date: %s" % (str(elem),
                                                            elem - Date(),
                                                            str(Date()))

    print "\n"

    #Swapping arguments works?
    print 23 + Date()

This module should be sufficient for most needs when handling dates. On the other hand, none of the calendar oddities (e.g. no zero year, missing dates and other such whatnots) are covered, so, if you need to handle dates before the definitive adoption of the gregorian calendar (the adoption varied from country to country) then I recommend the mxDateTime package.

10 comments

John Machin 19 years, 8 months ago  # | flag

Try running Pychecker over your code.

rhinospam 19 years, 3 months ago  # | flag

Bug in __lt__ and __le__. There is a hidous bug in the __lt__ and __le__ operators The fix should be something like the following.

    def __lt__(self, date):
        if self.year There is a hidous bug in the __lt__ and __le__ operators
The fix should be something like the following.

<pre>
    def __lt__(self, date):
        if self.year

</pre>

rhinospam 19 years, 3 months ago  # | flag

Re: previous comment. Gak! I posted the correct code in the previous comment, but the posting process munged it horribly. So... trust me, there really is this bug and I really do have a fix.

Kent Hu 19 years, 3 months ago  # | flag

__le__ and __lt__ bug. Yeah, I see the bug, too. In case it's not clear, the bug is that 2000-04-20 won't be less than 2002-07-03, because 20 > 3.

A simple fix that I used in my own date module is:

def __lt__(self, other):

return (self.year, self.month, self.day) &lt; (other.year, other.month, other.dy)

def __le__(self, other):

return (self.year, self.month, self.day) &lt;= (other.year, other.month, other.dy)
Gonçalo Rodrigues (author) 19 years, 1 month ago  # | flag

Thanks for caughting this newbie error. Thanks a bunch for caughting this! It had passed all my testing (not included in the code). I've applied your suggested fix - it's the simplest. Now I can go and hide in shame and never show my face again and...

Neil Hodgson 19 years, 1 month ago  # | flag

Can be made cross-platform. This module can easily be made cross platform by removing the unconditional import of pythoncom. The ToCOMTime method will not work then but all the other functionality is OK. The import can either be moved into the ToCOMTime method or wrapped in a try block like so: try: import pythoncom except ImportError: pass

Ralf Kaiser 18 years, 5 months ago  # | flag

small bug in testcode section. Thank you for the code, works nice. Found a small bug in the testcode section:

"temp += NumberDaysMonth(temp.month)" gives a wrong result when the loop is extended to compute dates over a leap year.

Should better be set to

"temp += NumberDaysMonth(temp.month, temp.year)"

Cheers, Ralf

Martin Hanus 17 years, 2 months ago  # | flag

Licence. I have not find any licence. Could you choose any to clarify distribution conditions? I like this package very much.

Shane Wang 11 years, 8 months ago  # | flag
if year%4 == 0:
    if year%100 == 0:
        if year%400 == 0:
            return 1
        else:
            return 0
    else:
        return 1
else:
    return 0

looks like we don't need to judge year%400==0, as my understanding, year%4==0 is true and year%100==0 is true, year%400==0 will be true for sure. correct me if my understanding is wrong

Sam Denton 6 years, 2 months ago  # | flag

Shane Wang:

>>> for divisor in 4, 100, 400: print "100 %% %i == %i" % (divisor, 100 % divisor)
100 % 4 == 0
100 % 100 == 0
100 % 400 == 100
Created by Gonçalo Rodrigues on Sat, 2 Mar 2002 (PSF)
Python recipes (4591)
Gonçalo Rodrigues's recipes (9)

Required Modules

Other Information and Tasks