Skip to content

Commit e8fb275

Browse files
committed
DateFormatter shows microseconds instead of %f for years <= 1900
It also fails to replace %y or %x correctly, since its strftime implementation replaces only 4-digit years. I added a boolean flag DateFormatter.replace_directives_before_1900: - If False, strftime uses the old implementation, which will not replace %f and which will replace incorrect values for %y and %x. - If True, strftime will first try a few regular expressions to replace %y/%x/%f with the appropriate datetime values. I'm not positive this covers all cases but I don't know of any cases where this fails right now. Add a simple test for DateFormatter with and without this flag. closes #3179 Change-Id: Idff9d06cbc6dc00a3cb8dcf113983d82dbdd3fde
1 parent f529297 commit e8fb275

File tree

2 files changed

+82
-10
lines changed

2 files changed

+82
-10
lines changed

lib/matplotlib/dates.py

+43-10
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ def __init__(self, fmt, tz=None):
401401
tz = _get_rc_timezone()
402402
self.fmt = fmt
403403
self.tz = tz
404+
self.replace_directives_before_1900 = False
404405

405406
def __call__(self, x, pos=0):
406407
if x == 0:
@@ -426,16 +427,28 @@ def _findall(self, text, substr):
426427
i = j + 1
427428
return sites
428429

429-
# Dalke: I hope I did this math right. Every 28 years the
430-
# calendar repeats, except through century leap years excepting
431-
# the 400 year leap years. But only if you're using the Gregorian
432-
# calendar.
430+
def strftime_pre_1900(self, dt, fmt, replace_directives=False):
431+
"""Call time.strftime for years before 1900 by rolling
432+
forward a multiple of 28 years.
433433
434-
def strftime(self, dt, fmt):
435-
fmt = self.illegal_s.sub(r"\1", fmt)
436-
fmt = fmt.replace("%s", "s")
437-
if dt.year > 1900:
438-
return cbook.unicode_safe(dt.strftime(fmt))
434+
Dalke: I hope I did this math right. Every 28 years the
435+
calendar repeats, except through century leap years excepting
436+
the 400 year leap years. But only if you're using the Gregorian
437+
calendar.
438+
439+
This implementation uses time's strftime and only handles 4-digit
440+
years. If replace_directive is set to True, as a hack we try and
441+
replace %y %x %f with the appropriate values.
442+
"""
443+
if replace_directives:
444+
replace_map = \
445+
{'f': '{:06d}'.format(dt.microsecond),
446+
'x': '{:02d}/{:02d}/{:02d}'.format(dt.month, dt.day, dt.year % 100),
447+
'y': '{:02d}'.format(dt.year % 100)}
448+
for directive, replacement in replace_map.items():
449+
# Replace all (odd # of %s) followed by f, x, or y
450+
fmt = re.sub(r'((^|[^%])(%%)*)%' + directive,
451+
r'\g<1>' + replacement, fmt)
439452

440453
year = dt.year
441454
# For every non-leap year century, advance by
@@ -444,7 +457,7 @@ def strftime(self, dt, fmt):
444457
off = 6 * (delta // 100 + delta // 400)
445458
year = year + off
446459

447-
# Move to around the year 2000
460+
# Move to between the years 1973 and 2000
448461
year = year + ((2000 - year) // 28) * 28
449462
timetuple = dt.timetuple()
450463
s1 = time.strftime(fmt, (year,) + timetuple[1:])
@@ -453,6 +466,10 @@ def strftime(self, dt, fmt):
453466
s2 = time.strftime(fmt, (year + 28,) + timetuple[1:])
454467
sites2 = self._findall(s2, str(year + 28))
455468

469+
# Generate timestamp string for year and year+28, and replace
470+
# instances of respective years in same location with dt's year
471+
# TODO: similar fixes should be applied for %y %x, which
472+
# output the year without its century (2-digit, in [00,99])
456473
sites = []
457474
for site in sites1:
458475
if site in sites2:
@@ -465,6 +482,22 @@ def strftime(self, dt, fmt):
465482

466483
return cbook.unicode_safe(s)
467484

485+
def strftime(self, dt, fmt):
486+
"""Refer to documentation for datetime.strftime.
487+
488+
Warning: For years before 1900, the following datetime directives
489+
will not work: %y %x %f. An attempt to replace these can be
490+
enabled by setting replace_directives_before_1900 = True.
491+
"""
492+
fmt = self.illegal_s.sub(r"\1", fmt)
493+
fmt = fmt.replace("%s", "s")
494+
if dt.year >= 1900:
495+
# Note: in python 3.3 this is okay for years >= 1000,
496+
# refer to http://bugs.python.org/issue177742
497+
return cbook.unicode_safe(dt.strftime(fmt))
498+
499+
return self.strftime_pre_1900(dt, fmt, self.replace_directives_before_1900)
500+
468501

469502
class IndexDateFormatter(ticker.Formatter):
470503
"""

lib/matplotlib/tests/test_dates.py

+39
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,45 @@ def test_DateFormatter():
159159
fig.autofmt_xdate()
160160

161161

162+
def test_date_formatter_strftime():
163+
"""
164+
Tests that DateFormatter matches datetime.strftime,
165+
check microseconds for years before 1900 for bug #3179.
166+
"""
167+
def test_strftime_fields(dt):
168+
"""For datetime object dt, check DateFormatter fields"""
169+
formatter = mdates.DateFormatter("%w %d %m %Y %H %I %M %S")
170+
# Compute date fields without using datetime.strftime,
171+
# since datetime.strftime does not work before year 1900
172+
formatted_date_str = (
173+
"{weekday} {day:02d} {month:02d} {full_year:4d} "
174+
"{hour24:02d} {hour12:02d} {minute:02d} {second:02d}"
175+
.format(
176+
# weeknum=dt.isocalendar()[1], # %U/%W {weeknum:02d}
177+
# %w Sunday=0, weekday() Monday=0
178+
weekday=str((dt.weekday() + 1) % 7),
179+
day=dt.day,
180+
month=dt.month,
181+
full_year=dt.year,
182+
hour24=dt.hour,
183+
hour12=((dt.hour-1) % 12) + 1,
184+
minute=dt.minute,
185+
second=dt.second))
186+
assert_equal(formatter.strftime(dt, formatter.fmt), formatted_date_str)
187+
188+
formatter2 = mdates.DateFormatter("%y %f %x")
189+
formatter2.replace_directives_before_1900 = True
190+
formatted_date_str2 = (
191+
"{year:02d} {microsecond:06d} {month:02d}/{day:02d}/{year:02d}".format(
192+
day=dt.day, month=dt.month, year=dt.year % 100, microsecond=dt.microsecond))
193+
assert_equal(formatter2.strftime(dt, formatter2.fmt), formatted_date_str2)
194+
195+
for year in range(1, 3000, 71):
196+
# Iterate through random set of years
197+
test_strftime_fields(datetime.datetime(year, 1, 1))
198+
test_strftime_fields(datetime.datetime(year, 2, 3, 4, 5, 6, 123456))
199+
200+
162201
def test_date_formatter_callable():
163202
scale = -11
164203
locator = mock.Mock(_get_unit=mock.Mock(return_value=scale))

0 commit comments

Comments
 (0)