Source code for irfpy.util.utc

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