r''' Universal time related module.
.. codeauthor:: Yoshifumi Futaana
.. warning::
Version 3.3.0 of ``matplotlib`` changed the ``num2date`` functionality
in a backward incompatible way.
https://matplotlib.org/3.3.1/api/dates_api.html
This influence the time handling in this module.
Upgrade ``irfpy.util`` to v4.6 or higher.
.. code-block:: sh
pip install --find-links=https://irfpy.irf.se/sdist irfpy.util -U
The module has functionalities of:
- conversion among many formats in the universal time
- production of regularly gridded time interval
**Conversion**
This contais functionality of connverting among UTC-epoch,
datetime, Julday, and matlab time.
You may also be interested in Matplotlib's package ``matplotlib.dates``
where several similar functions are defined.
.. table:: Conversions
================= ================= =============== ================================== ============= ================ ============
From \ To String expression UTC epoch datetime.datetime julday.Julday matplotlib float matlab float
================= ================= =============== ================================== ============= ================ ============
String expression dateutil.parser.parse()
UTC epoch dateutime.datetime.fromtimestamp()
datetime.datetime obj.strftime() obj.timestamp()
julday.Julday
matplotlib float
matlab float
================= ================= =============== ================================== ============= ================ ============
.. todo::
Fill in the table specifying the conversion of times.
.. todo::
Refactor the functions to coordinate the consistency in the function names for time conversion.
**Regular gridding**
- :func:`irfpy.util.utc.dtrange`: Return a list of equally-gridded datetime objects
- :func:`irfpy.util.utc.trange`: Return a generator of equally-gridded datetime objects
'''
import datetime
import calendar
import logging
_logger = logging.getLogger(__name__)
import dateutil.parser
import numpy as _np
import matplotlib.dates as mpldates
from irfpy.util import julday
class _EpochCorrector:
""" Epoch definition changed in matplotlib 3.3.0.
The correction is needed to seeminglessly handles the difference.
"""
def __init__(self):
classical_epoch1 = datetime.datetime(1, 1, 1) # The classical definition of epoch for 1
self.classical_epoch1_number = mpldates.date2num(classical_epoch1) # The epoch number for 0001-01-01:
_logger.debug('Classical: {}'.format(self.classical_epoch1_number))
# Obtain the epoch date in the running system.
try:
_mplepoch = mpldates.get_epoch() # This method was implemented in matplotlib v3.3.0
_epoch1 = dateutil.parser.parse(_mplepoch) + datetime.timedelta(days=1) # This is the system epoch (n=1)
except AttributeError: # If using matplotlib <3.3.0
_epoch1 = datetime.datetime(1, 1, 1) # The classical definition
# Epoch date of the current system represented by the classical system
self.current_epoch1_number = mpldates.date2num(_epoch1)
_logger.debug('Current: {}'.format(self.current_epoch1_number))
# Offset in between. Usually, either 0 or 719613
self.offset = self.current_epoch1_number - self.classical_epoch1_number
_logger.debug('Offset: {:.20f}'.format(self.offset))
# Offset in between in timedelta object. Either 0 or 719613 days usually.
self.offset_timedelta = _epoch1 - classical_epoch1
_logger.debug('Offset: {}'.format(self.offset_timedelta))
def num2date(self, number):
""" Convert the days from the epoch to the datetime object.
The epoch is always 0001-01-01 (January 1st, year 1 AD), and the number is in days, but with 1 added.
Note that the epoch in matplotlib's num2date is configurable after v3.3.0.
This method can be used regardless of the setting.
:param number: Days after the epoch (0001-01-01). It should be 1 or more.
:returns: The ``datetime.datetime`` object corresponding to the number
>>> print(num2date(1))
0001-01-01 00:00:00+00:00
"""
return _np.array(mpldates.num2date(number)) - self.offset_timedelta
def date2num(self, dt):
""" Convert the days from the epoch to the datetime object.
The epoch is always 0001-01-01 (January 1st, year 1 AD), and the number is in days, but with 1 added.
Note that the epoch in matplotlib's num2date is configurable after v3.3.0.
This method can be used regardless of the setting.
:param dt: The ``datetime.datetime`` object
:param number: Days after the epoch (0001-01-01) plus 1. It should be 1 or more.
>>> print(date2num(datetime.datetime(1, 1, 1)))
1.0
>>> t = datetime.datetime(2006, 12, 2, 15, 30, 0)
>>> print(date2num(t)) # doctest: +ELLIPSIS
732647.6458...
>>> print(num2date(date2num(t)))
2006-12-02 15:30:00+00:00
"""
return mpldates.date2num(dt) + self.offset
_epoch_corrector = _EpochCorrector()
num2date = _epoch_corrector.num2date
date2num = _epoch_corrector.date2num
[docs]def datestr2mplnum(string_date):
""" Date expression of string to maplotlib.dates float
It may be similar to :func:`matplotlib.dates.datestr2num`
but when used in np.genfromtxt converter, the datestr2num
fails due to the toordinary conversion exception.
.. warning::
After v3.3.0 of Matplotlib, the matplotlib epoch was
changed and configurable.
The example below is for pre-3.3.0 Matplotlib behavior.
>>> s = '2014-10-30T15:27:13.332'
>>> n1 = datestr2mplnum(s)
>>> print(n1) # doctest: +SKIP
735536.6439043055
>>> n2 = mpldates.datestr2num(s)
>>> print(n2) # doctest: +SKIP
735536.6439043055
"""
dd = dateutil.parser.parse(string_date)
n = mpldates.date2num(dd)
return n
[docs]def dt2tt(dt):
'''Convert datetime object to the UNIX epoch time
>>> dt2tt(datetime.datetime(1970, 1, 1))
0.0
'''
return calendar.timegm(dt.utctimetuple()) + dt.microsecond / 1e6
[docs]def tt2dt(tt):
'''Convert the UNIX epoch time to datetime instance
>>> print(tt2dt(0.0))
1970-01-01 00:00:00
'''
return datetime.datetime.utcfromtimestamp(tt)
[docs]def dt2jd(dt):
'''Convert datetime object to irfpy.util.julday.Julday object.
>>> jd = dt2jd(datetime.datetime(1970, 1, 1))
>>> print(jd)
2440587.50000(1970-01-01T00:00:00.000)
'''
return julday.Julday(dt.year, dt.month, dt.day, dt.hour, dt.minute,
dt.second + dt.microsecond / 1e6)
[docs]def jd2dt(jd):
'''Convert irfpy.util.julday.Julday object to datetime.datetime object
>>> from irfpy.util import julday
>>> jd = julday.Julday(1970, 1, 1, 0, 0, 0.0)
>>> print(jd)
2440587.50000(1970-01-01T00:00:00.000)
>>> print(jd2dt(jd))
1970-01-01 00:00:00
'''
return jd.getDatetime()
[docs]def tt2jd(tt):
""" Convert the UNIX epoch time to Julius day
>>> print(tt2jd(0))
2440587.50000(1970-01-01T00:00:00.000)
"""
dt = tt2dt(tt)
return dt2jd(dt)
[docs]def jd2tt(jd):
""" Convert the julian day to UNIX epoch time
>>> from irfpy.util import julday
>>> jd = julday.Julday(1970, 1, 1, 0, 0, 0.0)
>>> print(jd)
2440587.50000(1970-01-01T00:00:00.000)
>>> print(jd2tt(jd))
0.0
"""
dt = jd2dt(jd)
return dt2tt(dt)
[docs]def mat2dt(matlabtime, remove_tz=True):
''' Convert matlab floating time to ``datetime.datetime`` instance.
:param matlabtime: Matlab time.
:type matlabtime: ``float``
:param remove_tz: Time zone (UTC) is removed if set True.
Keep as it was if set False. ``num2data`` function add `tzinfo`
as UTC, while most of the ``irfpy`` library does not.
So by default, the information will be removed.
:type remove_tz: ``boolean``
Matlab time is a ``float`` and similar to Julian day, but offset is
different. Also ``matplotlib`` has different offset.
Thus, this method will convert Matlab time to ``datatime.datetime``
instance.
To convert ``matplotlib`` time to ``datetime.datetime``, you can use
``matplotlib.dates.num2date`` method.
:param matlabtime: Matlab time
:returns: ``datetime.datetime``
>>> from irfpy.util.utc import num2date, date2num
>>> mattime = 733408 # corresponding to 2008-01-01 00:00:00
>>> pytime = mat2dt(mattime)
>>> print(pytime.strftime('%F %T'))
2008-01-01 00:00:00
>>> print(pytime.strftime('%F %T %Z').strip())
2008-01-01 00:00:00
>>> pytime2 = mat2dt(mattime, remove_tz=False)
>>> print(pytime2.strftime('%F %T %Z'))
2008-01-01 00:00:00 UTC
>>> mpltime = date2num(pytime) # If you use matplotlib time, take care!
>>> print('%.1f' % mpltime) # This is different from mattime by 366 days.
733042.0
'''
dt = num2date(matlabtime - 366)
if remove_tz:
dt = dt.replace(tzinfo=None)
return dt
[docs]def dt2mat(dt):
''' Convert the datetime.datetime instance to matlab floating time.
Note that the function will return matlab time, not matplotlib time.
They have a difference by 366.
>>> t = datetime.datetime(2006, 12, 2, 15, 30, 0)
>>> tmat = dt2mat(t)
>>> print(tmat) # doctest: +ELLIPSIS
733013.645833...
>>> trev = mat2dt(tmat)
>>> print(trev)
2006-12-02 15:30:00
If you use date2num in matplotlib.dates module, you will get 366 day difference.
This is matplotlib time.
>>> print(date2num(t)) # doctest: +ELLIPSIS
732647.645833...
'''
num = date2num(dt) + 366
return num
[docs]def convert(intime, outfmt=float):
'''Convert between irfpy.util.julday.Julday, datetime.datetime, and time_t.
:param intime: Time in supported format.
:param outfmt: Specify the format. irfpy.util.julday.Julday, datetime,
and time_t(float) is supported. Default is time_t.
Supporeted time format is ``irfpy.util.julday.Julday``,
``datetime.datetime`` and time_t (=float).
Matplotlib's number (also float) is *NOT* supported.
'''
if isinstance(intime, julday.Julday):
if outfmt == julday.Julday:
return intime
elif outfmt == datetime.datetime:
return jd2dt(intime)
elif outfmt == float:
return jd2tt(intime)
else:
raise RuntimeError('Format unknown: %s' % outfmt)
elif isinstance(intime, datetime.datetime):
if outfmt == julday.Julday:
return dt2jd(intime)
elif outfmt == datetime.datetime:
return intime
elif outfmt == float:
return dt2tt(intime)
else:
raise RuntimeError('Format unknown: %s' % outfmt)
else:
### Try to interpret as a time_t.
try:
intime_flt = float(intime)
except TypeError as e:
raise RuntimeError('Input Format unknown: %s' % intime.__class__)
if outfmt == julday.Julday:
return tt2jd(intime_flt)
elif outfmt == datetime.datetime:
return tt2dt(intime_flt)
elif outfmt == float:
return intime
else:
raise RuntimeError('Format unknown: %s' % outfmt)
[docs]def ymd2doy(year, month, day):
''' Convert the calendar time to day of year.
'''
origin = datetime.date(year, 1, 1)
oneday = datetime.timedelta(days=1)
origin = origin
target = datetime.date(year, month, day)
_logger.debug('Date of the day %s, origin of the year %s' %
(target, origin))
diff = target - origin
return diff.days + 1
[docs]def doy2ymd(year, doi):
''' Convert the day of year to the calendar day as a tuple of (month, day).
If the speicified doy does not exist for the specified year,
ValueError is raised.
'''
origin = datetime.date(year, 1, 1)
days = datetime.timedelta(days=(doi - 1))
result = origin + days
if result.year != year:
msg = 'Given doi (%d) does not exist for the year %d' % (doi, year)
_logger.error(msg)
raise ValueError(msg)
return result.month, result.day
[docs]def dtrange(t0, t1, dt):
""" Return the equally stepped datetime object.
Return the equally separated datatime objects
between t0 and t1 (exclusive) with a step of dt.
Indeed, this is an alias to :func:`irfpy.util.timeseries.dtrange`.
>>> t0 = datetime.datetime(2014, 10, 1)
>>> t1 = datetime.datetime(2014, 10, 10)
>>> dt = datetime.timedelta(hours=12)
>>> tlist = dtrange(t0, t1, dt)
>>> print(len(tlist))
18
>>> print(tlist[5])
2014-10-03 12:00:00
>>> print(tlist[-1])
2014-10-09 12:00:00
"""
from irfpy.util import timeseries
return timeseries.dtrange(t0, t1, dt)
[docs]def dtlinspace(t0, t1, ndiv):
""" Return the equally separated datetime objects.
:param t0: Start time
:param t1: Stop time
:param ndiv: Number of elements
:return: A list of datetime object
"""
dt = (t1 - t0).total_seconds()
dtlist = [t0 + datetime.timedelta(seconds=dt) * (i / (ndiv - 1)) for i in range(ndiv)]
return dtlist
[docs]def trange(t0, t1, dt):
""" Generator of equally separated datetime object.
>>> t0 = datetime.datetime(2014, 1, 1)
>>> t1 = datetime.datetime(2014, 1, 3)
>>> dt = datetime.timedelta(hours=12)
>>> for t in trange(t0, t1, dt):
... print(t)
2014-01-01 00:00:00
2014-01-01 12:00:00
2014-01-02 00:00:00
2014-01-02 12:00:00
Of course you can by enclosure.
>>> tlist = [t.strftime('%FT%T') for t in trange(t0, t1, dt)]
>>> print(tlist)
['2014-01-01T00:00:00', '2014-01-01T12:00:00', '2014-01-02T00:00:00', '2014-01-02T12:00:00']
"""
t = t0
while t < t1:
yield t
t += dt
[docs]def round_sec(t):
""" Return the rounded time to the resolution of second.
>>> t = datetime.datetime(2010, 3, 10, 5, 10, 25, 499999)
>>> print(round_sec(t))
2010-03-10 05:10:25
>>> t = datetime.datetime(2010, 3, 10, 5, 10, 25, 500000)
>>> print(round_sec(t))
2010-03-10 05:10:26
"""
if t.microsecond >= 500000:
_t = t.replace(microsecond=0) + datetime.timedelta(seconds=1)
else:
_t = t.replace(microsecond=0)
return _t