Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 268ca34

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. Instead, we now use a regular expression to replace %f with the microsecond value. (We could also be tricky and call strftime again with the original datetime object ..) We also make substitutions for both 2-digit and 4-digit years, which for example are displayed by %y, %Y, and %x. Minor point: in time.h, strftime will not use padding for %y and %Y but will use zero-padding for %x. Since this is unlikely to ever be a cause of concern and isn't documented anywhere afaik, I've used zero-padding for all three. (Certainly it's preferable to just printing the wrong year!) Add tests and (maybe excessively long?) comments. closes #3179 Change-Id: Idff9d06cbc6dc00a3cb8dcf113983d82dbdd3fde
1 parent 2ac9ac2 commit 268ca34

File tree

3 files changed

+143
-35
lines changed

3 files changed

+143
-35
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
DateFormatter strftime
2+
----------------------
3+
Date formatters' (:class:`~matplotlib.dates.DateFormatter`)
4+
:meth:`~matplotlib.dates.DateFormatter.strftime` method will format
5+
a :class:`datetime.datetime` object with the format string passed to
6+
the formatter's constructor. This method accepts datetimes with years
7+
before 1900, unlike :meth:`datetime.datetime.strftime`.

lib/matplotlib/dates.py

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ class DateFormatter(ticker.Formatter):
394394

395395
def __init__(self, fmt, tz=None):
396396
"""
397-
*fmt* is an :func:`strftime` format string; *tz* is the
397+
*fmt* is a :func:`strftime` format string; *tz* is the
398398
:class:`tzinfo` instance.
399399
"""
400400
if tz is None:
@@ -414,28 +414,54 @@ def __call__(self, x, pos=0):
414414
def set_tzinfo(self, tz):
415415
self.tz = tz
416416

417-
def _findall(self, text, substr):
418-
# Also finds overlaps
419-
sites = []
417+
def _replace_common_substr(self, s1, s2, sub1, sub2, replacement):
418+
"""Helper function for replacing substrings sub1 and sub2
419+
located at the same indexes in strings s1 and s2 respectively,
420+
with the string replacement. It is expected that sub1 and sub2
421+
have the same length. Returns the pair s1, s2 after the
422+
substitutions.
423+
"""
424+
# Find common indexes of substrings sub1 in s1 and sub2 in s2
425+
# and make substitutions inplace. Because this is inplace,
426+
# it is okay if len(replacement) != len(sub1), len(sub2).
420427
i = 0
421-
while 1:
422-
j = text.find(substr, i)
428+
while True:
429+
j = s1.find(sub1, i)
423430
if j == -1:
424431
break
425-
sites.append(j)
432+
426433
i = j + 1
427-
return sites
434+
if s2[j:j + len(sub2)] != sub2:
435+
continue
428436

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.
437+
s1 = s1[:j] + replacement + s1[j + len(sub1):]
438+
s2 = s2[:j] + replacement + s2[j + len(sub2):]
433439

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))
440+
return s1, s2
441+
442+
def strftime_pre_1900(self, dt, fmt=None):
443+
"""Call time.strftime for years before 1900 by rolling
444+
forward a multiple of 28 years.
445+
446+
*fmt* is a :func:`strftime` format string.
447+
448+
Dalke: I hope I did this math right. Every 28 years the
449+
calendar repeats, except through century leap years excepting
450+
the 400 year leap years. But only if you're using the Gregorian
451+
calendar.
452+
"""
453+
if fmt is None:
454+
fmt = self.fmt
455+
456+
# Since python's time module's strftime implementation does not
457+
# support %f microsecond (but the datetime module does), use a
458+
# regular expression substitution to replace instances of %f.
459+
# Note that this can be useful since python's floating-point
460+
# precision representation for datetime causes precision to be
461+
# more accurate closer to year 0 (around the year 2000, precision
462+
# can be at 10s of microseconds).
463+
fmt = re.sub(r'((^|[^%])(%%)*)%f',
464+
r'\g<1>{0:06d}'.format(dt.microsecond), fmt)
439465

