"""
This module provides an object that can handle a time range.
"""
from datetime import timedelta
import astropy.units as u
from astropy.time import Time, TimeDelta
from sunpy import config
from sunpy.time import is_time_equal, parse_time
from sunpy.time.time import _variables_for_parse_time_docstring
from sunpy.util.decorators import add_common_docstring, deprecated
TIME_FORMAT = config.get('general', 'time_format')
__all__ = ['TimeRange']
[docs]
@add_common_docstring(**_variables_for_parse_time_docstring())
class TimeRange:
"""
A class to create and handle time ranges.
.. note::
Regardless of how a `sunpy.time.TimeRange` is constructed it will always
provide a positive time range where the start time is before the end time.
``__contains__`` has been implemented which means you can
check if a time is within the time range you have created.
Please see the example section below.
Parameters
----------
a : {parse_time_types}
A time (the start time) specified as a parse_time-compatible
time string, number, or a datetime object.
b : {parse_time_types}
Another time (the end time) specified as a
parse_time-compatible time string, number, or a datetime object.
May also be the size of the time range specified as a timedelta object,
or a `~astropy.units.Quantity`.
Examples
--------
>>> import numpy as np
>>> from astropy.time import TimeDelta
>>> import astropy.units as u
>>> from sunpy.time import TimeRange
>>> time_range = TimeRange('2010/03/04 00:10', '2010/03/04 00:20')
>>> time_range = TimeRange(('2010/03/04 00:10', '2010/03/04 00:20'))
>>> time_range = TimeRange('2010/03/04 00:10', 400 * u.s)
>>> TimeRange('2010/03/04 00:10', 400 * u.day)
<sunpy.time.timerange.TimeRange object at ...>
Start: 2010-03-04 00:10:00
End: 2011-04-08 00:10:00
Center:2010-09-20 00:10:00
Duration:400.0 days or
9600.0 hours or
576000.0 minutes or
34560000.0 seconds
<BLANKLINE>
>>> time1 = '2014/5/5 12:11'
>>> time2 = '2012/5/5 12:11'
>>> time_range = TimeRange('2014/05/04 13:54', '2018/02/03 12:12')
>>> time1 in time_range
True
>>> time2 in time_range
False
>>> time_range = TimeRange('2014/05/05 12:00', '2014/05/10 12:00')
>>> time_arr = np.arange(time_range.start, time_range.end, TimeDelta(24*60*60, format = "sec"))
>>> time_arr
array([<Time object: scale='utc' format='isot' value=2014-05-05T12:00:00.000>,
<Time object: scale='utc' format='isot' value=2014-05-06T12:00:00.000>,
<Time object: scale='utc' format='isot' value=2014-05-07T12:00:00.000>,
<Time object: scale='utc' format='isot' value=2014-05-08T12:00:00.000>,
<Time object: scale='utc' format='isot' value=2014-05-09T12:00:00.000>],
dtype=object)
>>> time_arr[np.logical_and(time_arr > time_range.start, time_arr < time_range.end)]
array([<Time object: scale='utc' format='isot' value=2014-05-06T12:00:00.000>,
<Time object: scale='utc' format='isot' value=2014-05-07T12:00:00.000>,
<Time object: scale='utc' format='isot' value=2014-05-08T12:00:00.000>,
<Time object: scale='utc' format='isot' value=2014-05-09T12:00:00.000>],
dtype=object)
"""
def __init__(self, a, b=None, format=None):
# If a is a TimeRange object, copy attributes to new instance.
self._t1 = None
self._t2 = None
if isinstance(a, TimeRange):
self.__dict__ = a.__dict__.copy()
return
# Normalize different input types
if b is None:
x = parse_time(a[0], format=format)
if len(a) != 2:
raise ValueError('If b is None a must have two elements')
else:
y = a[1]
else:
x = parse_time(a, format=format)
y = b
if isinstance(y, u.Quantity):
y = TimeDelta(y)
if isinstance(y, timedelta):
y = TimeDelta(y, format='datetime')
# Timedelta
if isinstance(y, TimeDelta):
if y.jd >= 0:
self._t1 = x
self._t2 = x + y
else:
self._t1 = x + y
self._t2 = x
return
# Otherwise, assume that the second argument is parse_time-compatible
y = parse_time(y, format=format)
if isinstance(y, Time):
if x < y:
self._t1 = x
self._t2 = y
else:
self._t1 = y
self._t2 = x
@property
def start(self):
"""
Get the start time.
Returns
-------
`astropy.time.Time`
The start time.
"""
return self._t1
@property
def end(self):
"""
Get the end time.
Returns
-------
`astropy.time.Time`
The end time.
"""
return self._t2
@property
def dt(self):
"""
Get the length of the time range. Always a positive value.
Returns
-------
`astropy.time.TimeDelta`
The difference between the start and the end time.
"""
return self._t2 - self._t1
@property
def center(self):
"""
Gets the center of the time range.
Returns
-------
`astropy.time.Time`
The center time.
"""
return self.start + self.dt / 2
@property
def hours(self):
"""
Get the number of hours elapsed.
Returns
-------
`astropy.units.Quantity`
The amount of hours between the start and end time.
"""
return self.dt.to('hour')
@property
def days(self):
"""
Gets the number of days elapsed.
Returns
-------
`astropy.units.Quantity`
The amount of days between the start and end time.
"""
return self.dt.to('d')
@property
def seconds(self):
"""
Gets the number of seconds elapsed.
Returns
-------
`astropy.units.Quantity`
The amount of seconds between the start and end time.
"""
return self.dt.to('s')
@property
def minutes(self):
"""
Gets the number of minutes elapsed.
Returns
-------
`astropy.units.Quantity`
The amount of minutes between the start and end time.
"""
return self.dt.to('min')
def __eq__(self, other):
"""
Check that two `sunpy.time.TimeRange` have the same start and end
datetime.
Parameters
----------
other : `~sunpy.time.timerange.TimeRange`
The second `sunpy.time.TimeRange` to compare to.
Returns
-------
`bool`
`True` if equal, `False` otherwise.
"""
if isinstance(other, TimeRange):
return is_time_equal(
self.start, other.start) and is_time_equal(self.end, other.end)
return NotImplemented
def __ne__(self, other):
"""
Check two `sunpy.time.TimeRange` have different start or end datetimes.
Parameters
----------
other : `~sunpy.time.timerange.TimeRange`
The second `sunpy.time.TimeRange` to compare to.
Returns
-------
`bool`
`True` if non-equal, `False` otherwise.
"""
if isinstance(other, TimeRange):
return not (is_time_equal(
self.start, other.start) and is_time_equal(self.end, other.end))
return NotImplemented
def __repr__(self):
"""
Returns a human-readable representation of `sunpy.time.TimeRange`.
"""
t1 = self.start.strftime(TIME_FORMAT)
t2 = self.end.strftime(TIME_FORMAT)
center = self.center.strftime(TIME_FORMAT)
fully_qualified_name = f'{self.__class__.__module__}.{self.__class__.__name__}'
return (f' <{fully_qualified_name} object at {hex(id(self))}>' +
'\n Start:'.ljust(12) + t1 +
'\n End:'.ljust(12) + t2 +
'\n Center:'.ljust(12) + center +
'\n Duration:'.ljust(12) + str(self.days.value) + ' days or' +
'\n '.ljust(12) + str(self.hours.value) + ' hours or' +
'\n '.ljust(12) + str(self.minutes.value) + ' minutes or' +
'\n '.ljust(12) + str(self.seconds.value) + ' seconds' +
'\n')
[docs]
def split(self, n=2):
"""
Splits the time range into multiple equally sized parts.
Parameters
----------
n : `int`, optional
The number of times to split the time range (must >= 1).
Defaults to 2.
Returns
-------
`list`
A list of equally sized `sunpy.time.TimeRange` between the start and end times.
"""
if n <= 0:
raise ValueError('n must be greater than or equal to 1')
subsections = []
previous_time = self.start
next_time = None
for _ in range(n):
next_time = previous_time + self.dt / n
next_range = TimeRange(previous_time, next_time)
subsections.append(next_range)
previous_time = next_time
return subsections
[docs]
def window(self, cadence, window):
"""
Split the time range up into a series of `~sunpy.time.TimeRange` that
are ``window`` long, with a cadence of ``cadence``.
Parameters
----------
cadence : `~astropy.units.quantity.Quantity`, `astropy.time.TimeDelta`
Cadence.
window : `~astropy.units.quantity.Quantity`, `astropy.time.TimeDelta`
The length of window.
Returns
-------
`list`
A list of `~sunpy.time.TimeRange`, that are ``window`` long
and separated by ``cadence``.
Examples
--------
>>> import astropy.units as u
>>> from sunpy.time import TimeRange
>>> time_range = TimeRange('2010/03/04 00:10', '2010/03/04 01:20')
>>> time_range.window(60*60*u.s, window=12*u.s) # doctest: +SKIP
[ <sunpy.time.timerange.TimeRange object at 0x7f0214bfc208>
Start: 2010-03-04 00:10:00
End: 2010-03-04 00:10:12
Center:2010-03-04 00:10:06
Duration:0.0001388888888888889 days or
0.003333333333333333 hours or
0.2 minutes or
12.0 seconds,
<sunpy.time.timerange.TimeRange object at 0x7f01fe43ac50>
Start: 2010-03-04 01:10:00
End: 2010-03-04 01:10:12
Center:2010-03-04 01:10:06
Duration:0.0001388888888888889 days or
0.003333333333333333 hours or
0.2 minutes or
12.0 seconds,
<sunpy.time.timerange.TimeRange object at 0x7f01fb90b898>
Start: 2010-03-04 02:10:00
End: 2010-03-04 02:10:12
Center:2010-03-04 02:10:06
Duration:0.0001388888888888889 days or
0.003333333333333333 hours or
0.2 minutes or
12.0 seconds]
"""
if not isinstance(window, TimeDelta):
window = TimeDelta(window)
if not isinstance(cadence, TimeDelta):
cadence = TimeDelta(cadence)
n = 1
times = [TimeRange(self.start, self.start + window)]
while times[-1].end < self.end:
times.append(TimeRange(self.start + cadence * n,
self.start + cadence * n + window))
n += 1
return times
[docs]
def next(self):
"""
Shift the time range forward by the amount of time elapsed.
"""
dt = self.dt
self._t1 = self._t1 + dt
self._t2 = self._t2 + dt
return self
[docs]
def previous(self):
"""
Shift the time range backward by the amount of time elapsed.
"""
dt = self.dt
self._t1 = self._t1 - dt
self._t2 = self._t2 - dt
return self
[docs]
def shift(self, dt_start, dt_end):
"""
Shift the start and the end of the time range.
To extend the time range both forwards and backwards, specify a negative value
for ``dt_start`` and a positive value for ``dt_end``.
Parameters
----------
dt_start : `astropy.time.TimeDelta`, `datetime.timedelta`, `astropy.units.Quantity`
The amount to shift the start time.
dt_end : `astropy.time.TimeDelta`, `datetime.timedelta`, `astropy.units.Quantity`
The amount to shift the end time.
"""
dt_start = TimeDelta(dt_start, scale=self.dt.scale) if not isinstance(dt_start, TimeDelta) else dt_start
dt_end = TimeDelta(dt_end, scale=self.dt.scale) if not isinstance(dt_end, TimeDelta) else dt_end
self._t1 = self._t1 + dt_start
self._t2 = self._t2 + dt_end
if self._t1 > self._t2:
self._t1, self._t2 = self._t2, self._t1
[docs]
@deprecated('6.1', alternative='sunpy.time.TimeRange.shift')
def extend(self, dt_start, dt_end):
"""
Shift the start and the end of the time range.
To extend the time range both forwards and backwards, specify a negative value
for ``dt_start`` and a positive value for ``dt_end``.
Parameters
----------
dt_start : `astropy.time.TimeDelta`
The amount to shift the start time.
dt_end : `astropy.time.TimeDelta`
The amount to shift the end time.
"""
self.shift(dt_start, dt_end)
[docs]
def get_dates(self):
"""
Return all partial days contained within the time range.
"""
delta = self.end.to_datetime().date() - self.start.to_datetime().date()
dates = []
dates = [
parse_time(self.start.strftime('%Y-%m-%d')) + TimeDelta(i*u.day)
for i in range(delta.days + 1)
]
return dates
@add_common_docstring(**_variables_for_parse_time_docstring())
def __contains__(self, time):
"""
Checks whether the given time lies within this range. Both limits are
inclusive (i.e., ``__contains__(t1)`` and ``__contains__(t2)`` always
return `True).that.
Parameters
----------
time : {parse_time_types}
{parse_time_desc}
Returns
-------
`bool`
`True` if time lies between start and end, `False` otherwise.
Examples
--------
>>> from sunpy.time import TimeRange
>>> time1 = '2014/5/5 12:11'
>>> time2 = '2012/5/5 12:11'
>>> time_range = TimeRange('2014/05/04 13:54', '2018/02/03 12:12')
>>> time1 in time_range
True
>>> time2 in time_range
False
"""
this_time = parse_time(time)
return this_time >= self.start and this_time <= self.end
[docs]
def intersects(self, other):
"""
Return `True` if this interval overlaps with *other*.
Parameters
----------
other : sunpy.time.TimeRange
Other `~sunpy.time.TimeRange` to check for intersection.
Notes
-----
Both intervals are treated as closed, i.e., their endpoints are included.
"""
# Order intervals so int1 has the earliest start time
if other.start > self.start:
int_first = self
int_second = other
else:
int_first = other
int_second = self
return int_second.start <= int_first.end