"""
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
"""
import datetime
import re
from typing import ClassVar, Iterator, List, Optional, Union
import dateutil.parser
import dateutil.relativedelta as rdelta
import typepy
DEFAULT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
def _to_norm_relativedelta(td: Union[datetime.timedelta, rdelta.relativedelta]) -> rdelta.relativedelta:
if isinstance(td, rdelta.relativedelta):
return td.normalized()
return rdelta.relativedelta(seconds=int(td.total_seconds()), microseconds=td.microseconds).normalized()
def _compare_relativedelta(lhs: rdelta.relativedelta, rhs: rdelta.relativedelta) -> int:
if lhs.years < rhs.years:
return -1
if lhs.years > rhs.years:
return 1
if lhs.months < rhs.months:
return -1
if lhs.months > rhs.months:
return 1
if lhs.days < rhs.days:
return -1
if lhs.days > rhs.days:
return 1
if lhs.hours < rhs.hours:
return -1
if lhs.hours > rhs.hours:
return 1
if lhs.minutes < rhs.minutes:
return -1
if lhs.minutes > rhs.minutes:
return 1
if lhs.seconds < rhs.seconds:
return -1
if lhs.seconds > rhs.seconds:
return 1
if lhs.microseconds < rhs.microseconds:
return -1
if lhs.microseconds > rhs.microseconds:
return 1
return 0
def _compare_timedelta(lhs: Union[datetime.timedelta, rdelta.relativedelta], seconds: int) -> int:
return _compare_relativedelta(_to_norm_relativedelta(lhs), rdelta.relativedelta(seconds=seconds))
def _normalize_datetime_value(
value: Union[datetime.datetime, str, None], timezone: Optional[datetime.tzinfo]
) -> Optional[datetime.datetime]:
if value is None:
return None
try:
return typepy.type.DateTime(value, strict_level=typepy.StrictLevel.MIN, timezone=timezone).convert()
except typepy.TypeConversionError as e:
raise ValueError(e)
[docs]
class DateTimeRange:
"""
A class that represents a range of datetime.
:param Union[datetime.datetime, str, None] start_datetime: |param_start_datetime|
:param Union[datetime.datetime, str, None] end_datetime: |param_end_datetime|
:param Optional[str] start_time_format:
Conversion format string for :py:attr:`.start_datetime`.
:param Optional[str] end_time_format:
Conversion format string for :py:attr:`.end_datetime`.
:param Optional[datetime.tzinfo] timezone:
Timezone of the time range.
:Examples:
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
:Output:
.. parsed-literal::
2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900
.. py:attribute:: start_time_format
:type: str
:value: "%Y-%m-%dT%H:%M:%S%z"
Conversion format string for :py:attr:`.start_datetime`.
.. seealso:: :py:meth:`.get_start_time_str`
.. py:attribute:: end_time_format
:type: str
:value: "%Y-%m-%dT%H:%M:%S%z"
Conversion format string for :py:attr:`.end_datetime`.
.. seealso:: :py:meth:`.get_end_time_str`
"""
NOT_A_TIME_STR: ClassVar[str] = "NaT"
[docs]
def __init__(
self,
start_datetime: Union[datetime.datetime, str, None] = None,
end_datetime: Union[datetime.datetime, str, None] = None,
start_time_format: Optional[str] = None,
end_time_format: Optional[str] = None,
timezone: Optional[datetime.tzinfo] = None,
) -> None:
self.set_time_range(start_datetime, end_datetime, timezone)
self.start_time_format = start_time_format or DEFAULT_TIME_FORMAT
self.end_time_format = end_time_format or DEFAULT_TIME_FORMAT
self.is_output_elapse = False
self.separator = " - "
[docs]
def __repr__(self) -> str:
if self.is_output_elapse and self.end_datetime and self.start_datetime:
suffix = f" ({self.end_datetime - self.start_datetime})"
else:
suffix = ""
return self.separator.join((self.get_start_time_str(), self.get_end_time_str())) + suffix
[docs]
def __eq__(self, other: object) -> bool:
if not isinstance(other, DateTimeRange):
return False
return all([self.start_datetime == other.start_datetime, self.end_datetime == other.end_datetime])
[docs]
def __ne__(self, other: object) -> bool:
if not isinstance(other, DateTimeRange):
return True
return any([self.start_datetime != other.start_datetime, self.end_datetime != other.end_datetime])
def __add__(self, other: Union[datetime.timedelta, rdelta.relativedelta]) -> "DateTimeRange":
if self.start_datetime is None and self.end_datetime is None:
raise TypeError("range is not set")
start_datetime = self.start_datetime
if start_datetime:
start_datetime += other
end_datetime = self.end_datetime
if end_datetime:
end_datetime += other
return DateTimeRange(start_datetime, end_datetime)
def __iadd__(self, other: Union[datetime.timedelta, rdelta.relativedelta]) -> "DateTimeRange":
if self.start_datetime is None and self.end_datetime is None:
raise TypeError("range is not set")
timezone = self.timezone
if self.start_datetime:
self.set_start_datetime(self.start_datetime + other, timezone)
if self.end_datetime:
self.set_end_datetime(self.end_datetime + other, timezone)
return self
def __sub__(self, other: Union[datetime.timedelta, rdelta.relativedelta]) -> "DateTimeRange":
if self.start_datetime is None and self.end_datetime is None:
raise TypeError("range is not set")
start_datetime = self.start_datetime
if start_datetime:
start_datetime -= other
end_datetime = self.end_datetime
if end_datetime:
end_datetime -= other
return DateTimeRange(start_datetime, end_datetime)
def __isub__(self, other: Union[datetime.timedelta, rdelta.relativedelta]) -> "DateTimeRange":
if self.start_datetime is None and self.end_datetime is None:
raise TypeError("range is not set")
timezone = self.timezone
if self.start_datetime:
self.set_start_datetime(self.start_datetime - other, timezone)
if self.end_datetime:
self.set_end_datetime(self.end_datetime - other, timezone)
return self
[docs]
def __contains__(self, x: Union[datetime.datetime, "DateTimeRange", str]) -> bool:
"""
:param x:
|datetime|/``DateTimeRange`` instance to compare.
Parse and convert to |datetime| if the value type is |str|.
:type x: |datetime|/``DateTimeRange``/|str|
:return: |True| if the ``x`` is within the time range
:rtype: bool
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
print("2015-03-22T10:05:00+0900" in time_range)
print("2015-03-22T10:15:00+0900" in time_range)
time_range_smaller = DateTimeRange("2015-03-22T10:03:00+0900", "2015-03-22T10:07:00+0900")
print(time_range_smaller in time_range)
:Output:
.. parsed-literal::
True
False
True
.. seealso::
:py:meth:`.validate_time_inversion`
"""
self.validate_time_inversion()
assert self.start_datetime
assert self.end_datetime
if isinstance(x, DateTimeRange):
x.validate_time_inversion()
assert x.start_datetime
assert x.end_datetime
return x.start_datetime >= self.start_datetime and x.end_datetime <= self.end_datetime
value = dateutil.parser.parse(x) if isinstance(x, str) else x
return self.start_datetime <= value <= self.end_datetime
@property
def start_datetime(self) -> Optional[datetime.datetime]:
"""
:return: Start time of the time range.
:rtype: Optional[datetime.datetime]
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
time_range.start_datetime
:Output:
.. parsed-literal::
datetime.datetime(2015, 3, 22, 10, 0, tzinfo=tzoffset(None, 32400))
"""
return self.__start_datetime
@property
def end_datetime(self) -> Optional[datetime.datetime]:
"""
:return: End time of the time range.
:rtype: Optional[datetime.datetime]
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
time_range.end_datetime
:Output:
.. parsed-literal::
datetime.datetime(2015, 3, 22, 10, 10, tzinfo=tzoffset(None, 32400))
"""
return self.__end_datetime
@property
def timezone(self) -> Optional[datetime.tzinfo]:
"""
:return: Timezone of the time range.
:rtype: Optional[datetime.tzinfo]
"""
if self.start_datetime and self.start_datetime.tzinfo:
return self.start_datetime.tzinfo
if self.end_datetime and self.end_datetime.tzinfo:
return self.end_datetime.tzinfo
return None
@property
def timedelta(self) -> datetime.timedelta:
"""
:return:
(|attr_end_datetime| - |attr_start_datetime|) as |timedelta|
:rtype: datetime.timedelta
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
time_range.timedelta
:Output:
.. parsed-literal::
datetime.timedelta(0, 600)
"""
if self.start_datetime is None:
raise TypeError("Must set start_datetime")
if self.end_datetime is None:
raise TypeError("Must set end_datetime")
return self.end_datetime - self.start_datetime
[docs]
def is_set(self) -> bool:
"""
:return:
|True| if both |attr_start_datetime| and
|attr_end_datetime| were not |None|.
:rtype: bool
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange()
print(time_range.is_set())
time_range.set_time_range("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
print(time_range.is_set())
:Output:
.. parsed-literal::
False
True
"""
return all([self.start_datetime is not None, self.end_datetime is not None])
[docs]
def validate_time_inversion(self) -> None:
"""
Check time inversion of the time range.
:raises ValueError:
If |attr_start_datetime| is
bigger than |attr_end_datetime|.
:raises TypeError:
Any one of |attr_start_datetime| and |attr_end_datetime|,
or both is inappropriate datetime value.
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:10:00+0900", "2015-03-22T10:00:00+0900")
try:
time_range.validate_time_inversion()
except ValueError:
print("time inversion")
:Output:
.. parsed-literal::
time inversion
"""
if not self.is_set():
# for python2/3 compatibility
raise TypeError
assert self.start_datetime
assert self.end_datetime
if self.start_datetime.tzinfo != self.end_datetime.tzinfo:
raise ValueError(f"timezone mismatch: start={self.start_datetime.tzinfo}, end={self.end_datetime.tzinfo}")
if self.start_datetime > self.end_datetime:
raise ValueError(
"time inversion found: {:s} > {:s}".format(str(self.start_datetime), str(self.end_datetime))
)
[docs]
def is_valid_timerange(self) -> bool:
"""
:return:
|True| if the time range is
not null and not time inversion.
:rtype: bool
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange()
print(time_range.is_valid_timerange())
time_range.set_time_range("2015-03-22T10:20:00+0900", "2015-03-22T10:10:00+0900")
print(time_range.is_valid_timerange())
time_range.set_time_range("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
print(time_range.is_valid_timerange())
:Output:
.. parsed-literal::
False
False
True
.. seealso::
:py:meth:`.is_set`
:py:meth:`.validate_time_inversion`
"""
try:
self.validate_time_inversion()
except (TypeError, ValueError):
return False
return self.is_set()
[docs]
def is_intersection(
self,
x: "DateTimeRange",
intersection_threshold: Union[datetime.timedelta, rdelta.relativedelta, None] = None,
) -> bool:
"""
:param DateTimeRange x: Value to compare
:param Union[datetime.timedelta, dateutil.relativedelta.relativedelta, None] intersection_threshold:
Minimum time constraint that an intersection must have.
Defaults to ``None`` (no constraint).
:return: |True| if intersect with ``x``
:rtype: bool
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
x = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
time_range.is_intersection(x)
:Output:
.. parsed-literal::
True
"""
return self.intersection(x, intersection_threshold).is_set()
[docs]
def get_start_time_str(self) -> str:
"""
:return:
|attr_start_datetime| as |str| formatted with
|attr_start_time_format|.
Return |NaT| if the invalid value or the invalid format.
:rtype: str
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
print(time_range.get_start_time_str())
time_range.start_time_format = "%Y/%m/%d %H:%M:%S"
print(time_range.get_start_time_str())
:Output:
.. parsed-literal::
2015-03-22T10:00:00+0900
2015/03/22 10:00:00
"""
if self.start_datetime is None:
return self.NOT_A_TIME_STR
try:
return self.start_datetime.strftime(self.start_time_format)
except AttributeError:
return self.NOT_A_TIME_STR
[docs]
def get_end_time_str(self) -> str:
"""
:return:
|attr_end_datetime| as a |str| formatted with
|attr_end_time_format|.
Return |NaT| if invalid datetime or format.
:rtype: str
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
print(time_range.get_end_time_str())
time_range.end_time_format = "%Y/%m/%d %H:%M:%S"
print(time_range.get_end_time_str())
:Output:
.. parsed-literal::
2015-03-22T10:10:00+0900
2015/03/22 10:10:00
"""
if self.end_datetime is None:
return self.NOT_A_TIME_STR
try:
return self.end_datetime.strftime(self.end_time_format)
except AttributeError:
return self.NOT_A_TIME_STR
[docs]
def get_timedelta_second(self) -> float:
"""
:return: (|attr_end_datetime| - |attr_start_datetime|) as seconds
:rtype: float
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
time_range.get_timedelta_second()
:Output:
.. parsed-literal::
600.0
"""
return self.timedelta.total_seconds()
[docs]
def set_start_datetime(
self, value: Union[datetime.datetime, str, None], timezone: Optional[datetime.tzinfo] = None
) -> None:
"""
Set the start time of the time range.
:param Union[datetime.datetime, str, None] value: |param_start_datetime|
:param Optional[datetime.tzinfo] timezone: |param_timezone|
:raises ValueError: If the value is invalid as a |datetime| value.
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange()
print(time_range)
time_range.set_start_datetime("2015-03-22T10:00:00+0900")
print(time_range)
:Output:
.. parsed-literal::
NaT - NaT
2015-03-22T10:00:00+0900 - NaT
"""
self.__start_datetime = _normalize_datetime_value(value, timezone)
[docs]
def set_end_datetime(
self, value: Union[datetime.datetime, str, None], timezone: Optional[datetime.tzinfo] = None
) -> None:
"""
Set the end time of the time range.
:param Union[datetime.datetime, str, None] value: |param_end_datetime|
:param Optional[datetime.tzinfo] timezone: |param_timezone|
:raises ValueError: If the value is invalid as a |datetime| value.
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange()
print(time_range)
time_range.set_end_datetime("2015-03-22T10:10:00+0900")
print(time_range)
:Output:
.. parsed-literal::
NaT - NaT
NaT - 2015-03-22T10:10:00+0900
"""
self.__end_datetime = _normalize_datetime_value(value, timezone)
[docs]
def set_time_range(
self,
start: Union[datetime.datetime, str, None],
end: Union[datetime.datetime, str, None],
timezone: Optional[datetime.tzinfo] = None,
) -> None:
"""
:param Union[datetime.datetime, str, None] start: |param_start_datetime|
:param Union[datetime.datetime, str, None] end: |param_end_datetime|
:param Optional[datetime.tzinfo] timezone: |param_timezone|
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange()
print(time_range)
time_range.set_time_range("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
print(time_range)
:Output:
.. parsed-literal::
NaT - NaT
2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900
"""
self.set_start_datetime(start, timezone)
self.set_end_datetime(end, timezone)
[docs]
def range(self, step: Union[datetime.timedelta, rdelta.relativedelta]) -> Iterator[datetime.datetime]:
"""
Return an iterator object.
:param step: Step of iteration.
:type step: |timedelta|/dateutil.relativedelta.relativedelta
:return: iterator
:rtype: iterator
:Sample Code:
.. code:: python
import datetime
from datetimerange import DateTimeRange
time_range = DateTimeRange("2015-01-01T00:00:00+0900", "2015-01-04T00:00:00+0900")
for value in time_range.range(datetime.timedelta(days=1)):
print(value)
:Output:
.. parsed-literal::
2015-01-01 00:00:00+09:00
2015-01-02 00:00:00+09:00
2015-01-03 00:00:00+09:00
2015-01-04 00:00:00+09:00
"""
cmp_step_w_zero = _compare_timedelta(step, seconds=0)
if cmp_step_w_zero == 0:
raise ValueError("step must be not zero")
is_inversion = False
try:
self.validate_time_inversion()
except ValueError:
is_inversion = True
assert self.start_datetime
assert self.end_datetime
current_datetime = self.start_datetime
if not is_inversion:
if cmp_step_w_zero < 0:
raise ValueError(f"invalid step: expect greater than 0, actual={step}")
while current_datetime <= self.end_datetime:
yield current_datetime
current_datetime = current_datetime + step
else:
if cmp_step_w_zero > 0:
raise ValueError(f"invalid step: expect less than 0, actual={step}")
while current_datetime >= self.end_datetime:
yield current_datetime
current_datetime = current_datetime + step
[docs]
def intersection(
self,
x: "DateTimeRange",
intersection_threshold: Union[datetime.timedelta, rdelta.relativedelta, None] = None,
) -> "DateTimeRange":
"""
Create a new time range that overlaps the input and the current time range.
If no overlaps found, return a time range that set ``None`` for both start and end time.
:param DateTimeRange x:
Value to compute intersection with the current time range.
:param Union[datetime.timedelta, dateutil.relativedelta.relativedelta, None] intersection_threshold:
Minimum time constraint that an intersection must have.
Defaults to ``None`` (no constraint).
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
dtr0 = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
dtr1 = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
dtr0.intersection(dtr1)
:Output:
.. parsed-literal::
2015-03-22T10:05:00+0900 - 2015-03-22T10:10:00+0900
"""
self.validate_time_inversion()
x.validate_time_inversion()
assert self.start_datetime
assert self.end_datetime
assert x.start_datetime
assert x.end_datetime
if any([x.start_datetime in self, self.start_datetime in x]):
start_datetime = max(self.start_datetime, x.start_datetime)
end_datetime = min(self.end_datetime, x.end_datetime)
else:
start_datetime = None
end_datetime = None
if intersection_threshold is not None:
if start_datetime is None or end_datetime is None:
return DateTimeRange(
start_datetime=None,
end_datetime=None,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
)
delta = end_datetime - start_datetime
if (
_compare_relativedelta(
_to_norm_relativedelta(delta),
_to_norm_relativedelta(intersection_threshold),
)
< 0
):
start_datetime = None
end_datetime = None
return DateTimeRange(
start_datetime=start_datetime,
end_datetime=end_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
)
[docs]
def subtract(self, x: "DateTimeRange") -> List["DateTimeRange"]:
"""
Remove a time range from this one and return the result.
- The result will be ``[self.copy()]`` if the second range does not overlap the first
- The result will be ``[]`` if the second range wholly encompasses the first range
- The result will be ``[new_range]`` if the second range overlaps one end of the range
- The result will be ``[new_range1, new_range2]`` if the second range is
an internal sub range of the first
:param DateTimeRange x:
Range to remove from this one.
:return: List(DateTimeRange)
List of new ranges when the second range is removed from this one
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
dtr0 = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
dtr1 = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
dtr0.subtract(dtr1)
:Output:
.. parsed-literal::
[2015-03-22T10:00:00+0900 - 2015-03-22T10:05:00+0900]
"""
overlap = self.intersection(x)
# No intersection, return a copy of the original
if not overlap.is_set() or overlap.get_timedelta_second() <= 0:
return [
DateTimeRange(
start_datetime=self.start_datetime,
end_datetime=self.end_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
)
]
# Case 2, full overlap, subtraction results in empty set
if overlap.start_datetime == self.start_datetime and overlap.end_datetime == self.end_datetime:
return []
# Case 3, overlap on start
if overlap.start_datetime == self.start_datetime:
return [
DateTimeRange(
start_datetime=overlap.end_datetime,
end_datetime=self.end_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
)
]
# Case 4, overlap on end
if overlap.end_datetime == self.end_datetime:
return [
DateTimeRange(
start_datetime=self.start_datetime,
end_datetime=overlap.start_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
)
]
# Case 5, underlap, two new ranges are needed.
return [
DateTimeRange(
start_datetime=self.start_datetime,
end_datetime=overlap.start_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
),
DateTimeRange(
start_datetime=overlap.end_datetime,
end_datetime=self.end_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
),
]
[docs]
def encompass(self, x: "DateTimeRange") -> "DateTimeRange":
"""
Create a new time range that encompasses the input and the current time range.
:param DateTimeRange x:
Value to compute encompass with the current time range.
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
dtr0 = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
dtr1 = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
dtr0.encompass(dtr1)
:Output:
.. parsed-literal::
2015-03-22T10:00:00+0900 - 2015-03-22T10:15:00+0900
"""
self.validate_time_inversion()
x.validate_time_inversion()
assert self.start_datetime
assert self.end_datetime
assert x.start_datetime
assert x.end_datetime
return DateTimeRange(
start_datetime=min(self.start_datetime, x.start_datetime),
end_datetime=max(self.end_datetime, x.end_datetime),
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
)
[docs]
def truncate(self, percentage: float) -> None:
"""
Truncate ``percentage`` / 2 [%] of whole time from first and last time.
:param float percentage: Percentage of truncate.
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
time_range = DateTimeRange(
"2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
time_range.is_output_elapse = True
print(time_range)
time_range.truncate(10)
print(time_range)
:Output:
.. parsed-literal::
2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900 (0:10:00)
2015-03-22T10:00:30+0900 - 2015-03-22T10:09:30+0900 (0:09:00)
"""
self.validate_time_inversion()
if percentage < 0:
raise ValueError("discard_percent must be greater or equal to zero: " + str(percentage))
if percentage == 0:
return
discard_time = self.timedelta // int(100) * int(percentage / 2)
if self.__start_datetime:
self.__start_datetime += discard_time
if self.__end_datetime:
self.__end_datetime -= discard_time
[docs]
def split(self, separator: Union[str, datetime.datetime]) -> List["DateTimeRange"]:
"""
Split the DateTimerange in two DateTimerange at a specific datetime.
:param Union[str, datetime.datetime] separator:
Date and time to split the DateTimeRange.
This value will be included for both of the ranges after split.
:Sample Code:
.. code:: python
from datetimerange import DateTimeRange
dtr = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
dtr.split("2015-03-22T10:05:00+0900")
:Output:
.. parsed-literal::
[2015-03-22T10:00:00+0900 - 2015-03-22T10:05:00+0900,
2015-03-22T10:05:00+0900 - 2015-03-22T10:10:00+0900]
"""
self.validate_time_inversion()
separatingseparation = _normalize_datetime_value(separator, timezone=None)
assert separatingseparation
if (separatingseparation not in self) or (separatingseparation in (self.start_datetime, self.end_datetime)):
return [
DateTimeRange(
start_datetime=self.start_datetime,
end_datetime=self.end_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
)
]
return [
DateTimeRange(
start_datetime=self.start_datetime,
end_datetime=separatingseparation,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
),
DateTimeRange(
start_datetime=separatingseparation,
end_datetime=self.end_datetime,
start_time_format=self.start_time_format,
end_time_format=self.end_time_format,
),
]
[docs]
@classmethod
def from_range_text(
cls,
range_text: str,
separator: str = r"\s+\-\s+",
start_time_format: Optional[str] = None,
end_time_format: Optional[str] = None,
timezone: Optional[datetime.tzinfo] = None,
) -> "DateTimeRange":
"""Create a ``DateTimeRange`` instance from a datetime range text.
:param str range_text:
Input text that includes datetime range.
e.g. ``2021-01-23T10:00:00+0400 - 2021-01-232T10:10:00+0400``
:param str separator:
Regular expression that separating the ``range_text`` to start and end time.
:return: DateTimeRange
Created instance.
"""
datetime_ranges = re.split(separator, range_text.strip())
if len(datetime_ranges) != 2:
raise ValueError(f"range_text should include two datetime that separated by hyphen: got={datetime_ranges}")
return DateTimeRange(
start_datetime=datetime_ranges[0],
end_datetime=datetime_ranges[1],
start_time_format=start_time_format,
end_time_format=end_time_format,
timezone=timezone,
)