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

A strftime implementation that supports proleptic Gregorian dates before 1900

Python, 102 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
# Format a datetime through its full proleptic Gregorian date range.
#
# >>> strftime(datetime.date(1850, 8, 2), "%Y/%M/%d was a %A")
# '1850/00/02 was a Friday'
# >>>

import re, datetime, time

# remove the unsupposed "%s" command.  But don't
# do it if there's an even number of %s before the s
# because those are all escaped.  Can't simply
# remove the s because the result of
#  %sY
# should be %Y if %s isn't supported, not the
# 4 digit year.
_illegal_s = re.compile(r"((^|[^%])(%%)*%s)")

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

# Every 28 years the calendar repeats, except through century leap
# years where it's 6 years.  But only if you're using the Gregorian
# calendar.  ;)

def strftime(dt, fmt):
    if _illegal_s.search(fmt):
        raise TypeError("This strftime implementation does not handle %s")
    if dt.year > 1900:
        return dt.strftime(fmt)

    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"
    # Get the weekdays.  Can't hard code them; they could be
    # localized.
    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 "Testing century", testdate.year
        day = strftime(testdate, "%A")
        if nextday[prevday] != day:
            raise AssertionError(str(testdate))
        prevday = day
        testdate = testdate + one_day

if __name__ == "__main__":
    test()

Python's datetime supports Gregorian dates between 0001/01/01 and 9999/12/31. Of course no one used these dates before the creation of the Gregorian calendar so the date Jan. 13, 1054 makes sense only by extending the calendar backwards. This is called the proleptic Gregorian calendar.

The strftime method on the datetime doesn't support years before 1900. time.strftime has tricky Y2K specific code so that year 03 is the same as Y 2003. Neither can be used to print a formatted proleptic date in the full range of datetime.

This function is a bit of a hack to support that range. The calendar usually repeats every 28 years, excepting years through century years which are leap years in which case it's a 6 year repeat. What I do is use the standard strftime for any date after 1900. For those before I find the year near Y2000 which has the same calendar and use that time instead. Everything will be correct except the %Y and %s fields.

Fixing the %Y is done by finding another date with the same calendar (shift by 28 years). If the text was the first year in the first and the second year in the second then replace that stretch of text with the original year.

I specifically do not support %s, which the number of seconds in the current epoch. Should it be the number of seconds since Jan. 1 of year 1?

You can also find this code in the matplotlib.dates library. Originally contributed by Andrew Dalke to comp.lang.python .