440466
year = dt.year
441467
# For every non-leap year century, advance by
@@ -444,26 +470,52 @@ def strftime(self, dt, fmt):
444470
off = 6 * (delta // 100 + delta // 400)
445471
year = year + off
446472

447-
# Move to around the year 2000
448-
year = year + ((2000 - year) // 28) * 28
473+
# Move to between the years 1973 and 2000
474+
year1 = year + ((2000 - year) // 28) * 28
475+
year2 = year1 + 28
449476
timetuple = dt.timetuple()
450-
s1 = time.strftime(fmt, (year,) + timetuple[1:])
451-
sites1 = self._findall(s1, str(year))
452-
453-
s2 = time.strftime(fmt, (year + 28,) + timetuple[1:])
454-
sites2 = self._findall(s2, str(year + 28))
455-
456-
sites = []
457-
for site in sites1:
458-
if site in sites2:
459-
sites.append(site)
460-
461-
s = s1
462-
syear = "%4d" % (dt.year,)
463-
for site in sites:
464-
s = s[:site] + syear + s[site + 4:]
477+
# Generate timestamp string for year and year+28
478+
s1 = time.strftime(fmt, (year1,) + timetuple[1:])
479+
s2 = time.strftime(fmt, (year2,) + timetuple[1:])
480+
481+
# Replace instances of respective years (both 2-digit and 4-digit)
482+
# that are located at the same indexes of s1, s2 with dt's year.
483+
# Note that C++'s strftime implementation does not use padded
484+
# zeros or padded whitespace for %y or %Y for years before 100, but
485+
# uses padded zeros for %x. (For example, try the runnable examples
486+
# with .tm_year in the interval [-1900, -1800] on
487+
# http://en.cppreference.com/w/c/chrono/strftime.) For ease of
488+
# implementation, we always use padded zeros for %y, %Y, and %x.
489+
s1, s2 = self._replace_common_substr(s1, s2,
490+
"{0:04d}".format(year1),
491+
"{0:04d}".format(year2),
492+
"{0:04d}".format(dt.year))
493+
s1, s2 = self._replace_common_substr(s1, s2,
494+
"{0:02d}".format(year1 % 100),
495+
"{0:02d}".format(year2 % 100),
496+
"{0:02d}".format(dt.year % 100))
497+
return cbook.unicode_safe(s1)
498+
499+
def strftime(self, dt, fmt=None):
500+
"""Refer to documentation for datetime.strftime.
501+
502+
*fmt* is a :func:`strftime` format string.
503+
504+
Warning: For years before 1900, depending upon the current
505+
locale it is possible that the year displayed with %x might
506+
be incorrect. For years before 100, %y and %Y will yield
507+
zero-padded strings.
508+
"""
509+
if fmt is None:
510+
fmt = self.fmt
511+
fmt = self.illegal_s.sub(r"\1", fmt)
512+
fmt = fmt.replace("%s", "s")
513+
if dt.year >= 1900:
514+
# Note: in python 3.3 this is okay for years >= 1000,
515+
# refer to http://bugs.python.org/issue177742
516+
return cbook.unicode_safe(dt.strftime(fmt))
465517

466-
return cbook.unicode_safe(s)
518+
return self.strftime_pre_1900(dt, fmt)
467519

468520

469521
class IndexDateFormatter(ticker.Formatter):

lib/matplotlib/tests/test_dates.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,55 @@ 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+
as well as a few related issues for years before 1900.
167+
"""
168+
def test_strftime_fields(dt):
169+
"""For datetime object dt, check DateFormatter fields"""
170+
# Note: the last couple of %%s are to check multiple %s are handled
171+
# properly; %% should get replaced by %.
172+
formatter = mdates.DateFormatter("%w %d %m %y %Y %H %I %M %S %%%f %%x")
173+
# Compute date fields without using datetime.strftime,
174+
# since datetime.strftime does not work before year 1900
175+
formatted_date_str = (
176+
"{weekday} {day:02d} {month:02d} {year:02d} {full_year:04d} "
177+
"{hour24:02d} {hour12:02d} {minute:02d} {second:02d} "
178+
"%{microsecond:06d} %x"
179+
.format(
180+
# weeknum=dt.isocalendar()[1], # %U/%W {weeknum:02d}
181+
# %w Sunday=0, weekday() Monday=0
182+
weekday=str((dt.weekday() + 1) % 7),
183+
day=dt.day,
184+
month=dt.month,
185+
year=dt.year % 100,
186+
full_year=dt.year,
187+
hour24=dt.hour,
188+
hour12=((dt.hour-1) % 12) + 1,
189+
minute=dt.minute,
190+
second=dt.second,
191+
microsecond=dt.microsecond))
192+
assert_equal(formatter.strftime(dt), formatted_date_str)
193+
194+
try:
195+
# Test strftime("%x") with the current locale.
196+
import locale # Might not exist on some platforms, such as Windows
197+
locale_formatter = mdates.DateFormatter("%x")
198+
locale_d_fmt = locale.nl_langinfo(locale.D_FMT)
199+
expanded_formatter = mdates.DateFormatter(locale_d_fmt)
200+
assert_equal(locale_formatter.strftime(dt),
201+
expanded_formatter.strftime(dt))
202+
except ImportError:
203+
pass
204+
205+
for year in range(1, 3000, 71):
206+
# Iterate through random set of years
207+
test_strftime_fields(datetime.datetime(year, 1, 1))
208+
test_strftime_fields(datetime.datetime(year, 2, 3, 4, 5, 6, 12345))
209+
210+
162211
def test_date_formatter_callable():
163212
scale = -11
164213
locator = mock.Mock(_get_unit=mock.Mock(return_value=scale))

0 commit comments

Comments
 (0)