1# -*- coding: utf-8 -*-
2# Licensed under a 3-clause BSD style license - see LICENSE.rst
3import fnmatch
4import time
5import re
6import datetime
7import warnings
8from decimal import Decimal
9from collections import OrderedDict, defaultdict
10
11import numpy as np
12import erfa
13
14from astropy.utils.decorators import lazyproperty, classproperty
15from astropy.utils.exceptions import AstropyDeprecationWarning
16import astropy.units as u
17
18from . import _parse_times
19from . import utils
20from .utils import day_frac, quantity_day_frac, two_sum, two_product
21from . import conf
22
23__all__ = ['TimeFormat', 'TimeJD', 'TimeMJD', 'TimeFromEpoch', 'TimeUnix',
24           'TimeUnixTai', 'TimeCxcSec', 'TimeGPS', 'TimeDecimalYear',
25           'TimePlotDate', 'TimeUnique', 'TimeDatetime', 'TimeString',
26           'TimeISO', 'TimeISOT', 'TimeFITS', 'TimeYearDayTime',
27           'TimeEpochDate', 'TimeBesselianEpoch', 'TimeJulianEpoch',
28           'TimeDeltaFormat', 'TimeDeltaSec', 'TimeDeltaJD',
29           'TimeEpochDateString', 'TimeBesselianEpochString',
30           'TimeJulianEpochString', 'TIME_FORMATS', 'TIME_DELTA_FORMATS',
31           'TimezoneInfo', 'TimeDeltaDatetime', 'TimeDatetime64', 'TimeYMDHMS',
32           'TimeNumeric', 'TimeDeltaNumeric']
33
34__doctest_skip__ = ['TimePlotDate']
35
36# These both get filled in at end after TimeFormat subclasses defined.
37# Use an OrderedDict to fix the order in which formats are tried.
38# This ensures, e.g., that 'isot' gets tried before 'fits'.
39TIME_FORMATS = OrderedDict()
40TIME_DELTA_FORMATS = OrderedDict()
41
42# Translations between deprecated FITS timescales defined by
43# Rots et al. 2015, A&A 574:A36, and timescales used here.
44FITS_DEPRECATED_SCALES = {'TDT': 'tt', 'ET': 'tt',
45                          'GMT': 'utc', 'UT': 'utc', 'IAT': 'tai'}
46
47
48def _regexify_subfmts(subfmts):
49    """
50    Iterate through each of the sub-formats and try substituting simple
51    regular expressions for the strptime codes for year, month, day-of-month,
52    hour, minute, second.  If no % characters remain then turn the final string
53    into a compiled regex.  This assumes time formats do not have a % in them.
54
55    This is done both to speed up parsing of strings and to allow mixed formats
56    where strptime does not quite work well enough.
57    """
58    new_subfmts = []
59    for subfmt_tuple in subfmts:
60        subfmt_in = subfmt_tuple[1]
61        if isinstance(subfmt_in, str):
62            for strptime_code, regex in (('%Y', r'(?P<year>\d\d\d\d)'),
63                                         ('%m', r'(?P<mon>\d{1,2})'),
64                                         ('%d', r'(?P<mday>\d{1,2})'),
65                                         ('%H', r'(?P<hour>\d{1,2})'),
66                                         ('%M', r'(?P<min>\d{1,2})'),
67                                         ('%S', r'(?P<sec>\d{1,2})')):
68                subfmt_in = subfmt_in.replace(strptime_code, regex)
69
70            if '%' not in subfmt_in:
71                subfmt_tuple = (subfmt_tuple[0],
72                                re.compile(subfmt_in + '$'),
73                                subfmt_tuple[2])
74        new_subfmts.append(subfmt_tuple)
75
76    return tuple(new_subfmts)
77
78
79class TimeFormat:
80    """
81    Base class for time representations.
82
83    Parameters
84    ----------
85    val1 : numpy ndarray, list, number, str, or bytes
86        Values to initialize the time or times.  Bytes are decoded as ascii.
87    val2 : numpy ndarray, list, or number; optional
88        Value(s) to initialize the time or times.  Only used for numerical
89        input, to help preserve precision.
90    scale : str
91        Time scale of input value(s)
92    precision : int
93        Precision for seconds as floating point
94    in_subfmt : str
95        Select subformat for inputting string times
96    out_subfmt : str
97        Select subformat for outputting string times
98    from_jd : bool
99        If true then val1, val2 are jd1, jd2
100    """
101
102    _default_scale = 'utc'  # As of astropy 0.4
103    subfmts = ()
104    _registry = TIME_FORMATS
105
106    def __init__(self, val1, val2, scale, precision,
107                 in_subfmt, out_subfmt, from_jd=False):
108        self.scale = scale  # validation of scale done later with _check_scale
109        self.precision = precision
110        self.in_subfmt = in_subfmt
111        self.out_subfmt = out_subfmt
112
113        self._jd1, self._jd2 = None, None
114
115        if from_jd:
116            self.jd1 = val1
117            self.jd2 = val2
118        else:
119            val1, val2 = self._check_val_type(val1, val2)
120            self.set_jds(val1, val2)
121
122    def __init_subclass__(cls, **kwargs):
123        # Register time formats that define a name, but leave out astropy_time since
124        # it is not a user-accessible format and is only used for initialization into
125        # a different format.
126        if 'name' in cls.__dict__ and cls.name != 'astropy_time':
127            # FIXME: check here that we're not introducing a collision with
128            # an existing method or attribute; problem is it could be either
129            # astropy.time.Time or astropy.time.TimeDelta, and at the point
130            # where this is run neither of those classes have necessarily been
131            # constructed yet.
132            if 'value' in cls.__dict__ and not hasattr(cls.value, "fget"):
133                raise ValueError("If defined, 'value' must be a property")
134
135            cls._registry[cls.name] = cls
136
137        # If this class defines its own subfmts, preprocess the definitions.
138        if 'subfmts' in cls.__dict__:
139            cls.subfmts = _regexify_subfmts(cls.subfmts)
140
141        return super().__init_subclass__(**kwargs)
142
143    @classmethod
144    def _get_allowed_subfmt(cls, subfmt):
145        """Get an allowed subfmt for this class, either the input ``subfmt``
146        if this is valid or '*' as a default.  This method gets used in situations
147        where the format of an existing Time object is changing and so the
148        out_ or in_subfmt may need to be coerced to the default '*' if that
149        ``subfmt`` is no longer valid.
150        """
151        try:
152            cls._select_subfmts(subfmt)
153        except ValueError:
154            subfmt = '*'
155        return subfmt
156
157    @property
158    def in_subfmt(self):
159        return self._in_subfmt
160
161    @in_subfmt.setter
162    def in_subfmt(self, subfmt):
163        # Validate subfmt value for this class, raises ValueError if not.
164        self._select_subfmts(subfmt)
165        self._in_subfmt = subfmt
166
167    @property
168    def out_subfmt(self):
169        return self._out_subfmt
170
171    @out_subfmt.setter
172    def out_subfmt(self, subfmt):
173        # Validate subfmt value for this class, raises ValueError if not.
174        self._select_subfmts(subfmt)
175        self._out_subfmt = subfmt
176
177    @property
178    def jd1(self):
179        return self._jd1
180
181    @jd1.setter
182    def jd1(self, jd1):
183        self._jd1 = _validate_jd_for_storage(jd1)
184        if self._jd2 is not None:
185            self._jd1, self._jd2 = _broadcast_writeable(self._jd1, self._jd2)
186
187    @property
188    def jd2(self):
189        return self._jd2
190
191    @jd2.setter
192    def jd2(self, jd2):
193        self._jd2 = _validate_jd_for_storage(jd2)
194        if self._jd1 is not None:
195            self._jd1, self._jd2 = _broadcast_writeable(self._jd1, self._jd2)
196
197    def __len__(self):
198        return len(self.jd1)
199
200    @property
201    def scale(self):
202        """Time scale"""
203        self._scale = self._check_scale(self._scale)
204        return self._scale
205
206    @scale.setter
207    def scale(self, val):
208        self._scale = val
209
210    def mask_if_needed(self, value):
211        if self.masked:
212            value = np.ma.array(value, mask=self.mask, copy=False)
213        return value
214
215    @property
216    def mask(self):
217        if 'mask' not in self.cache:
218            self.cache['mask'] = np.isnan(self.jd2)
219            if self.cache['mask'].shape:
220                self.cache['mask'].flags.writeable = False
221        return self.cache['mask']
222
223    @property
224    def masked(self):
225        if 'masked' not in self.cache:
226            self.cache['masked'] = bool(np.any(self.mask))
227        return self.cache['masked']
228
229    @property
230    def jd2_filled(self):
231        return np.nan_to_num(self.jd2) if self.masked else self.jd2
232
233    @lazyproperty
234    def cache(self):
235        """
236        Return the cache associated with this instance.
237        """
238        return defaultdict(dict)
239
240    def _check_val_type(self, val1, val2):
241        """Input value validation, typically overridden by derived classes"""
242        # val1 cannot contain nan, but val2 can contain nan
243        isfinite1 = np.isfinite(val1)
244        if val1.size > 1:  # Calling .all() on a scalar is surprisingly slow
245            isfinite1 = isfinite1.all()  # Note: arr.all() about 3x faster than np.all(arr)
246        elif val1.size == 0:
247            isfinite1 = False
248        ok1 = (val1.dtype.kind == 'f' and val1.dtype.itemsize >= 8
249               and isfinite1 or val1.size == 0)
250        ok2 = val2 is None or (
251            val2.dtype.kind == 'f' and val2.dtype.itemsize >= 8
252            and not np.any(np.isinf(val2))) or val2.size == 0
253        if not (ok1 and ok2):
254            raise TypeError('Input values for {} class must be finite doubles'
255                            .format(self.name))
256
257        if getattr(val1, 'unit', None) is not None:
258            # Convert any quantity-likes to days first, attempting to be
259            # careful with the conversion, so that, e.g., large numbers of
260            # seconds get converted without losing precision because
261            # 1/86400 is not exactly representable as a float.
262            val1 = u.Quantity(val1, copy=False)
263            if val2 is not None:
264                val2 = u.Quantity(val2, copy=False)
265
266            try:
267                val1, val2 = quantity_day_frac(val1, val2)
268            except u.UnitsError:
269                raise u.UnitConversionError(
270                    "only quantities with time units can be "
271                    "used to instantiate Time instances.")
272            # We now have days, but the format may expect another unit.
273            # On purpose, multiply with 1./day_unit because typically it is
274            # 1./erfa.DAYSEC, and inverting it recovers the integer.
275            # (This conversion will get undone in format's set_jds, hence
276            # there may be room for optimizing this.)
277            factor = 1. / getattr(self, 'unit', 1.)
278            if factor != 1.:
279                val1, carry = two_product(val1, factor)
280                carry += val2 * factor
281                val1, val2 = two_sum(val1, carry)
282
283        elif getattr(val2, 'unit', None) is not None:
284            raise TypeError('Cannot mix float and Quantity inputs')
285
286        if val2 is None:
287            val2 = np.array(0, dtype=val1.dtype)
288
289        def asarray_or_scalar(val):
290            """
291            Remove ndarray subclasses since for jd1/jd2 we want a pure ndarray
292            or a Python or numpy scalar.
293            """
294            return np.asarray(val) if isinstance(val, np.ndarray) else val
295
296        return asarray_or_scalar(val1), asarray_or_scalar(val2)
297
298    def _check_scale(self, scale):
299        """
300        Return a validated scale value.
301
302        If there is a class attribute 'scale' then that defines the default /
303        required time scale for this format.  In this case if a scale value was
304        provided that needs to match the class default, otherwise return
305        the class default.
306
307        Otherwise just make sure that scale is in the allowed list of
308        scales.  Provide a different error message if `None` (no value) was
309        supplied.
310        """
311        if scale is None:
312            scale = self._default_scale
313
314        if scale not in TIME_SCALES:
315            raise ScaleValueError("Scale value '{}' not in "
316                                  "allowed values {}"
317                                  .format(scale, TIME_SCALES))
318
319        return scale
320
321    def set_jds(self, val1, val2):
322        """
323        Set internal jd1 and jd2 from val1 and val2.  Must be provided
324        by derived classes.
325        """
326        raise NotImplementedError
327
328    def to_value(self, parent=None, out_subfmt=None):
329        """
330        Return time representation from internal jd1 and jd2 in specified
331        ``out_subfmt``.
332
333        This is the base method that ignores ``parent`` and uses the ``value``
334        property to compute the output. This is done by temporarily setting
335        ``self.out_subfmt`` and calling ``self.value``. This is required for
336        legacy Format subclasses prior to astropy 4.0  New code should instead
337        implement the value functionality in ``to_value()`` and then make the
338        ``value`` property be a simple call to ``self.to_value()``.
339
340        Parameters
341        ----------
342        parent : object
343            Parent `~astropy.time.Time` object associated with this
344            `~astropy.time.TimeFormat` object
345        out_subfmt : str or None
346            Output subformt (use existing self.out_subfmt if `None`)
347
348        Returns
349        -------
350        value : numpy.array, numpy.ma.array
351            Array or masked array of formatted time representation values
352        """
353        # Get value via ``value`` property, overriding out_subfmt temporarily if needed.
354        if out_subfmt is not None:
355            out_subfmt_orig = self.out_subfmt
356            try:
357                self.out_subfmt = out_subfmt
358                value = self.value
359            finally:
360                self.out_subfmt = out_subfmt_orig
361        else:
362            value = self.value
363
364        return self.mask_if_needed(value)
365
366    @property
367    def value(self):
368        raise NotImplementedError
369
370    @classmethod
371    def _select_subfmts(cls, pattern):
372        """
373        Return a list of subformats where name matches ``pattern`` using
374        fnmatch.
375
376        If no subformat matches pattern then a ValueError is raised.  A special
377        case is a format with no allowed subformats, i.e. subfmts=(), and
378        pattern='*'.  This is OK and happens when this method is used for
379        validation of an out_subfmt.
380        """
381        if not isinstance(pattern, str):
382            raise ValueError('subfmt attribute must be a string')
383
384        subfmts = [x for x in cls.subfmts if fnmatch.fnmatchcase(x[0], pattern)]
385        if len(subfmts) == 0 and pattern != '*':
386            if len(cls.subfmts) == 0:
387                raise ValueError(f'subformat not allowed for format {cls.name}')
388            else:
389                subfmt_names = [x[0] for x in cls.subfmts]
390                raise ValueError(f'subformat {pattern!r} must match one of '
391                                 f'{subfmt_names} for format {cls.name}')
392
393        return subfmts
394
395
396class TimeNumeric(TimeFormat):
397    subfmts = (
398        ('float', np.float64, None, np.add),
399        ('long', np.longdouble, utils.longdouble_to_twoval,
400         utils.twoval_to_longdouble),
401        ('decimal', np.object_, utils.decimal_to_twoval,
402         utils.twoval_to_decimal),
403        ('str', np.str_, utils.decimal_to_twoval, utils.twoval_to_string),
404        ('bytes', np.bytes_, utils.bytes_to_twoval, utils.twoval_to_bytes),
405    )
406
407    def _check_val_type(self, val1, val2):
408        """Input value validation, typically overridden by derived classes"""
409        # Save original state of val2 because the super()._check_val_type below
410        # may change val2 from None to np.array(0). The value is saved in order
411        # to prevent a useless and slow call to np.result_type() below in the
412        # most common use-case of providing only val1.
413        orig_val2_is_none = val2 is None
414
415        if val1.dtype.kind == 'f':
416            val1, val2 = super()._check_val_type(val1, val2)
417        elif (not orig_val2_is_none
418              or not (val1.dtype.kind in 'US'
419                      or (val1.dtype.kind == 'O'
420                          and all(isinstance(v, Decimal) for v in val1.flat)))):
421            raise TypeError(
422                'for {} class, input should be doubles, string, or Decimal, '
423                'and second values are only allowed for doubles.'
424                .format(self.name))
425
426        val_dtype = (val1.dtype if orig_val2_is_none else
427                     np.result_type(val1.dtype, val2.dtype))
428        subfmts = self._select_subfmts(self.in_subfmt)
429        for subfmt, dtype, convert, _ in subfmts:
430            if np.issubdtype(val_dtype, dtype):
431                break
432        else:
433            raise ValueError('input type not among selected sub-formats.')
434
435        if convert is not None:
436            try:
437                val1, val2 = convert(val1, val2)
438            except Exception:
439                raise TypeError(
440                    'for {} class, input should be (long) doubles, string, '
441                    'or Decimal, and second values are only allowed for '
442                    '(long) doubles.'.format(self.name))
443
444        return val1, val2
445
446    def to_value(self, jd1=None, jd2=None, parent=None, out_subfmt=None):
447        """
448        Return time representation from internal jd1 and jd2.
449        Subclasses that require ``parent`` or to adjust the jds should
450        override this method.
451        """
452        # TODO: do this in __init_subclass__?
453        if self.__class__.value.fget is not self.__class__.to_value:
454            return self.value
455
456        if jd1 is None:
457            jd1 = self.jd1
458        if jd2 is None:
459            jd2 = self.jd2
460        if out_subfmt is None:
461            out_subfmt = self.out_subfmt
462        subfmt = self._select_subfmts(out_subfmt)[0]
463        kwargs = {}
464        if subfmt[0] in ('str', 'bytes'):
465            unit = getattr(self, 'unit', 1)
466            digits = int(np.ceil(np.log10(unit / np.finfo(float).eps)))
467            # TODO: allow a way to override the format.
468            kwargs['fmt'] = f'.{digits}f'
469        value = subfmt[3](jd1, jd2, **kwargs)
470        return self.mask_if_needed(value)
471
472    value = property(to_value)
473
474
475class TimeJD(TimeNumeric):
476    """
477    Julian Date time format.
478    This represents the number of days since the beginning of
479    the Julian Period.
480    For example, 2451544.5 in JD is midnight on January 1, 2000.
481    """
482    name = 'jd'
483
484    def set_jds(self, val1, val2):
485        self._check_scale(self._scale)  # Validate scale.
486        self.jd1, self.jd2 = day_frac(val1, val2)
487
488
489class TimeMJD(TimeNumeric):
490    """
491    Modified Julian Date time format.
492    This represents the number of days since midnight on November 17, 1858.
493    For example, 51544.0 in MJD is midnight on January 1, 2000.
494    """
495    name = 'mjd'
496
497    def set_jds(self, val1, val2):
498        self._check_scale(self._scale)  # Validate scale.
499        jd1, jd2 = day_frac(val1, val2)
500        jd1 += erfa.DJM0  # erfa.DJM0=2400000.5 (from erfam.h).
501        self.jd1, self.jd2 = day_frac(jd1, jd2)
502
503    def to_value(self, **kwargs):
504        jd1 = self.jd1 - erfa.DJM0  # This cannot lose precision.
505        jd2 = self.jd2
506        return super().to_value(jd1=jd1, jd2=jd2, **kwargs)
507
508    value = property(to_value)
509
510
511class TimeDecimalYear(TimeNumeric):
512    """
513    Time as a decimal year, with integer values corresponding to midnight
514    of the first day of each year.  For example 2000.5 corresponds to the
515    ISO time '2000-07-02 00:00:00'.
516    """
517    name = 'decimalyear'
518
519    def set_jds(self, val1, val2):
520        self._check_scale(self._scale)  # Validate scale.
521
522        sum12, err12 = two_sum(val1, val2)
523        iy_start = np.trunc(sum12).astype(int)
524        extra, y_frac = two_sum(sum12, -iy_start)
525        y_frac += extra + err12
526
527        val = (val1 + val2).astype(np.double)
528        iy_start = np.trunc(val).astype(int)
529
530        imon = np.ones_like(iy_start)
531        iday = np.ones_like(iy_start)
532        ihr = np.zeros_like(iy_start)
533        imin = np.zeros_like(iy_start)
534        isec = np.zeros_like(y_frac)
535
536        # Possible enhancement: use np.unique to only compute start, stop
537        # for unique values of iy_start.
538        scale = self.scale.upper().encode('ascii')
539        jd1_start, jd2_start = erfa.dtf2d(scale, iy_start, imon, iday,
540                                          ihr, imin, isec)
541        jd1_end, jd2_end = erfa.dtf2d(scale, iy_start + 1, imon, iday,
542                                      ihr, imin, isec)
543
544        t_start = Time(jd1_start, jd2_start, scale=self.scale, format='jd')
545        t_end = Time(jd1_end, jd2_end, scale=self.scale, format='jd')
546        t_frac = t_start + (t_end - t_start) * y_frac
547
548        self.jd1, self.jd2 = day_frac(t_frac.jd1, t_frac.jd2)
549
550    def to_value(self, **kwargs):
551        scale = self.scale.upper().encode('ascii')
552        iy_start, ims, ids, ihmsfs = erfa.d2dtf(scale, 0,  # precision=0
553                                                self.jd1, self.jd2_filled)
554        imon = np.ones_like(iy_start)
555        iday = np.ones_like(iy_start)
556        ihr = np.zeros_like(iy_start)
557        imin = np.zeros_like(iy_start)
558        isec = np.zeros_like(self.jd1)
559
560        # Possible enhancement: use np.unique to only compute start, stop
561        # for unique values of iy_start.
562        scale = self.scale.upper().encode('ascii')
563        jd1_start, jd2_start = erfa.dtf2d(scale, iy_start, imon, iday,
564                                          ihr, imin, isec)
565        jd1_end, jd2_end = erfa.dtf2d(scale, iy_start + 1, imon, iday,
566                                      ihr, imin, isec)
567        # Trying to be precise, but more than float64 not useful.
568        dt = (self.jd1 - jd1_start) + (self.jd2 - jd2_start)
569        dt_end = (jd1_end - jd1_start) + (jd2_end - jd2_start)
570        decimalyear = iy_start + dt / dt_end
571
572        return super().to_value(jd1=decimalyear, jd2=np.float64(0.0), **kwargs)
573
574    value = property(to_value)
575
576
577class TimeFromEpoch(TimeNumeric):
578    """
579    Base class for times that represent the interval from a particular
580    epoch as a floating point multiple of a unit time interval (e.g. seconds
581    or days).
582    """
583
584    @classproperty(lazy=True)
585    def _epoch(cls):
586        # Ideally we would use `def epoch(cls)` here and not have the instance
587        # property below. However, this breaks the sphinx API docs generation
588        # in a way that was not resolved. See #10406 for details.
589        return Time(cls.epoch_val, cls.epoch_val2, scale=cls.epoch_scale,
590                    format=cls.epoch_format)
591
592    @property
593    def epoch(self):
594        """Reference epoch time from which the time interval is measured"""
595        return self._epoch
596
597    def set_jds(self, val1, val2):
598        """
599        Initialize the internal jd1 and jd2 attributes given val1 and val2.
600        For an TimeFromEpoch subclass like TimeUnix these will be floats giving
601        the effective seconds since an epoch time (e.g. 1970-01-01 00:00:00).
602        """
603        # Form new JDs based on epoch time + time from epoch (converted to JD).
604        # One subtlety that might not be obvious is that 1.000 Julian days in
605        # UTC can be 86400 or 86401 seconds.  For the TimeUnix format the
606        # assumption is that every day is exactly 86400 seconds, so this is, in
607        # principle, doing the math incorrectly, *except* that it matches the
608        # definition of Unix time which does not include leap seconds.
609
610        # note: use divisor=1./self.unit, since this is either 1 or 1/86400,
611        # and 1/86400 is not exactly representable as a float64, so multiplying
612        # by that will cause rounding errors. (But inverting it as a float64
613        # recovers the exact number)
614        day, frac = day_frac(val1, val2, divisor=1. / self.unit)
615
616        jd1 = self.epoch.jd1 + day
617        jd2 = self.epoch.jd2 + frac
618
619        # For the usual case that scale is the same as epoch_scale, we only need
620        # to ensure that abs(jd2) <= 0.5. Since abs(self.epoch.jd2) <= 0.5 and
621        # abs(frac) <= 0.5, we can do simple (fast) checks and arithmetic here
622        # without another call to day_frac(). Note also that `round(jd2.item())`
623        # is about 10x faster than `np.round(jd2)`` for a scalar.
624        if self.epoch.scale == self.scale:
625            jd1_extra = np.round(jd2) if jd2.shape else round(jd2.item())
626            jd1 += jd1_extra
627            jd2 -= jd1_extra
628
629            self.jd1, self.jd2 = jd1, jd2
630            return
631
632        # Create a temporary Time object corresponding to the new (jd1, jd2) in
633        # the epoch scale (e.g. UTC for TimeUnix) then convert that to the
634        # desired time scale for this object.
635        #
636        # A known limitation is that the transform from self.epoch_scale to
637        # self.scale cannot involve any metadata like lat or lon.
638        try:
639            tm = getattr(Time(jd1, jd2, scale=self.epoch_scale,
640                              format='jd'), self.scale)
641        except Exception as err:
642            raise ScaleValueError("Cannot convert from '{}' epoch scale '{}'"
643                                  "to specified scale '{}', got error:\n{}"
644                                  .format(self.name, self.epoch_scale,
645                                          self.scale, err)) from err
646
647        self.jd1, self.jd2 = day_frac(tm._time.jd1, tm._time.jd2)
648
649    def to_value(self, parent=None, **kwargs):
650        # Make sure that scale is the same as epoch scale so we can just
651        # subtract the epoch and convert
652        if self.scale != self.epoch_scale:
653            if parent is None:
654                raise ValueError('cannot compute value without parent Time object')
655            try:
656                tm = getattr(parent, self.epoch_scale)
657            except Exception as err:
658                raise ScaleValueError("Cannot convert from '{}' epoch scale '{}'"
659                                      "to specified scale '{}', got error:\n{}"
660                                      .format(self.name, self.epoch_scale,
661                                              self.scale, err)) from err
662
663            jd1, jd2 = tm._time.jd1, tm._time.jd2
664        else:
665            jd1, jd2 = self.jd1, self.jd2
666
667        # This factor is guaranteed to be exactly representable, which
668        # means time_from_epoch1 is calculated exactly.
669        factor = 1. / self.unit
670        time_from_epoch1 = (jd1 - self.epoch.jd1) * factor
671        time_from_epoch2 = (jd2 - self.epoch.jd2) * factor
672
673        return super().to_value(jd1=time_from_epoch1, jd2=time_from_epoch2, **kwargs)
674
675    value = property(to_value)
676
677    @property
678    def _default_scale(self):
679        return self.epoch_scale
680
681
682class TimeUnix(TimeFromEpoch):
683    """
684    Unix time (UTC): seconds from 1970-01-01 00:00:00 UTC, ignoring leap seconds.
685
686    For example, 946684800.0 in Unix time is midnight on January 1, 2000.
687
688    NOTE: this quantity is not exactly unix time and differs from the strict
689    POSIX definition by up to 1 second on days with a leap second.  POSIX
690    unix time actually jumps backward by 1 second at midnight on leap second
691    days while this class value is monotonically increasing at 86400 seconds
692    per UTC day.
693    """
694    name = 'unix'
695    unit = 1.0 / erfa.DAYSEC  # in days (1 day == 86400 seconds)
696    epoch_val = '1970-01-01 00:00:00'
697    epoch_val2 = None
698    epoch_scale = 'utc'
699    epoch_format = 'iso'
700
701
702class TimeUnixTai(TimeUnix):
703    """
704    Unix time (TAI): SI seconds elapsed since 1970-01-01 00:00:00 TAI (see caveats).
705
706    This will generally differ from standard (UTC) Unix time by the cumulative
707    integral number of leap seconds introduced into UTC since 1972-01-01 UTC
708    plus the initial offset of 10 seconds at that date.
709
710    This convention matches the definition of linux CLOCK_TAI
711    (https://www.cl.cam.ac.uk/~mgk25/posix-clocks.html),
712    and the Precision Time Protocol
713    (https://en.wikipedia.org/wiki/Precision_Time_Protocol), which
714    is also used by the White Rabbit protocol in High Energy Physics:
715    https://white-rabbit.web.cern.ch.
716
717    Caveats:
718
719    - Before 1972, fractional adjustments to UTC were made, so the difference
720      between ``unix`` and ``unix_tai`` time is no longer an integer.
721    - Because of the fractional adjustments, to be very precise, ``unix_tai``
722      is the number of seconds since ``1970-01-01 00:00:00 TAI`` or equivalently
723      ``1969-12-31 23:59:51.999918 UTC``.  The difference between TAI and UTC
724      at that epoch was 8.000082 sec.
725    - On the day of a positive leap second the difference between ``unix`` and
726      ``unix_tai`` times increases linearly through the day by 1.0. See also the
727      documentation for the `~astropy.time.TimeUnix` class.
728    - Negative leap seconds are possible, though none have been needed to date.
729
730    Examples
731    --------
732
733      >>> # get the current offset between TAI and UTC
734      >>> from astropy.time import Time
735      >>> t = Time('2020-01-01', scale='utc')
736      >>> t.unix_tai - t.unix
737      37.0
738
739      >>> # Before 1972, the offset between TAI and UTC was not integer
740      >>> t = Time('1970-01-01', scale='utc')
741      >>> t.unix_tai - t.unix  # doctest: +FLOAT_CMP
742      8.000082
743
744      >>> # Initial offset of 10 seconds in 1972
745      >>> t = Time('1972-01-01', scale='utc')
746      >>> t.unix_tai - t.unix
747      10.0
748    """
749    name = 'unix_tai'
750    epoch_val = '1970-01-01 00:00:00'
751    epoch_scale = 'tai'
752
753
754class TimeCxcSec(TimeFromEpoch):
755    """
756    Chandra X-ray Center seconds from 1998-01-01 00:00:00 TT.
757    For example, 63072064.184 is midnight on January 1, 2000.
758    """
759    name = 'cxcsec'
760    unit = 1.0 / erfa.DAYSEC  # in days (1 day == 86400 seconds)
761    epoch_val = '1998-01-01 00:00:00'
762    epoch_val2 = None
763    epoch_scale = 'tt'
764    epoch_format = 'iso'
765
766
767class TimeGPS(TimeFromEpoch):
768    """GPS time: seconds from 1980-01-06 00:00:00 UTC
769    For example, 630720013.0 is midnight on January 1, 2000.
770
771    Notes
772    =====
773    This implementation is strictly a representation of the number of seconds
774    (including leap seconds) since midnight UTC on 1980-01-06.  GPS can also be
775    considered as a time scale which is ahead of TAI by a fixed offset
776    (to within about 100 nanoseconds).
777
778    For details, see https://www.usno.navy.mil/USNO/time/gps/usno-gps-time-transfer
779    """
780    name = 'gps'
781    unit = 1.0 / erfa.DAYSEC  # in days (1 day == 86400 seconds)
782    epoch_val = '1980-01-06 00:00:19'
783    # above epoch is the same as Time('1980-01-06 00:00:00', scale='utc').tai
784    epoch_val2 = None
785    epoch_scale = 'tai'
786    epoch_format = 'iso'
787
788
789class TimePlotDate(TimeFromEpoch):
790    """
791    Matplotlib `~matplotlib.pyplot.plot_date` input:
792    1 + number of days from 0001-01-01 00:00:00 UTC
793
794    This can be used directly in the matplotlib `~matplotlib.pyplot.plot_date`
795    function::
796
797      >>> import matplotlib.pyplot as plt
798      >>> jyear = np.linspace(2000, 2001, 20)
799      >>> t = Time(jyear, format='jyear', scale='utc')
800      >>> plt.plot_date(t.plot_date, jyear)
801      >>> plt.gcf().autofmt_xdate()  # orient date labels at a slant
802      >>> plt.draw()
803
804    For example, 730120.0003703703 is midnight on January 1, 2000.
805    """
806    # This corresponds to the zero reference time for matplotlib plot_date().
807    # Note that TAI and UTC are equivalent at the reference time.
808    name = 'plot_date'
809    unit = 1.0
810    epoch_val = 1721424.5  # Time('0001-01-01 00:00:00', scale='tai').jd - 1
811    epoch_val2 = None
812    epoch_scale = 'utc'
813    epoch_format = 'jd'
814
815    @lazyproperty
816    def epoch(self):
817        """Reference epoch time from which the time interval is measured"""
818        try:
819            # Matplotlib >= 3.3 has a get_epoch() function
820            from matplotlib.dates import get_epoch
821        except ImportError:
822            # If no get_epoch() then the epoch is '0001-01-01'
823            _epoch = self._epoch
824        else:
825            # Get the matplotlib date epoch as an ISOT string in UTC
826            epoch_utc = get_epoch()
827            from erfa import ErfaWarning
828            with warnings.catch_warnings():
829                # Catch possible dubious year warnings from erfa
830                warnings.filterwarnings('ignore', category=ErfaWarning)
831                _epoch = Time(epoch_utc, scale='utc', format='isot')
832            _epoch.format = 'jd'
833
834        return _epoch
835
836
837class TimeStardate(TimeFromEpoch):
838    """
839    Stardate: date units from 2318-07-05 12:00:00 UTC.
840    For example, stardate 41153.7 is 00:52 on April 30, 2363.
841    See http://trekguide.com/Stardates.htm#TNG for calculations and reference points
842    """
843    name = 'stardate'
844    unit = 0.397766856  # Stardate units per day
845    epoch_val = '2318-07-05 11:00:00'  # Date and time of stardate 00000.00
846    epoch_val2 = None
847    epoch_scale = 'tai'
848    epoch_format = 'iso'
849
850
851class TimeUnique(TimeFormat):
852    """
853    Base class for time formats that can uniquely create a time object
854    without requiring an explicit format specifier.  This class does
855    nothing but provide inheritance to identify a class as unique.
856    """
857
858
859class TimeAstropyTime(TimeUnique):
860    """
861    Instantiate date from an Astropy Time object (or list thereof).
862
863    This is purely for instantiating from a Time object.  The output
864    format is the same as the first time instance.
865    """
866    name = 'astropy_time'
867
868    def __new__(cls, val1, val2, scale, precision,
869                in_subfmt, out_subfmt, from_jd=False):
870        """
871        Use __new__ instead of __init__ to output a class instance that
872        is the same as the class of the first Time object in the list.
873        """
874        val1_0 = val1.flat[0]
875        if not (isinstance(val1_0, Time) and all(type(val) is type(val1_0)
876                                                 for val in val1.flat)):
877            raise TypeError('Input values for {} class must all be same '
878                            'astropy Time type.'.format(cls.name))
879
880        if scale is None:
881            scale = val1_0.scale
882
883        if val1.shape:
884            vals = [getattr(val, scale)._time for val in val1]
885            jd1 = np.concatenate([np.atleast_1d(val.jd1) for val in vals])
886            jd2 = np.concatenate([np.atleast_1d(val.jd2) for val in vals])
887
888            # Collect individual location values and merge into a single location.
889            if any(tm.location is not None for tm in val1):
890                if any(tm.location is None for tm in val1):
891                    raise ValueError('cannot concatenate times unless all locations '
892                                     'are set or no locations are set')
893                locations = []
894                for tm in val1:
895                    location = np.broadcast_to(tm.location, tm._time.jd1.shape,
896                                               subok=True)
897                    locations.append(np.atleast_1d(location))
898
899                location = np.concatenate(locations)
900
901            else:
902                location = None
903        else:
904            val = getattr(val1_0, scale)._time
905            jd1, jd2 = val.jd1, val.jd2
906            location = val1_0.location
907
908        OutTimeFormat = val1_0._time.__class__
909        self = OutTimeFormat(jd1, jd2, scale, precision, in_subfmt, out_subfmt,
910                             from_jd=True)
911
912        # Make a temporary hidden attribute to transfer location back to the
913        # parent Time object where it needs to live.
914        self._location = location
915
916        return self
917
918
919class TimeDatetime(TimeUnique):
920    """
921    Represent date as Python standard library `~datetime.datetime` object
922
923    Example::
924
925      >>> from astropy.time import Time
926      >>> from datetime import datetime
927      >>> t = Time(datetime(2000, 1, 2, 12, 0, 0), scale='utc')
928      >>> t.iso
929      '2000-01-02 12:00:00.000'
930      >>> t.tt.datetime
931      datetime.datetime(2000, 1, 2, 12, 1, 4, 184000)
932    """
933    name = 'datetime'
934
935    def _check_val_type(self, val1, val2):
936        if not all(isinstance(val, datetime.datetime) for val in val1.flat):
937            raise TypeError('Input values for {} class must be '
938                            'datetime objects'.format(self.name))
939        if val2 is not None:
940            raise ValueError(
941                f'{self.name} objects do not accept a val2 but you provided {val2}')
942        return val1, None
943
944    def set_jds(self, val1, val2):
945        """Convert datetime object contained in val1 to jd1, jd2"""
946        # Iterate through the datetime objects, getting year, month, etc.
947        iterator = np.nditer([val1, None, None, None, None, None, None],
948                             flags=['refs_ok', 'zerosize_ok'],
949                             op_dtypes=[None] + 5*[np.intc] + [np.double])
950        for val, iy, im, id, ihr, imin, dsec in iterator:
951            dt = val.item()
952
953            if dt.tzinfo is not None:
954                dt = (dt - dt.utcoffset()).replace(tzinfo=None)
955
956            iy[...] = dt.year
957            im[...] = dt.month
958            id[...] = dt.day
959            ihr[...] = dt.hour
960            imin[...] = dt.minute
961            dsec[...] = dt.second + dt.microsecond / 1e6
962
963        jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'),
964                              *iterator.operands[1:])
965        self.jd1, self.jd2 = day_frac(jd1, jd2)
966
967    def to_value(self, timezone=None, parent=None, out_subfmt=None):
968        """
969        Convert to (potentially timezone-aware) `~datetime.datetime` object.
970
971        If ``timezone`` is not ``None``, return a timezone-aware datetime
972        object.
973
974        Parameters
975        ----------
976        timezone : {`~datetime.tzinfo`, None}, optional
977            If not `None`, return timezone-aware datetime.
978
979        Returns
980        -------
981        `~datetime.datetime`
982            If ``timezone`` is not ``None``, output will be timezone-aware.
983        """
984        if out_subfmt is not None:
985            # Out_subfmt not allowed for this format, so raise the standard
986            # exception by trying to validate the value.
987            self._select_subfmts(out_subfmt)
988
989        if timezone is not None:
990            if self._scale != 'utc':
991                raise ScaleValueError("scale is {}, must be 'utc' when timezone "
992                                      "is supplied.".format(self._scale))
993
994        # Rather than define a value property directly, we have a function,
995        # since we want to be able to pass in timezone information.
996        scale = self.scale.upper().encode('ascii')
997        iys, ims, ids, ihmsfs = erfa.d2dtf(scale, 6,  # 6 for microsec
998                                           self.jd1, self.jd2_filled)
999        ihrs = ihmsfs['h']
1000        imins = ihmsfs['m']
1001        isecs = ihmsfs['s']
1002        ifracs = ihmsfs['f']
1003        iterator = np.nditer([iys, ims, ids, ihrs, imins, isecs, ifracs, None],
1004                             flags=['refs_ok', 'zerosize_ok'],
1005                             op_dtypes=7*[None] + [object])
1006
1007        for iy, im, id, ihr, imin, isec, ifracsec, out in iterator:
1008            if isec >= 60:
1009                raise ValueError('Time {} is within a leap second but datetime '
1010                                 'does not support leap seconds'
1011                                 .format((iy, im, id, ihr, imin, isec, ifracsec)))
1012            if timezone is not None:
1013                out[...] = datetime.datetime(iy, im, id, ihr, imin, isec, ifracsec,
1014                                             tzinfo=TimezoneInfo()).astimezone(timezone)
1015            else:
1016                out[...] = datetime.datetime(iy, im, id, ihr, imin, isec, ifracsec)
1017
1018        return self.mask_if_needed(iterator.operands[-1])
1019
1020    value = property(to_value)
1021
1022
1023class TimeYMDHMS(TimeUnique):
1024    """
1025    ymdhms: A Time format to represent Time as year, month, day, hour,
1026    minute, second (thus the name ymdhms).
1027
1028    Acceptable inputs must have keys or column names in the "YMDHMS" set of
1029    ``year``, ``month``, ``day`` ``hour``, ``minute``, ``second``:
1030
1031    - Dict with keys in the YMDHMS set
1032    - NumPy structured array, record array or astropy Table, or single row
1033      of those types, with column names in the YMDHMS set
1034
1035    One can supply a subset of the YMDHMS values, for instance only 'year',
1036    'month', and 'day'.  Inputs have the following defaults::
1037
1038      'month': 1, 'day': 1, 'hour': 0, 'minute': 0, 'second': 0
1039
1040    When the input is supplied as a ``dict`` then each value can be either a
1041    scalar value or an array.  The values will be broadcast to a common shape.
1042
1043    Example::
1044
1045      >>> from astropy.time import Time
1046      >>> t = Time({'year': 2015, 'month': 2, 'day': 3,
1047      ...           'hour': 12, 'minute': 13, 'second': 14.567},
1048      ...           scale='utc')
1049      >>> t.iso
1050      '2015-02-03 12:13:14.567'
1051      >>> t.ymdhms.year
1052      2015
1053    """
1054    name = 'ymdhms'
1055
1056    def _check_val_type(self, val1, val2):
1057        """
1058        This checks inputs for the YMDHMS format.
1059
1060        It is bit more complex than most format checkers because of the flexible
1061        input that is allowed.  Also, it actually coerces ``val1`` into an appropriate
1062        dict of ndarrays that can be used easily by ``set_jds()``.  This is useful
1063        because it makes it easy to get default values in that routine.
1064
1065        Parameters
1066        ----------
1067        val1 : ndarray or None
1068        val2 : ndarray or None
1069
1070        Returns
1071        -------
1072        val1_as_dict, val2 : val1 as dict or None, val2 is always None
1073
1074        """
1075        if val2 is not None:
1076            raise ValueError('val2 must be None for ymdhms format')
1077
1078        ymdhms = ['year', 'month', 'day', 'hour', 'minute', 'second']
1079
1080        if val1.dtype.names:
1081            # Convert to a dict of ndarray
1082            val1_as_dict = {name: val1[name] for name in val1.dtype.names}
1083
1084        elif val1.shape == (0,):
1085            # Input was empty list [], so set to None and set_jds will handle this
1086            return None, None
1087
1088        elif (val1.dtype.kind == 'O'
1089              and val1.shape == ()
1090              and isinstance(val1.item(), dict)):
1091            # Code gets here for input as a dict.  The dict input
1092            # can be either scalar values or N-d arrays.
1093
1094            # Extract the item (which is a dict) and broadcast values to the
1095            # same shape here.
1096            names = val1.item().keys()
1097            values = val1.item().values()
1098            val1_as_dict = {name: value for name, value
1099                            in zip(names, np.broadcast_arrays(*values))}
1100
1101        else:
1102            raise ValueError('input must be dict or table-like')
1103
1104        # Check that the key names now are good.
1105        names = val1_as_dict.keys()
1106        required_names = ymdhms[:len(names)]
1107
1108        def comma_repr(vals):
1109            return ', '.join(repr(val) for val in vals)
1110
1111        bad_names = set(names) - set(ymdhms)
1112        if bad_names:
1113            raise ValueError(f'{comma_repr(bad_names)} not allowed as YMDHMS key name(s)')
1114
1115        if set(names) != set(required_names):
1116            raise ValueError(f'for {len(names)} input key names '
1117                             f'you must supply {comma_repr(required_names)}')
1118
1119        return val1_as_dict, val2
1120
1121    def set_jds(self, val1, val2):
1122        if val1 is None:
1123            # Input was empty list []
1124            jd1 = np.array([], dtype=np.float64)
1125            jd2 = np.array([], dtype=np.float64)
1126
1127        else:
1128            jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'),
1129                                  val1['year'],
1130                                  val1.get('month', 1),
1131                                  val1.get('day', 1),
1132                                  val1.get('hour', 0),
1133                                  val1.get('minute', 0),
1134                                  val1.get('second', 0))
1135
1136        self.jd1, self.jd2 = day_frac(jd1, jd2)
1137
1138    @property
1139    def value(self):
1140        scale = self.scale.upper().encode('ascii')
1141        iys, ims, ids, ihmsfs = erfa.d2dtf(scale, 9,
1142                                           self.jd1, self.jd2_filled)
1143
1144        out = np.empty(self.jd1.shape, dtype=[('year', 'i4'),
1145                                              ('month', 'i4'),
1146                                              ('day', 'i4'),
1147                                              ('hour', 'i4'),
1148                                              ('minute', 'i4'),
1149                                              ('second', 'f8')])
1150        out['year'] = iys
1151        out['month'] = ims
1152        out['day'] = ids
1153        out['hour'] = ihmsfs['h']
1154        out['minute'] = ihmsfs['m']
1155        out['second'] = ihmsfs['s'] + ihmsfs['f'] * 10**(-9)
1156        out = out.view(np.recarray)
1157
1158        return self.mask_if_needed(out)
1159
1160
1161class TimezoneInfo(datetime.tzinfo):
1162    """
1163    Subclass of the `~datetime.tzinfo` object, used in the
1164    to_datetime method to specify timezones.
1165
1166    It may be safer in most cases to use a timezone database package like
1167    pytz rather than defining your own timezones - this class is mainly
1168    a workaround for users without pytz.
1169    """
1170    @u.quantity_input(utc_offset=u.day, dst=u.day)
1171    def __init__(self, utc_offset=0 * u.day, dst=0 * u.day, tzname=None):
1172        """
1173        Parameters
1174        ----------
1175        utc_offset : `~astropy.units.Quantity`, optional
1176            Offset from UTC in days. Defaults to zero.
1177        dst : `~astropy.units.Quantity`, optional
1178            Daylight Savings Time offset in days. Defaults to zero
1179            (no daylight savings).
1180        tzname : str or None, optional
1181            Name of timezone
1182
1183        Examples
1184        --------
1185        >>> from datetime import datetime
1186        >>> from astropy.time import TimezoneInfo  # Specifies a timezone
1187        >>> import astropy.units as u
1188        >>> utc = TimezoneInfo()    # Defaults to UTC
1189        >>> utc_plus_one_hour = TimezoneInfo(utc_offset=1*u.hour)  # UTC+1
1190        >>> dt_aware = datetime(2000, 1, 1, 0, 0, 0, tzinfo=utc_plus_one_hour)
1191        >>> print(dt_aware)
1192        2000-01-01 00:00:00+01:00
1193        >>> print(dt_aware.astimezone(utc))
1194        1999-12-31 23:00:00+00:00
1195        """
1196        if utc_offset == 0 and dst == 0 and tzname is None:
1197            tzname = 'UTC'
1198        self._utcoffset = datetime.timedelta(utc_offset.to_value(u.day))
1199        self._tzname = tzname
1200        self._dst = datetime.timedelta(dst.to_value(u.day))
1201
1202    def utcoffset(self, dt):
1203        return self._utcoffset
1204
1205    def tzname(self, dt):
1206        return str(self._tzname)
1207
1208    def dst(self, dt):
1209        return self._dst
1210
1211
1212class TimeString(TimeUnique):
1213    """
1214    Base class for string-like time representations.
1215
1216    This class assumes that anything following the last decimal point to the
1217    right is a fraction of a second.
1218
1219    **Fast C-based parser**
1220
1221    Time format classes can take advantage of a fast C-based parser if the times
1222    are represented as fixed-format strings with year, month, day-of-month,
1223    hour, minute, second, OR year, day-of-year, hour, minute, second. This can
1224    be a factor of 20 or more faster than the pure Python parser.
1225
1226    Fixed format means that the components always have the same number of
1227    characters. The Python parser will accept ``2001-9-2`` as a date, but the C
1228    parser would require ``2001-09-02``.
1229
1230    A subclass in this case must define a class attribute ``fast_parser_pars``
1231    which is a `dict` with all of the keys below. An inherited attribute is not
1232    checked, only an attribute in the class ``__dict__``.
1233
1234    - ``delims`` (tuple of int): ASCII code for character at corresponding
1235      ``starts`` position (0 => no character)
1236
1237    - ``starts`` (tuple of int): position where component starts (including
1238      delimiter if present). Use -1 for the month component for format that use
1239      day of year.
1240
1241    - ``stops`` (tuple of int): position where component ends. Use -1 to
1242      continue to end of string, or for the month component for formats that use
1243      day of year.
1244
1245    - ``break_allowed`` (tuple of int): if true (1) then the time string can
1246          legally end just before the corresponding component (e.g. "2000-01-01"
1247          is a valid time but "2000-01-01 12" is not).
1248
1249    - ``has_day_of_year`` (int): 0 if dates have year, month, day; 1 if year,
1250      day-of-year
1251    """
1252
1253    def __init_subclass__(cls, **kwargs):
1254        if 'fast_parser_pars' in cls.__dict__:
1255            fpp = cls.fast_parser_pars
1256            fpp = np.array(list(zip(map(chr, fpp['delims']),
1257                                    fpp['starts'],
1258                                    fpp['stops'],
1259                                    fpp['break_allowed'])),
1260                           _parse_times.dt_pars)
1261            if cls.fast_parser_pars['has_day_of_year']:
1262                fpp['start'][1] = fpp['stop'][1] = -1
1263            cls._fast_parser = _parse_times.create_parser(fpp)
1264
1265        super().__init_subclass__(**kwargs)
1266
1267    def _check_val_type(self, val1, val2):
1268        if val1.dtype.kind not in ('S', 'U') and val1.size:
1269            raise TypeError(f'Input values for {self.name} class must be strings')
1270        if val2 is not None:
1271            raise ValueError(
1272                f'{self.name} objects do not accept a val2 but you provided {val2}')
1273        return val1, None
1274
1275    def parse_string(self, timestr, subfmts):
1276        """Read time from a single string, using a set of possible formats."""
1277        # Datetime components required for conversion to JD by ERFA, along
1278        # with the default values.
1279        components = ('year', 'mon', 'mday', 'hour', 'min', 'sec')
1280        defaults = (None, 1, 1, 0, 0, 0)
1281        # Assume that anything following "." on the right side is a
1282        # floating fraction of a second.
1283        try:
1284            idot = timestr.rindex('.')
1285        except Exception:
1286            fracsec = 0.0
1287        else:
1288            timestr, fracsec = timestr[:idot], timestr[idot:]
1289            fracsec = float(fracsec)
1290
1291        for _, strptime_fmt_or_regex, _ in subfmts:
1292            if isinstance(strptime_fmt_or_regex, str):
1293                try:
1294                    tm = time.strptime(timestr, strptime_fmt_or_regex)
1295                except ValueError:
1296                    continue
1297                else:
1298                    vals = [getattr(tm, 'tm_' + component)
1299                            for component in components]
1300
1301            else:
1302                tm = re.match(strptime_fmt_or_regex, timestr)
1303                if tm is None:
1304                    continue
1305                tm = tm.groupdict()
1306                vals = [int(tm.get(component, default)) for component, default
1307                        in zip(components, defaults)]
1308
1309            # Add fractional seconds
1310            vals[-1] = vals[-1] + fracsec
1311            return vals
1312        else:
1313            raise ValueError(f'Time {timestr} does not match {self.name} format')
1314
1315    def set_jds(self, val1, val2):
1316        """Parse the time strings contained in val1 and set jd1, jd2"""
1317        # If specific input subformat is required then use the Python parser.
1318        # Also do this if Time format class does not define `use_fast_parser` or
1319        # if the fast parser is entirely disabled. Note that `use_fast_parser`
1320        # is ignored for format classes that don't have a fast parser.
1321        if (self.in_subfmt != '*'
1322                or '_fast_parser' not in self.__class__.__dict__
1323                or conf.use_fast_parser == 'False'):
1324            jd1, jd2 = self.get_jds_python(val1, val2)
1325        else:
1326            try:
1327                jd1, jd2 = self.get_jds_fast(val1, val2)
1328            except Exception:
1329                # Fall through to the Python parser unless fast is forced.
1330                if conf.use_fast_parser == 'force':
1331                    raise
1332                else:
1333                    jd1, jd2 = self.get_jds_python(val1, val2)
1334
1335        self.jd1 = jd1
1336        self.jd2 = jd2
1337
1338    def get_jds_python(self, val1, val2):
1339        """Parse the time strings contained in val1 and get jd1, jd2"""
1340        # Select subformats based on current self.in_subfmt
1341        subfmts = self._select_subfmts(self.in_subfmt)
1342        # Be liberal in what we accept: convert bytes to ascii.
1343        # Here .item() is needed for arrays with entries of unequal length,
1344        # to strip trailing 0 bytes.
1345        to_string = (str if val1.dtype.kind == 'U' else
1346                     lambda x: str(x.item(), encoding='ascii'))
1347        iterator = np.nditer([val1, None, None, None, None, None, None],
1348                             flags=['zerosize_ok'],
1349                             op_dtypes=[None] + 5 * [np.intc] + [np.double])
1350        for val, iy, im, id, ihr, imin, dsec in iterator:
1351            val = to_string(val)
1352            iy[...], im[...], id[...], ihr[...], imin[...], dsec[...] = (
1353                self.parse_string(val, subfmts))
1354
1355        jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'),
1356                              *iterator.operands[1:])
1357        jd1, jd2 = day_frac(jd1, jd2)
1358
1359        return jd1, jd2
1360
1361    def get_jds_fast(self, val1, val2):
1362        """Use fast C parser to parse time strings in val1 and get jd1, jd2"""
1363        # Handle bytes or str input and convert to uint8.  We need to the
1364        # dtype _parse_times.dt_u1 instead of uint8, since otherwise it is
1365        # not possible to create a gufunc with structured dtype output.
1366        # See note about ufunc type resolver in pyerfa/erfa/ufunc.c.templ.
1367        if val1.dtype.kind == 'U':
1368            # Note: val1.astype('S') is *very* slow, so we check ourselves
1369            # that the input is pure ASCII.
1370            val1_uint32 = val1.view((np.uint32, val1.dtype.itemsize // 4))
1371            if np.any(val1_uint32 > 127):
1372                raise ValueError('input is not pure ASCII')
1373
1374            # It might be possible to avoid making a copy via astype with
1375            # cleverness in parse_times.c but leave that for another day.
1376            chars = val1_uint32.astype(_parse_times.dt_u1)
1377
1378        else:
1379            chars = val1.view((_parse_times.dt_u1, val1.dtype.itemsize))
1380
1381        # Call the fast parsing ufunc.
1382        time_struct = self._fast_parser(chars)
1383        jd1, jd2 = erfa.dtf2d(self.scale.upper().encode('ascii'),
1384                              time_struct['year'],
1385                              time_struct['month'],
1386                              time_struct['day'],
1387                              time_struct['hour'],
1388                              time_struct['minute'],
1389                              time_struct['second'])
1390        return day_frac(jd1, jd2)
1391
1392    def str_kwargs(self):
1393        """
1394        Generator that yields a dict of values corresponding to the
1395        calendar date and time for the internal JD values.
1396        """
1397        scale = self.scale.upper().encode('ascii'),
1398        iys, ims, ids, ihmsfs = erfa.d2dtf(scale, self.precision,
1399                                           self.jd1, self.jd2_filled)
1400
1401        # Get the str_fmt element of the first allowed output subformat
1402        _, _, str_fmt = self._select_subfmts(self.out_subfmt)[0]
1403
1404        yday = None
1405        has_yday = '{yday:' in str_fmt
1406
1407        ihrs = ihmsfs['h']
1408        imins = ihmsfs['m']
1409        isecs = ihmsfs['s']
1410        ifracs = ihmsfs['f']
1411        for iy, im, id, ihr, imin, isec, ifracsec in np.nditer(
1412                [iys, ims, ids, ihrs, imins, isecs, ifracs],
1413                flags=['zerosize_ok']):
1414            if has_yday:
1415                yday = datetime.datetime(iy, im, id).timetuple().tm_yday
1416
1417            yield {'year': int(iy), 'mon': int(im), 'day': int(id),
1418                   'hour': int(ihr), 'min': int(imin), 'sec': int(isec),
1419                   'fracsec': int(ifracsec), 'yday': yday}
1420
1421    def format_string(self, str_fmt, **kwargs):
1422        """Write time to a string using a given format.
1423
1424        By default, just interprets str_fmt as a format string,
1425        but subclasses can add to this.
1426        """
1427        return str_fmt.format(**kwargs)
1428
1429    @property
1430    def value(self):
1431        # Select the first available subformat based on current
1432        # self.out_subfmt
1433        subfmts = self._select_subfmts(self.out_subfmt)
1434        _, _, str_fmt = subfmts[0]
1435
1436        # TODO: fix this ugly hack
1437        if self.precision > 0 and str_fmt.endswith('{sec:02d}'):
1438            str_fmt += '.{fracsec:0' + str(self.precision) + 'd}'
1439
1440        # Try to optimize this later.  Can't pre-allocate because length of
1441        # output could change, e.g. year rolls from 999 to 1000.
1442        outs = []
1443        for kwargs in self.str_kwargs():
1444            outs.append(str(self.format_string(str_fmt, **kwargs)))
1445
1446        return np.array(outs).reshape(self.jd1.shape)
1447
1448
1449class TimeISO(TimeString):
1450    """
1451    ISO 8601 compliant date-time format "YYYY-MM-DD HH:MM:SS.sss...".
1452    For example, 2000-01-01 00:00:00.000 is midnight on January 1, 2000.
1453
1454    The allowed subformats are:
1455
1456    - 'date_hms': date + hours, mins, secs (and optional fractional secs)
1457    - 'date_hm': date + hours, mins
1458    - 'date': date
1459    """
1460
1461    name = 'iso'
1462    subfmts = (('date_hms',
1463                '%Y-%m-%d %H:%M:%S',
1464                # XXX To Do - use strftime for output ??
1465                '{year:d}-{mon:02d}-{day:02d} {hour:02d}:{min:02d}:{sec:02d}'),
1466               ('date_hm',
1467                '%Y-%m-%d %H:%M',
1468                '{year:d}-{mon:02d}-{day:02d} {hour:02d}:{min:02d}'),
1469               ('date',
1470                '%Y-%m-%d',
1471                '{year:d}-{mon:02d}-{day:02d}'))
1472
1473    # Define positions and starting delimiter for year, month, day, hour,
1474    # minute, seconds components of an ISO time. This is used by the fast
1475    # C-parser parse_ymdhms_times()
1476    #
1477    #  "2000-01-12 13:14:15.678"
1478    #   01234567890123456789012
1479    #   yyyy-mm-dd hh:mm:ss.fff
1480    # Parsed as ('yyyy', '-mm', '-dd', ' hh', ':mm', ':ss', '.fff')
1481    fast_parser_pars = dict(
1482        delims=(0, ord('-'), ord('-'), ord(' '), ord(':'), ord(':'), ord('.')),
1483        starts=(0, 4, 7, 10, 13, 16, 19),
1484        stops=(3, 6, 9, 12, 15, 18, -1),
1485        # Break allowed *before*
1486        #              y  m  d  h  m  s  f
1487        break_allowed=(0, 0, 0, 1, 0, 1, 1),
1488        has_day_of_year=0)
1489
1490    def parse_string(self, timestr, subfmts):
1491        # Handle trailing 'Z' for UTC time
1492        if timestr.endswith('Z'):
1493            if self.scale != 'utc':
1494                raise ValueError("Time input terminating in 'Z' must have "
1495                                 "scale='UTC'")
1496            timestr = timestr[:-1]
1497        return super().parse_string(timestr, subfmts)
1498
1499
1500class TimeISOT(TimeISO):
1501    """
1502    ISO 8601 compliant date-time format "YYYY-MM-DDTHH:MM:SS.sss...".
1503    This is the same as TimeISO except for a "T" instead of space between
1504    the date and time.
1505    For example, 2000-01-01T00:00:00.000 is midnight on January 1, 2000.
1506
1507    The allowed subformats are:
1508
1509    - 'date_hms': date + hours, mins, secs (and optional fractional secs)
1510    - 'date_hm': date + hours, mins
1511    - 'date': date
1512    """
1513
1514    name = 'isot'
1515    subfmts = (('date_hms',
1516                '%Y-%m-%dT%H:%M:%S',
1517                '{year:d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}:{sec:02d}'),
1518               ('date_hm',
1519                '%Y-%m-%dT%H:%M',
1520                '{year:d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}'),
1521               ('date',
1522                '%Y-%m-%d',
1523                '{year:d}-{mon:02d}-{day:02d}'))
1524
1525    # See TimeISO for explanation
1526    fast_parser_pars = dict(
1527        delims=(0, ord('-'), ord('-'), ord('T'), ord(':'), ord(':'), ord('.')),
1528        starts=(0, 4, 7, 10, 13, 16, 19),
1529        stops=(3, 6, 9, 12, 15, 18, -1),
1530        # Break allowed *before*
1531        #              y  m  d  h  m  s  f
1532        break_allowed=(0, 0, 0, 1, 0, 1, 1),
1533        has_day_of_year=0)
1534
1535
1536class TimeYearDayTime(TimeISO):
1537    """
1538    Year, day-of-year and time as "YYYY:DOY:HH:MM:SS.sss...".
1539    The day-of-year (DOY) goes from 001 to 365 (366 in leap years).
1540    For example, 2000:001:00:00:00.000 is midnight on January 1, 2000.
1541
1542    The allowed subformats are:
1543
1544    - 'date_hms': date + hours, mins, secs (and optional fractional secs)
1545    - 'date_hm': date + hours, mins
1546    - 'date': date
1547    """
1548
1549    name = 'yday'
1550    subfmts = (('date_hms',
1551                '%Y:%j:%H:%M:%S',
1552                '{year:d}:{yday:03d}:{hour:02d}:{min:02d}:{sec:02d}'),
1553               ('date_hm',
1554                '%Y:%j:%H:%M',
1555                '{year:d}:{yday:03d}:{hour:02d}:{min:02d}'),
1556               ('date',
1557                '%Y:%j',
1558                '{year:d}:{yday:03d}'))
1559
1560    # Define positions and starting delimiter for year, month, day, hour,
1561    # minute, seconds components of an ISO time. This is used by the fast
1562    # C-parser parse_ymdhms_times()
1563    #
1564    #  "2000:123:13:14:15.678"
1565    #   012345678901234567890
1566    #   yyyy:ddd:hh:mm:ss.fff
1567    # Parsed as ('yyyy', ':ddd', ':hh', ':mm', ':ss', '.fff')
1568    #
1569    # delims: character at corresponding `starts` position (0 => no character)
1570    # starts: position where component starts (including delimiter if present)
1571    # stops: position where component ends (-1 => continue to end of string)
1572
1573    fast_parser_pars = dict(
1574        delims=(0, 0, ord(':'), ord(':'), ord(':'), ord(':'), ord('.')),
1575        starts=(0, -1, 4, 8, 11, 14, 17),
1576        stops=(3, -1, 7, 10, 13, 16, -1),
1577        # Break allowed before:
1578        #              y  m  d  h  m  s  f
1579        break_allowed=(0, 0, 0, 1, 0, 1, 1),
1580        has_day_of_year=1)
1581
1582
1583class TimeDatetime64(TimeISOT):
1584    name = 'datetime64'
1585
1586    def _check_val_type(self, val1, val2):
1587        if not val1.dtype.kind == 'M':
1588            if val1.size > 0:
1589                raise TypeError('Input values for {} class must be '
1590                                'datetime64 objects'.format(self.name))
1591            else:
1592                val1 = np.array([], 'datetime64[D]')
1593        if val2 is not None:
1594            raise ValueError(
1595                f'{self.name} objects do not accept a val2 but you provided {val2}')
1596
1597        return val1, None
1598
1599    def set_jds(self, val1, val2):
1600        # If there are any masked values in the ``val1`` datetime64 array
1601        # ('NaT') then stub them with a valid date so downstream parse_string
1602        # will work.  The value under the mask is arbitrary but a "modern" date
1603        # is good.
1604        mask = np.isnat(val1)
1605        masked = np.any(mask)
1606        if masked:
1607            val1 = val1.copy()
1608            val1[mask] = '2000'
1609
1610        # Make sure M(onth) and Y(ear) dates will parse and convert to bytestring
1611        if val1.dtype.name in ['datetime64[M]', 'datetime64[Y]']:
1612            val1 = val1.astype('datetime64[D]')
1613        val1 = val1.astype('S')
1614
1615        # Standard ISO string parsing now
1616        super().set_jds(val1, val2)
1617
1618        # Finally apply mask if necessary
1619        if masked:
1620            self.jd2[mask] = np.nan
1621
1622    @property
1623    def value(self):
1624        precision = self.precision
1625        self.precision = 9
1626        ret = super().value
1627        self.precision = precision
1628        return ret.astype('datetime64')
1629
1630
1631class TimeFITS(TimeString):
1632    """
1633    FITS format: "[±Y]YYYY-MM-DD[THH:MM:SS[.sss]]".
1634
1635    ISOT but can give signed five-digit year (mostly for negative years);
1636
1637    The allowed subformats are:
1638
1639    - 'date_hms': date + hours, mins, secs (and optional fractional secs)
1640    - 'date': date
1641    - 'longdate_hms': as 'date_hms', but with signed 5-digit year
1642    - 'longdate': as 'date', but with signed 5-digit year
1643
1644    See Rots et al., 2015, A&A 574:A36 (arXiv:1409.7583).
1645    """
1646    name = 'fits'
1647    subfmts = (
1648        ('date_hms',
1649         (r'(?P<year>\d{4})-(?P<mon>\d\d)-(?P<mday>\d\d)T'
1650          r'(?P<hour>\d\d):(?P<min>\d\d):(?P<sec>\d\d(\.\d*)?)'),
1651         '{year:04d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}:{sec:02d}'),
1652        ('date',
1653         r'(?P<year>\d{4})-(?P<mon>\d\d)-(?P<mday>\d\d)',
1654         '{year:04d}-{mon:02d}-{day:02d}'),
1655        ('longdate_hms',
1656         (r'(?P<year>[+-]\d{5})-(?P<mon>\d\d)-(?P<mday>\d\d)T'
1657          r'(?P<hour>\d\d):(?P<min>\d\d):(?P<sec>\d\d(\.\d*)?)'),
1658         '{year:+06d}-{mon:02d}-{day:02d}T{hour:02d}:{min:02d}:{sec:02d}'),
1659        ('longdate',
1660         r'(?P<year>[+-]\d{5})-(?P<mon>\d\d)-(?P<mday>\d\d)',
1661         '{year:+06d}-{mon:02d}-{day:02d}'))
1662    # Add the regex that parses the scale and possible realization.
1663    # Support for this is deprecated.  Read old style but no longer write
1664    # in this style.
1665    subfmts = tuple(
1666        (subfmt[0],
1667         subfmt[1] + r'(\((?P<scale>\w+)(\((?P<realization>\w+)\))?\))?',
1668         subfmt[2]) for subfmt in subfmts)
1669
1670    def parse_string(self, timestr, subfmts):
1671        """Read time and deprecated scale if present"""
1672        # Try parsing with any of the allowed sub-formats.
1673        for _, regex, _ in subfmts:
1674            tm = re.match(regex, timestr)
1675            if tm:
1676                break
1677        else:
1678            raise ValueError(f'Time {timestr} does not match {self.name} format')
1679        tm = tm.groupdict()
1680        # Scale and realization are deprecated and strings in this form
1681        # are no longer created.  We issue a warning but still use the value.
1682        if tm['scale'] is not None:
1683            warnings.warn("FITS time strings should no longer have embedded time scale.",
1684                          AstropyDeprecationWarning)
1685            # If a scale was given, translate from a possible deprecated
1686            # timescale identifier to the scale used by Time.
1687            fits_scale = tm['scale'].upper()
1688            scale = FITS_DEPRECATED_SCALES.get(fits_scale, fits_scale.lower())
1689            if scale not in TIME_SCALES:
1690                raise ValueError("Scale {!r} is not in the allowed scales {}"
1691                                 .format(scale, sorted(TIME_SCALES)))
1692            # If no scale was given in the initialiser, set the scale to
1693            # that given in the string.  Realization is ignored
1694            # and is only supported to allow old-style strings to be
1695            # parsed.
1696            if self._scale is None:
1697                self._scale = scale
1698            if scale != self.scale:
1699                raise ValueError("Input strings for {} class must all "
1700                                 "have consistent time scales."
1701                                 .format(self.name))
1702        return [int(tm['year']), int(tm['mon']), int(tm['mday']),
1703                int(tm.get('hour', 0)), int(tm.get('min', 0)),
1704                float(tm.get('sec', 0.))]
1705
1706    @property
1707    def value(self):
1708        """Convert times to strings, using signed 5 digit if necessary."""
1709        if 'long' not in self.out_subfmt:
1710            # If we have times before year 0 or after year 9999, we can
1711            # output only in a "long" format, using signed 5-digit years.
1712            jd = self.jd1 + self.jd2
1713            if jd.size and (jd.min() < 1721425.5 or jd.max() >= 5373484.5):
1714                self.out_subfmt = 'long' + self.out_subfmt
1715        return super().value
1716
1717
1718class TimeEpochDate(TimeNumeric):
1719    """
1720    Base class for support floating point Besselian and Julian epoch dates
1721    """
1722    _default_scale = 'tt'  # As of astropy 3.2, this is no longer 'utc'.
1723
1724    def set_jds(self, val1, val2):
1725        self._check_scale(self._scale)  # validate scale.
1726        epoch_to_jd = getattr(erfa, self.epoch_to_jd)
1727        jd1, jd2 = epoch_to_jd(val1 + val2)
1728        self.jd1, self.jd2 = day_frac(jd1, jd2)
1729
1730    def to_value(self, **kwargs):
1731        jd_to_epoch = getattr(erfa, self.jd_to_epoch)
1732        value = jd_to_epoch(self.jd1, self.jd2)
1733        return super().to_value(jd1=value, jd2=np.float64(0.0), **kwargs)
1734
1735    value = property(to_value)
1736
1737
1738class TimeBesselianEpoch(TimeEpochDate):
1739    """Besselian Epoch year as floating point value(s) like 1950.0"""
1740    name = 'byear'
1741    epoch_to_jd = 'epb2jd'
1742    jd_to_epoch = 'epb'
1743
1744    def _check_val_type(self, val1, val2):
1745        """Input value validation, typically overridden by derived classes"""
1746        if hasattr(val1, 'to') and hasattr(val1, 'unit'):
1747            raise ValueError("Cannot use Quantities for 'byear' format, "
1748                             "as the interpretation would be ambiguous. "
1749                             "Use float with Besselian year instead. ")
1750        # FIXME: is val2 really okay here?
1751        return super()._check_val_type(val1, val2)
1752
1753
1754class TimeJulianEpoch(TimeEpochDate):
1755    """Julian Epoch year as floating point value(s) like 2000.0"""
1756    name = 'jyear'
1757    unit = erfa.DJY  # 365.25, the Julian year, for conversion to quantities
1758    epoch_to_jd = 'epj2jd'
1759    jd_to_epoch = 'epj'
1760
1761
1762class TimeEpochDateString(TimeString):
1763    """
1764    Base class to support string Besselian and Julian epoch dates
1765    such as 'B1950.0' or 'J2000.0' respectively.
1766    """
1767    _default_scale = 'tt'  # As of astropy 3.2, this is no longer 'utc'.
1768
1769    def set_jds(self, val1, val2):
1770        epoch_prefix = self.epoch_prefix
1771        # Be liberal in what we accept: convert bytes to ascii.
1772        to_string = (str if val1.dtype.kind == 'U' else
1773                     lambda x: str(x.item(), encoding='ascii'))
1774        iterator = np.nditer([val1, None], op_dtypes=[val1.dtype, np.double],
1775                             flags=['zerosize_ok'])
1776        for val, years in iterator:
1777            try:
1778                time_str = to_string(val)
1779                epoch_type, year_str = time_str[0], time_str[1:]
1780                year = float(year_str)
1781                if epoch_type.upper() != epoch_prefix:
1782                    raise ValueError
1783            except (IndexError, ValueError, UnicodeEncodeError):
1784                raise ValueError(f'Time {val} does not match {self.name} format')
1785            else:
1786                years[...] = year
1787
1788        self._check_scale(self._scale)  # validate scale.
1789        epoch_to_jd = getattr(erfa, self.epoch_to_jd)
1790        jd1, jd2 = epoch_to_jd(iterator.operands[-1])
1791        self.jd1, self.jd2 = day_frac(jd1, jd2)
1792
1793    @property
1794    def value(self):
1795        jd_to_epoch = getattr(erfa, self.jd_to_epoch)
1796        years = jd_to_epoch(self.jd1, self.jd2)
1797        # Use old-style format since it is a factor of 2 faster
1798        str_fmt = self.epoch_prefix + '%.' + str(self.precision) + 'f'
1799        outs = [str_fmt % year for year in years.flat]
1800        return np.array(outs).reshape(self.jd1.shape)
1801
1802
1803class TimeBesselianEpochString(TimeEpochDateString):
1804    """Besselian Epoch year as string value(s) like 'B1950.0'"""
1805    name = 'byear_str'
1806    epoch_to_jd = 'epb2jd'
1807    jd_to_epoch = 'epb'
1808    epoch_prefix = 'B'
1809
1810
1811class TimeJulianEpochString(TimeEpochDateString):
1812    """Julian Epoch year as string value(s) like 'J2000.0'"""
1813    name = 'jyear_str'
1814    epoch_to_jd = 'epj2jd'
1815    jd_to_epoch = 'epj'
1816    epoch_prefix = 'J'
1817
1818
1819class TimeDeltaFormat(TimeFormat):
1820    """Base class for time delta representations"""
1821
1822    _registry = TIME_DELTA_FORMATS
1823
1824    def _check_scale(self, scale):
1825        """
1826        Check that the scale is in the allowed list of scales, or is `None`
1827        """
1828        if scale is not None and scale not in TIME_DELTA_SCALES:
1829            raise ScaleValueError("Scale value '{}' not in "
1830                                  "allowed values {}"
1831                                  .format(scale, TIME_DELTA_SCALES))
1832
1833        return scale
1834
1835
1836class TimeDeltaNumeric(TimeDeltaFormat, TimeNumeric):
1837
1838    def set_jds(self, val1, val2):
1839        self._check_scale(self._scale)  # Validate scale.
1840        self.jd1, self.jd2 = day_frac(val1, val2, divisor=1. / self.unit)
1841
1842    def to_value(self, **kwargs):
1843        # Note that 1/unit is always exactly representable, so the
1844        # following multiplications are exact.
1845        factor = 1. / self.unit
1846        jd1 = self.jd1 * factor
1847        jd2 = self.jd2 * factor
1848        return super().to_value(jd1=jd1, jd2=jd2, **kwargs)
1849
1850    value = property(to_value)
1851
1852
1853class TimeDeltaSec(TimeDeltaNumeric):
1854    """Time delta in SI seconds"""
1855    name = 'sec'
1856    unit = 1. / erfa.DAYSEC  # for quantity input
1857
1858
1859class TimeDeltaJD(TimeDeltaNumeric):
1860    """Time delta in Julian days (86400 SI seconds)"""
1861    name = 'jd'
1862    unit = 1.
1863
1864
1865class TimeDeltaDatetime(TimeDeltaFormat, TimeUnique):
1866    """Time delta in datetime.timedelta"""
1867    name = 'datetime'
1868
1869    def _check_val_type(self, val1, val2):
1870        if not all(isinstance(val, datetime.timedelta) for val in val1.flat):
1871            raise TypeError('Input values for {} class must be '
1872                            'datetime.timedelta objects'.format(self.name))
1873        if val2 is not None:
1874            raise ValueError(
1875                f'{self.name} objects do not accept a val2 but you provided {val2}')
1876        return val1, None
1877
1878    def set_jds(self, val1, val2):
1879        self._check_scale(self._scale)  # Validate scale.
1880        iterator = np.nditer([val1, None, None],
1881                             flags=['refs_ok', 'zerosize_ok'],
1882                             op_dtypes=[None, np.double, np.double])
1883
1884        day = datetime.timedelta(days=1)
1885        for val, jd1, jd2 in iterator:
1886            jd1[...], other = divmod(val.item(), day)
1887            jd2[...] = other / day
1888
1889        self.jd1, self.jd2 = day_frac(iterator.operands[-2],
1890                                      iterator.operands[-1])
1891
1892    @property
1893    def value(self):
1894        iterator = np.nditer([self.jd1, self.jd2, None],
1895                             flags=['refs_ok', 'zerosize_ok'],
1896                             op_dtypes=[None, None, object])
1897
1898        for jd1, jd2, out in iterator:
1899            jd1_, jd2_ = day_frac(jd1, jd2)
1900            out[...] = datetime.timedelta(days=jd1_,
1901                                          microseconds=jd2_ * 86400 * 1e6)
1902
1903        return self.mask_if_needed(iterator.operands[-1])
1904
1905
1906def _validate_jd_for_storage(jd):
1907    if isinstance(jd, (float, int)):
1908        return np.array(jd, dtype=np.float_)
1909    if (isinstance(jd, np.generic)
1910        and (jd.dtype.kind == 'f' and jd.dtype.itemsize <= 8
1911             or jd.dtype.kind in 'iu')):
1912        return np.array(jd, dtype=np.float_)
1913    elif (isinstance(jd, np.ndarray)
1914          and jd.dtype.kind == 'f'
1915          and jd.dtype.itemsize == 8):
1916        return jd
1917    else:
1918        raise TypeError(
1919            f"JD values must be arrays (possibly zero-dimensional) "
1920            f"of floats but we got {jd!r} of type {type(jd)}")
1921
1922
1923def _broadcast_writeable(jd1, jd2):
1924    if jd1.shape == jd2.shape:
1925        return jd1, jd2
1926    # When using broadcast_arrays, *both* are flagged with
1927    # warn-on-write, even the one that wasn't modified, and
1928    # require "C" only clears the flag if it actually copied
1929    # anything.
1930    shape = np.broadcast(jd1, jd2).shape
1931    if jd1.shape == shape:
1932        s_jd1 = jd1
1933    else:
1934        s_jd1 = np.require(np.broadcast_to(jd1, shape),
1935                           requirements=["C", "W"])
1936    if jd2.shape == shape:
1937        s_jd2 = jd2
1938    else:
1939        s_jd2 = np.require(np.broadcast_to(jd2, shape),
1940                           requirements=["C", "W"])
1941    return s_jd1, s_jd2
1942
1943
1944# Import symbols from core.py that are used in this module. This succeeds
1945# because __init__.py imports format.py just before core.py.
1946from .core import Time, TIME_SCALES, TIME_DELTA_SCALES, ScaleValueError  # noqa
1947