views:

151

answers:

3

Can anyone explain the best way to format a date time string in Python where the date value is prior to the year 1900? strftime requires dates later than 1900.

+1  A: 

It's a bit cumbersome, but it works (at least in stable versions of python):

>>> ts = datetime.datetime(1895, 10, 6, 16, 4, 5)
>>> '{0.year}-{0.month:{1}}-{0.day:{1}} {0.hour:{1}}:{0.minute:{1}}'.format(ts, '02')
'1895-10-06 16:04'

note that str would still produce a readable string:

>>> str(ts)
'1895-10-06 16:04:05'

edit
The closest possible way to emulate the default behaviour is to hard-code the dictionary such as:

>>> d = {'%Y': '{0.year}', '%m': '{0.month:02}'}    # need to include all the formats
>>> '{%Y}-{%m}'.format(**d).format(ts)
'1895-10'

You'll need to enclose all format specifiers into the curly braces with the simple regex:

>>> re.sub('(%\w)', r'{\1}', '%Y-%m-%d %H sdf')
'{%Y}-{%m}-{%d} {%H} sdf'

and at the end we come to simple code:

def ancient_fmt(ts, fmt):
    fmt = fmt.replace('%%', '%')
    fmt = re.sub('(%\w)', r'{\1}', fmt)
    return fmt.format(**d).format(ts)

def main(ts, format):
    if ts.year < 1900:
        return ancient_format(ts, fmt)
    else:
        return ts.strftime(fmt)

where d is a global dictionary with keys corresponding to some specifiers in strftime table.

edit 2
To clarify: this approach will work only for the following specifiers: %Y, %m, %d, %H, %M, %S, %f, i.e., those that are numeric, if you need textual information, you'd better off with babel or any other solution.

SilentGhost
+1  A: 

The babel internationalization library seems to have no problems with it. See the docs for babel.dates

Steven
except that you need to use non-standard specifiers
SilentGhost
so why is that a problem? there are docs
Ned Deily
because they're multiplying entities w/o need
SilentGhost
As opposed to re-inventing code w/o need?? There's always a tradeoff. Or perhaps I don't understand your point.
Ned Deily
A: 

The calendar is exactly the same every 400 years. Therefore it is sufficient to change year by multiple of 400 such as year >= 1900 before calling datetime.strftime().

The code shows what problems such approach has:

#/usr/bin/env python2.6
import re
import warnings
from datetime import datetime


def strftime(datetime_, format, force=False):
    """`strftime()` that works for year < 1900.

    Disregard calendars shifts.

    >>> def f(fmt, force=False):
    ...     return strftime(datetime(1895, 10, 6, 11, 1, 2), fmt, force)
    >>> f('abc %Y %m %D') 
    'abc 1895 10 10/06/95'
    >>> f('%X')
    '11:01:02'
    >>> f('%c') #doctest:+NORMALIZE_WHITESPACE
    Traceback (most recent call last):
    ValueError: '%c', '%x' produce unreliable results for year < 1900
    use force=True to override
    >>> f('%c', force=True)
    'Sun Oct  6 11:01:02 1895'
    >>> f('%x') #doctest:+NORMALIZE_WHITESPACE
    Traceback (most recent call last):
    ValueError: '%c', '%x' produce unreliable results for year < 1900
    use force=True to override
    >>> f('%x', force=True)
    '10/06/95'
    >>> f('%%x %%Y %Y')
    '%x %Y 1895'
    """
    year = datetime_.year
    if year >= 1900:
       return datetime_.strftime(format)

    # make year larger then 1900 using 400 increment
    assert year < 1900
    factor = (1900 - year - 1) // 400 + 1
    future_year = year + factor * 400
    assert future_year > 1900

    format = Specifier('%Y').replace_in(format, year)
    result = datetime_.replace(year=future_year).strftime(format)
    if any(f.ispresent_in(format) for f in map(Specifier, ['%c', '%x'])):
        msg = "'%c', '%x' produce unreliable results for year < 1900"
        if not force:
            raise ValueError(msg + " use force=True to override")
        warnings.warn(msg)
        result = result.replace(str(future_year), str(year))
    assert (future_year % 100) == (year % 100) # last two digits are the same
    return result


class Specifier(str):
    """Model %Y and such in `strftime`'s format string."""
    def __new__(cls, *args):
        self = super(Specifier, cls).__new__(cls, *args)
        assert self.startswith('%')
        assert len(self) == 2
        self._regex = re.compile(r'(%*{0})'.format(str(self)))
        return self

    def ispresent_in(self, format):
        m = self._regex.search(format)
        return m and m.group(1).count('%') & 1 # odd number of '%'

    def replace_in(self, format, by):
        def repl(m):
            n = m.group(1).count('%')
            if n & 1: # odd number of '%'
                prefix = '%'*(n-1) if n > 0 else ''
                return prefix + str(by) # replace format
            else:
                return m.group(0) # leave unchanged
        return self._regex.sub(repl, format)


if __name__=="__main__":
    import doctest; doctest.testmod()
J.F. Sebastian