datetime strftime methods require year >= 1900

Andrew Dalke adalke at mindspring.com
Wed Sep 29 19:00:32 EDT 2004


John Hunter wrote:
> Does anyone know of a datetime string formatter that can handles
> strftime format strings over the full range that datetime objects
> support?

Here's what the Python source says

         /* Give up if the year is before 1900.
          * Python strftime() plays games with the year, and different
          * games depending on whether envar PYTHON2K is set.  This makes
          * years before 1900 a nightmare, even if the platform strftime
          * supports them (and not all do).
          * We could get a lot farther here by avoiding Python's strftime
          * wrapper and calling the C strftime() directly, but that isn't
          * an option in the Python implementation of this module.
          */

The underlying time.strftime module supports special
behaviour for dates < 1900.

 >>> time.accept2dyear
1
 >>> time.strftime("%Y", datetime.date(20, 1, 1).timetuple())
'2020'
 >>> time.accept2dyear = 0
 >>> time.strftime("%Y", datetime.date(20, 1, 1).timetuple())
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
ValueError: year >= 1900 required
 >>>

One concern about your request is, what a date mean
when you get before 1900?  I assume you want the proleptic
Gregorian calendar, that is, to apply it even when and
where it wasn't in use.

One way to fake it is to move the date to a date in the
supported time range which starts on the same day, then
use strftime on that new date.

It's not enough to find the fake year number in the
resulting string and convert it into the real year
number.  After all, the format string might be
"1980 %Y" and if the %Y expands to 1980 in your shifted
time frame then you don't know which to change.

To figure that out, move the date forward by 28 years
(which is the repeat cycle except for the non-leap
centuries) and do it again.  The parts of the two
strings that differ indicate where to put the change.

I tried to write this function but I wasn't sure
how to handle the non-leap year centuries.  It seems
to be that those are the same as 6 years later, so
that Jan. 1900's calendar looks like 1906's.

Here's what I came up with.  Seems to work.


# Format a datetime.date using the proleptic Gregorian calendar

import time, datetime

def _findall(text, substr):
     # Also finds overlaps
     sites = []
     i = 0
     while 1:
         j = text.find(substr, i)
         if j == -1:
             break
         sites.append(j)
         i=j+1
     return sites

# I hope I did this math right.  Every 28 years the
# calendar repeats, except through century leap years
# excepting the 400 year leap years.  But only if
# you're using the Gregorian calendar.

def strftime(dt, fmt):
     # WARNING: known bug with "%s", which is the number
     # of seconds since the epoch.  This is too harsh
     # of a check.  It should allow "%%s".
     fmt = fmt.replace("%s", "s")
     if dt.year > 1900:
         return time.strftime(fmt, dt.timetuple())

     year = dt.year
     # For every non-leap year century, advance by
     # 6 years to get into the 28-year repeat cycle
     delta = 2000 - year
     off = 6*(delta // 100 + delta // 400)
     year = year + off

     # Move to around the year 2000
     year = year + ((2000 - year)//28)*28
     timetuple = dt.timetuple()
     s1 = time.strftime(fmt, (year,) + timetuple[1:])
     sites1 = _findall(s1, str(year))

     s2 = time.strftime(fmt, (year+28,) + timetuple[1:])
     sites2 = _findall(s2, str(year+28))

     sites = []
     for site in sites1:
         if site in sites2:
             sites.append(site)

     s = s1
     syear = "%4d" % (dt.year,)
     for site in sites:
         s = s[:site] + syear + s[site+4:]
     return s

# Make sure that the day names are in order
# from 1/1/1 until August 2000
def test():
     s = strftime(datetime.date(1800, 9, 23),
                  "%Y has the same days as 1980 and 2008")
     if s != "1800 has the same days as 1980 and 2008":
         raise AssertionError(s)

     print "Testing all day names from 0001/01/01 until 2000/08/01"
     days = []
     for i in range(1, 10):
         days.append(datetime.date(2000, 1, i).strftime("%A"))
     nextday = {}
     for i in range(8):
         nextday[days[i]] = days[i+1]

     startdate = datetime.date(1, 1, 1)
     enddate = datetime.date(2000, 8, 1)
     prevday = strftime(startdate, "%A")
     one_day = datetime.timedelta(1)

     testdate = startdate + one_day
     while testdate < enddate:
         if (testdate.day == 1 and testdate.month == 1 and
             (testdate.year % 100 == 0)):
             print testdate.year
         day = strftime(testdate, "%A")
         if nextday[prevday] != day:
             raise AssertionError(str(testdate))
         prevday = day
         testdate = testdate + one_day

if __name__ == "__main__":
     test()


 >>> strftime(datetime.date(1850, 8, 2), "%Y/%M/%d was a %A")
'1850/00/02 was a Friday'
 >>>

% cal 8 1850
     August 1850
  S  M Tu  W Th  F  S
              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

				Andrew
				dalke at dalkescientific.com



More information about the Python-list mailing list