From 8e7f8cbfb4dc9def1029131d1e08addc2e2a2013 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 12 Jul 2017 10:04:18 +0100 Subject: [PATCH 1/3] Add num2timedelta method with test --- lib/matplotlib/dates.py | 32 ++++++++++++++++++++++++++++++ lib/matplotlib/tests/test_dates.py | 9 +++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 0d54c69f90e0..97216921d9b5 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -406,6 +406,38 @@ def num2date(x, tz=None): return _from_ordinalf_np_vectorized(x, tz).tolist() +def _ordinalf_to_timedelta(x): + return datetime.timedelta(days=x) + + +_ordinalf_to_timedelta_np_vectorized = np.vectorize(_ordinalf_to_timedelta) + + +def num2timedelta(x): + """ + Converts number of days to a :class:`timdelta` object. + If *x* is a sequence, a sequence of :class:`timedelta` objects will + be returned. + + Parameters + ---------- + x : float, sequence of floats + Number of days (fraction part represents hours, minutes, seconds) + + Returns + ------- + :class:`timedelta` + + """ + if not cbook.iterable(x): + return _ordinalf_to_timedelta(x) + else: + x = np.asarray(x) + if not x.size: + return x + return _ordinalf_to_timedelta_np_vectorized(x).tolist() + + def drange(dstart, dend, delta): """ Return a date range as float Gregorian ordinals. *dstart* and diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index d87ba38edde2..5a25e6182b7e 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -457,3 +457,12 @@ def test_DayLocator(): def test_tz_utc(): dt = datetime.datetime(1970, 1, 1, tzinfo=mdates.UTC) dt.tzname() + + +@pytest.mark.parametrize("x, tdelta", + [(1, datetime.timedelta(days=1)), + ([1, 1.5], [datetime.timedelta(days=1), + datetime.timedelta(days=1.5)])]) +def test_num2timedelta(x, tdelta): + dt = mdates.num2timedelta(x) + assert dt == tdelta From 16d12408b921aa99c7bf2d3809b90db52d773e72 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 12 Jul 2017 22:05:55 +0100 Subject: [PATCH 2/3] Add timedelta formatter Fix whitespace --- lib/matplotlib/dates.py | 81 +++++++++++++++++++++++++++++- lib/matplotlib/tests/test_dates.py | 15 ++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 97216921d9b5..e27c278a2f2c 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -118,10 +118,10 @@ import time import math import datetime +import string import warnings - from dateutil.rrule import (rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY) @@ -217,6 +217,9 @@ def _to_ordinalf(dt): dt = dt.astimezone(UTC) tzi = UTC + if isinstance(dt, datetime.timedelta): + return dt.total_seconds() / SEC_PER_DAY + base = float(dt.toordinal()) # If it's sufficiently datetime-like, it will have a `date()` method @@ -605,6 +608,82 @@ def strftime(self, dt, fmt=None): return self.strftime_pre_1900(dt, fmt) +class TimedeltaFormatter(ticker.Formatter): + + def __init__(self, fmt): + r""" + *fmt* is a format string, with accepted format arguments given in the + table below. For more information on format strings see + https://docs.python.org/3/library/string.html#format-string-syntax + + .. table:: Accepted format arguments + :widths: auto + + ======== ======= + Argument Meaning + ======== ======= + {D} Days + {H} Hours + {M} Minutes + {S} Seconds + {f} Microseconds + ========= ======= + + Examples + -------- + >>> from datetime import timedelta + >>> from matplotlib.dates import TimedeltaFormatter + >>> + >>> dt = timedelta(hours=1, minutes=0, seconds=3) + >>> fmt = '{H:02}:{M:02}:{S:02}' + >>> formatter = TimedeltaFormatter(fmt) + >>> formatter(dt) + 01:00:03 + >>> + >>> fmt = '{S}' + >>> formatter = TimedeltaFormatter(fmt) + >>> formatter(dt) + 3603 + >>> + >>> fmt = '{S}' + >>> dt = timedelta(microseconds=1e5) + >>> formatter(dt) + 0.1 + """ + self.fmt = fmt + + def __call__(self, x): + dt = num2timedelta(x) + return self._strfdelta(dt) + + def _strfdelta(self, dt): + # A custom method to format timedelta objects. See above for examples + formatter = string.Formatter() + d = {} + allkeys = ['D', 'H', 'M', 'S', 'f'] + secs = [SEC_PER_DAY, SEC_PER_HOUR, SEC_PER_MIN, 1, 1 / 1e6] + # Get list of all keys in the format string + keys = list(map(lambda x: x[1], list(formatter.parse(self.fmt)))) + + rem = dt.total_seconds() + # Cycle through all keys, and if key present in format calculate value + # of that key + for key, seconds in zip(allkeys, secs): + if key in keys: + d[key] = rem / seconds + _, rem = divmod(rem, seconds) + + # Cycle through and round every entry down to an int APART from the + # smallest unit present in format + foundlast = False + for key in allkeys[::-1]: + if key in keys: + if foundlast or key == 'f': + d[key] = int(np.floor(d[key])) + foundlast = True + return formatter.format(self.fmt, **d) + + class IndexDateFormatter(ticker.Formatter): """ Use with :class:`~matplotlib.ticker.IndexLocator` to cycle format diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 5a25e6182b7e..53c5144411bf 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -466,3 +466,18 @@ def test_tz_utc(): def test_num2timedelta(x, tdelta): dt = mdates.num2timedelta(x) assert dt == tdelta + + +@pytest.mark.parametrize('dt, fmt, out', + [(datetime.timedelta(days=8, hours=1, minutes=2, + seconds=3, microseconds=4), + '{D}d {H:02}:{M:02}:{S:02}.{f:06}', + '8d 01:02:03.000004'), + (datetime.timedelta(seconds=1), '{f}', '1000000'), + (datetime.timedelta(microseconds=1e5), '{S}', '0.1'), + ]) +def test_TimedeltaFormatter(dt, fmt, out): + formatter = mdates.TimedeltaFormatter(fmt) + x = mdates.date2num(dt) + formatted = formatter(x) + assert formatted == out From fbf39008cbff17f526ce847404da3c834f97a2b7 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 25 Jul 2017 09:42:27 +0100 Subject: [PATCH 3/3] Add Timedeltaformatter to __all__ --- lib/matplotlib/dates.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index e27c278a2f2c..851157704e11 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -137,7 +137,7 @@ __all__ = ('date2num', 'num2date', 'drange', 'epoch2num', - 'num2epoch', 'mx2num', 'DateFormatter', + 'num2epoch', 'mx2num', 'TimedeltaFormatter', 'DateFormatter', 'IndexDateFormatter', 'AutoDateFormatter', 'DateLocator', 'RRuleLocator', 'AutoDateLocator', 'YearLocator', 'MonthLocator', 'WeekdayLocator', @@ -619,15 +619,15 @@ def __init__(self, fmt): .. table:: Accepted format arguments :widths: auto - ======== ======= + ======== ============ Argument Meaning - ======== ======= + ======== ============ {D} Days {H} Hours {M} Minutes {S} Seconds {f} Microseconds - ========= ======= + ======== ============ Examples --------