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
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.
Try running Pychecker over your code.
Bug in __lt__ and __le__. There is a hidous bug in the __lt__ and __le__ operators The fix should be something like the following.
</pre>
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.
__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):
def __le__(self, other):
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...
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
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
Licence. I have not find any licence. Could you choose any to clarify distribution conditions? I like this package very much.
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
Shane Wang: