__init__.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. """
  2. .. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
  3. """
  4. import datetime
  5. import re
  6. from typing import List, Optional, Union
  7. import dateutil.parser
  8. import dateutil.relativedelta as rdelta
  9. import typepy
  10. from .__version__ import __author__, __copyright__, __email__, __license__, __version__
  11. class DateTimeRange:
  12. """
  13. A class that represents a range of datetime.
  14. :param datetime.datetime/str start_datetime: |param_start_datetime|
  15. :param datetime.datetime/str end_datetime: |param_end_datetime|
  16. :Examples:
  17. :Sample Code:
  18. .. code:: python
  19. from datetimerange import DateTimeRange
  20. DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  21. :Output:
  22. .. parsed-literal::
  23. 2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900
  24. .. py:attribute:: start_time_format
  25. :type: str
  26. :value: "%Y-%m-%dT%H:%M:%S%z"
  27. Conversion format string for :py:attr:`.start_datetime`.
  28. .. seealso:: :py:meth:`.get_start_time_str`
  29. .. py:attribute:: end_time_format
  30. :type: str
  31. :value: "%Y-%m-%dT%H:%M:%S%z"
  32. Conversion format string for :py:attr:`.end_datetime`.
  33. .. seealso:: :py:meth:`.get_end_time_str`
  34. """
  35. NOT_A_TIME_STR = "NaT"
  36. def __init__(
  37. self,
  38. start_datetime=None,
  39. end_datetime=None,
  40. start_time_format="%Y-%m-%dT%H:%M:%S%z",
  41. end_time_format="%Y-%m-%dT%H:%M:%S%z",
  42. ):
  43. self.set_time_range(start_datetime, end_datetime)
  44. self.start_time_format = start_time_format
  45. self.end_time_format = end_time_format
  46. self.is_output_elapse = False
  47. self.separator = " - "
  48. def __repr__(self):
  49. if self.is_output_elapse:
  50. suffix = f" ({self.end_datetime - self.start_datetime})"
  51. else:
  52. suffix = ""
  53. return self.separator.join((self.get_start_time_str(), self.get_end_time_str())) + suffix
  54. def __eq__(self, other):
  55. if not isinstance(other, DateTimeRange):
  56. return False
  57. return all(
  58. [self.start_datetime == other.start_datetime, self.end_datetime == other.end_datetime]
  59. )
  60. def __ne__(self, other):
  61. if not isinstance(other, DateTimeRange):
  62. return True
  63. return any(
  64. [self.start_datetime != other.start_datetime, self.end_datetime != other.end_datetime]
  65. )
  66. def __add__(self, other):
  67. return DateTimeRange(self.start_datetime + other, self.end_datetime + other)
  68. def __iadd__(self, other):
  69. self.set_start_datetime(self.start_datetime + other)
  70. self.set_end_datetime(self.end_datetime + other)
  71. return self
  72. def __sub__(self, other):
  73. return DateTimeRange(self.start_datetime - other, self.end_datetime - other)
  74. def __isub__(self, other):
  75. self.set_start_datetime(self.start_datetime - other)
  76. self.set_end_datetime(self.end_datetime - other)
  77. return self
  78. def __contains__(self, x):
  79. """
  80. :param x:
  81. |datetime|/``DateTimeRange`` instance to compare.
  82. Parse and convert to |datetime| if the value type is |str|.
  83. :type x: |datetime|/``DateTimeRange``/|str|
  84. :return: |True| if the ``x`` is within the time range
  85. :rtype: bool
  86. :Sample Code:
  87. .. code:: python
  88. from datetimerange import DateTimeRange
  89. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  90. print("2015-03-22T10:05:00+0900" in time_range)
  91. print("2015-03-22T10:15:00+0900" in time_range)
  92. time_range_smaller = DateTimeRange("2015-03-22T10:03:00+0900", "2015-03-22T10:07:00+0900")
  93. print(time_range_smaller in time_range)
  94. :Output:
  95. .. parsed-literal::
  96. True
  97. False
  98. True
  99. .. seealso::
  100. :py:meth:`.validate_time_inversion`
  101. """
  102. self.validate_time_inversion()
  103. if isinstance(x, DateTimeRange):
  104. return x.start_datetime >= self.start_datetime and x.end_datetime <= self.end_datetime
  105. try:
  106. value = dateutil.parser.parse(x)
  107. except (TypeError, AttributeError):
  108. value = x
  109. return self.start_datetime <= value <= self.end_datetime
  110. @property
  111. def start_datetime(self):
  112. """
  113. :return: Start time of the time range.
  114. :rtype: datetime.datetime
  115. :Sample Code:
  116. .. code:: python
  117. from datetimerange import DateTimeRange
  118. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  119. time_range.start_datetime
  120. :Output:
  121. .. parsed-literal::
  122. datetime.datetime(2015, 3, 22, 10, 0, tzinfo=tzoffset(None, 32400))
  123. """
  124. return self.__start_datetime
  125. @property
  126. def end_datetime(self):
  127. """
  128. :return: End time of the time range.
  129. :rtype: datetime.datetime
  130. :Sample Code:
  131. .. code:: python
  132. from datetimerange import DateTimeRange
  133. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  134. time_range.end_datetime
  135. :Output:
  136. .. parsed-literal::
  137. datetime.datetime(2015, 3, 22, 10, 10, tzinfo=tzoffset(None, 32400))
  138. """
  139. return self.__end_datetime
  140. @property
  141. def timedelta(self):
  142. """
  143. :return:
  144. (|attr_end_datetime| - |attr_start_datetime|) as |timedelta|
  145. :rtype: datetime.timedelta
  146. :Sample Code:
  147. .. code:: python
  148. from datetimerange import DateTimeRange
  149. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  150. time_range.timedelta
  151. :Output:
  152. .. parsed-literal::
  153. datetime.timedelta(0, 600)
  154. """
  155. return self.end_datetime - self.start_datetime
  156. def is_set(self):
  157. """
  158. :return:
  159. |True| if both |attr_start_datetime| and
  160. |attr_end_datetime| were not |None|.
  161. :rtype: bool
  162. :Sample Code:
  163. .. code:: python
  164. from datetimerange import DateTimeRange
  165. time_range = DateTimeRange()
  166. print(time_range.is_set())
  167. time_range.set_time_range("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  168. print(time_range.is_set())
  169. :Output:
  170. .. parsed-literal::
  171. False
  172. True
  173. """
  174. return all([self.start_datetime is not None, self.end_datetime is not None])
  175. def validate_time_inversion(self):
  176. """
  177. Check time inversion of the time range.
  178. :raises ValueError:
  179. If |attr_start_datetime| is
  180. bigger than |attr_end_datetime|.
  181. :raises TypeError:
  182. Any one of |attr_start_datetime| and |attr_end_datetime|,
  183. or both is inappropriate datetime value.
  184. :Sample Code:
  185. .. code:: python
  186. from datetimerange import DateTimeRange
  187. time_range = DateTimeRange("2015-03-22T10:10:00+0900", "2015-03-22T10:00:00+0900")
  188. try:
  189. time_range.validate_time_inversion()
  190. except ValueError:
  191. print("time inversion")
  192. :Output:
  193. .. parsed-literal::
  194. time inversion
  195. """
  196. if not self.is_set():
  197. # for python2/3 compatibility
  198. raise TypeError
  199. if self.start_datetime > self.end_datetime:
  200. raise ValueError(
  201. "time inversion found: {:s} > {:s}".format(
  202. str(self.start_datetime), str(self.end_datetime)
  203. )
  204. )
  205. def is_valid_timerange(self):
  206. """
  207. :return:
  208. |True| if the time range is
  209. not null and not time inversion.
  210. :rtype: bool
  211. :Sample Code:
  212. .. code:: python
  213. from datetimerange import DateTimeRange
  214. time_range = DateTimeRange()
  215. print(time_range.is_valid_timerange())
  216. time_range.set_time_range("2015-03-22T10:20:00+0900", "2015-03-22T10:10:00+0900")
  217. print(time_range.is_valid_timerange())
  218. time_range.set_time_range("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  219. print(time_range.is_valid_timerange())
  220. :Output:
  221. .. parsed-literal::
  222. False
  223. False
  224. True
  225. .. seealso::
  226. :py:meth:`.is_set`
  227. :py:meth:`.validate_time_inversion`
  228. """
  229. try:
  230. self.validate_time_inversion()
  231. except (TypeError, ValueError):
  232. return False
  233. return self.is_set()
  234. def is_intersection(self, x):
  235. """
  236. :param DateTimeRange x: Value to compare
  237. :return: |True| if intersect with ``x``
  238. :rtype: bool
  239. :Sample Code:
  240. .. code:: python
  241. from datetimerange import DateTimeRange
  242. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  243. x = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
  244. time_range.is_intersection(x)
  245. :Output:
  246. .. parsed-literal::
  247. True
  248. """
  249. return self.intersection(x).is_set()
  250. def get_start_time_str(self):
  251. """
  252. :return:
  253. |attr_start_datetime| as |str| formatted with
  254. |attr_start_time_format|.
  255. Return |NaT| if the invalid value or the invalid format.
  256. :rtype: str
  257. :Sample Code:
  258. .. code:: python
  259. from datetimerange import DateTimeRange
  260. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  261. print(time_range.get_start_time_str())
  262. time_range.start_time_format = "%Y/%m/%d %H:%M:%S"
  263. print(time_range.get_start_time_str())
  264. :Output:
  265. .. parsed-literal::
  266. 2015-03-22T10:00:00+0900
  267. 2015/03/22 10:00:00
  268. """
  269. try:
  270. return self.start_datetime.strftime(self.start_time_format)
  271. except AttributeError:
  272. return self.NOT_A_TIME_STR
  273. def get_end_time_str(self):
  274. """
  275. :return:
  276. |attr_end_datetime| as a |str| formatted with
  277. |attr_end_time_format|.
  278. Return |NaT| if invalid datetime or format.
  279. :rtype: str
  280. :Sample Code:
  281. .. code:: python
  282. from datetimerange import DateTimeRange
  283. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  284. print(time_range.get_end_time_str())
  285. time_range.end_time_format = "%Y/%m/%d %H:%M:%S"
  286. print(time_range.get_end_time_str())
  287. :Output:
  288. .. parsed-literal::
  289. 2015-03-22T10:10:00+0900
  290. 2015/03/22 10:10:00
  291. """
  292. try:
  293. return self.end_datetime.strftime(self.end_time_format)
  294. except AttributeError:
  295. return self.NOT_A_TIME_STR
  296. def get_timedelta_second(self):
  297. """
  298. :return: (|attr_end_datetime| - |attr_start_datetime|) as seconds
  299. :rtype: float
  300. :Sample Code:
  301. .. code:: python
  302. from datetimerange import DateTimeRange
  303. time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  304. time_range.get_timedelta_second()
  305. :Output:
  306. .. parsed-literal::
  307. 600.0
  308. """
  309. return self.timedelta.total_seconds()
  310. def set_start_datetime(self, value, timezone=None):
  311. """
  312. Set the start time of the time range.
  313. :param value: |param_start_datetime|
  314. :type value: |datetime|/|str|
  315. :raises ValueError: If the value is invalid as a |datetime| value.
  316. :Sample Code:
  317. .. code:: python
  318. from datetimerange import DateTimeRange
  319. time_range = DateTimeRange()
  320. print(time_range)
  321. time_range.set_start_datetime("2015-03-22T10:00:00+0900")
  322. print(time_range)
  323. :Output:
  324. .. parsed-literal::
  325. NaT - NaT
  326. 2015-03-22T10:00:00+0900 - NaT
  327. """
  328. self.__start_datetime = self.__normalize_datetime_value(value, timezone)
  329. def set_end_datetime(self, value, timezone=None):
  330. """
  331. Set the end time of the time range.
  332. :param datetime.datetime/str value: |param_end_datetime|
  333. :raises ValueError: If the value is invalid as a |datetime| value.
  334. :Sample Code:
  335. .. code:: python
  336. from datetimerange import DateTimeRange
  337. time_range = DateTimeRange()
  338. print(time_range)
  339. time_range.set_end_datetime("2015-03-22T10:10:00+0900")
  340. print(time_range)
  341. :Output:
  342. .. parsed-literal::
  343. NaT - NaT
  344. NaT - 2015-03-22T10:10:00+0900
  345. """
  346. self.__end_datetime = self.__normalize_datetime_value(value, timezone)
  347. def set_time_range(self, start, end):
  348. """
  349. :param datetime.datetime/str start: |param_start_datetime|
  350. :param datetime.datetime/str end: |param_end_datetime|
  351. :Sample Code:
  352. .. code:: python
  353. from datetimerange import DateTimeRange
  354. time_range = DateTimeRange()
  355. print(time_range)
  356. time_range.set_time_range("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  357. print(time_range)
  358. :Output:
  359. .. parsed-literal::
  360. NaT - NaT
  361. 2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900
  362. """
  363. self.set_start_datetime(start)
  364. self.set_end_datetime(end)
  365. @staticmethod
  366. def __compare_relativedelta(lhs, rhs):
  367. if lhs.years < rhs.years:
  368. return -1
  369. if lhs.years > rhs.years:
  370. return 1
  371. if lhs.months < rhs.months:
  372. return -1
  373. if lhs.months > rhs.months:
  374. return 1
  375. if lhs.days < rhs.days:
  376. return -1
  377. if lhs.days > rhs.days:
  378. return 1
  379. if lhs.hours < rhs.hours:
  380. return -1
  381. if lhs.hours > rhs.hours:
  382. return 1
  383. if lhs.minutes < rhs.minutes:
  384. return -1
  385. if lhs.minutes > rhs.minutes:
  386. return 1
  387. if lhs.seconds < rhs.seconds:
  388. return -1
  389. if lhs.seconds > rhs.seconds:
  390. return 1
  391. if lhs.microseconds < rhs.microseconds:
  392. return -1
  393. if lhs.microseconds > rhs.microseconds:
  394. return 1
  395. return 0
  396. def __compare_timedelta(self, lhs, seconds):
  397. try:
  398. rhs = datetime.timedelta(seconds=seconds)
  399. if lhs < rhs:
  400. return -1
  401. if lhs > rhs:
  402. return 1
  403. return 0
  404. except TypeError:
  405. return self.__compare_relativedelta(
  406. lhs.normalized(), rdelta.relativedelta(seconds=seconds)
  407. )
  408. def range(self, step):
  409. """
  410. Return an iterator object.
  411. :param step: Step of iteration.
  412. :type step: |timedelta|/dateutil.relativedelta.relativedelta
  413. :return: iterator
  414. :rtype: iterator
  415. :Sample Code:
  416. .. code:: python
  417. import datetime
  418. from datetimerange import DateTimeRange
  419. time_range = DateTimeRange("2015-01-01T00:00:00+0900", "2015-01-04T00:00:00+0900")
  420. for value in time_range.range(datetime.timedelta(days=1)):
  421. print(value)
  422. :Output:
  423. .. parsed-literal::
  424. 2015-01-01 00:00:00+09:00
  425. 2015-01-02 00:00:00+09:00
  426. 2015-01-03 00:00:00+09:00
  427. 2015-01-04 00:00:00+09:00
  428. """
  429. if self.__compare_timedelta(step, 0) == 0:
  430. raise ValueError("step must be not zero")
  431. is_inversion = False
  432. try:
  433. self.validate_time_inversion()
  434. except ValueError:
  435. is_inversion = True
  436. if not is_inversion:
  437. if self.__compare_timedelta(step, seconds=0) < 0:
  438. raise ValueError(f"invalid step: expect greater than 0, actual={step}")
  439. else:
  440. if self.__compare_timedelta(step, seconds=0) > 0:
  441. raise ValueError(f"invalid step: expect less than 0, actual={step}")
  442. current_datetime = self.start_datetime
  443. while current_datetime <= self.end_datetime:
  444. yield current_datetime
  445. current_datetime = current_datetime + step
  446. def intersection(self, x):
  447. """
  448. Newly set a time range that overlaps
  449. the input and the current time range.
  450. :param DateTimeRange x:
  451. Value to compute intersection with the current time range.
  452. :Sample Code:
  453. .. code:: python
  454. from datetimerange import DateTimeRange
  455. dtr0 = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  456. dtr1 = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
  457. dtr0.intersection(dtr1)
  458. :Output:
  459. .. parsed-literal::
  460. 2015-03-22T10:05:00+0900 - 2015-03-22T10:10:00+0900
  461. """
  462. self.validate_time_inversion()
  463. x.validate_time_inversion()
  464. if any([x.start_datetime in self, self.start_datetime in x]):
  465. start_datetime = max(self.start_datetime, x.start_datetime)
  466. end_datetime = min(self.end_datetime, x.end_datetime)
  467. else:
  468. start_datetime = None
  469. end_datetime = None
  470. return DateTimeRange(
  471. start_datetime=start_datetime,
  472. end_datetime=end_datetime,
  473. start_time_format=self.start_time_format,
  474. end_time_format=self.end_time_format,
  475. )
  476. def subtract(self, x):
  477. """
  478. Remove a time range from this one and return the result.
  479. - The result will be ``[self.copy()]`` if the second range does not overlap the first
  480. - The result will be ``[]`` if the second range wholly encompasses the first range
  481. - The result will be ``[new_range]`` if the second range overlaps one end of the range
  482. - The result will be ``[new_range1, new_range2]`` if the second range is
  483. an internal sub range of the first
  484. :param DateTimeRange x:
  485. Range to remove from this one.
  486. :return: List(DateTimeRange)
  487. List of new ranges when the second range is removed from this one
  488. :Sample Code:
  489. .. code:: python
  490. from datetimerange import DateTimeRange
  491. dtr0 = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  492. dtr1 = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
  493. dtr0.subtract(dtr1)
  494. :Output:
  495. .. parsed-literal::
  496. [2015-03-22T10:00:00+0900 - 2015-03-22T10:05:00+0900]
  497. """
  498. overlap = self.intersection(x)
  499. # No intersection, return a copy of the original
  500. if not overlap.is_set() or overlap.get_timedelta_second() <= 0:
  501. return [
  502. DateTimeRange(
  503. start_datetime=self.start_datetime,
  504. end_datetime=self.end_datetime,
  505. start_time_format=self.start_time_format,
  506. end_time_format=self.end_time_format,
  507. )
  508. ]
  509. # Case 2, full overlap, subtraction results in empty set
  510. if (
  511. overlap.start_datetime == self.start_datetime
  512. and overlap.end_datetime == self.end_datetime
  513. ):
  514. return []
  515. # Case 3, overlap on start
  516. if overlap.start_datetime == self.start_datetime:
  517. return [
  518. DateTimeRange(
  519. start_datetime=overlap.end_datetime,
  520. end_datetime=self.end_datetime,
  521. start_time_format=self.start_time_format,
  522. end_time_format=self.end_time_format,
  523. )
  524. ]
  525. # Case 4, overlap on end
  526. if overlap.end_datetime == self.end_datetime:
  527. return [
  528. DateTimeRange(
  529. start_datetime=self.start_datetime,
  530. end_datetime=overlap.start_datetime,
  531. start_time_format=self.start_time_format,
  532. end_time_format=self.end_time_format,
  533. )
  534. ]
  535. # Case 5, underlap, two new ranges are needed.
  536. return [
  537. DateTimeRange(
  538. start_datetime=self.start_datetime,
  539. end_datetime=overlap.start_datetime,
  540. start_time_format=self.start_time_format,
  541. end_time_format=self.end_time_format,
  542. ),
  543. DateTimeRange(
  544. start_datetime=overlap.end_datetime,
  545. end_datetime=self.end_datetime,
  546. start_time_format=self.start_time_format,
  547. end_time_format=self.end_time_format,
  548. ),
  549. ]
  550. def encompass(self, x):
  551. """
  552. Newly set a time range that encompasses
  553. the input and the current time range.
  554. :param DateTimeRange x:
  555. Value to compute encompass with the current time range.
  556. :Sample Code:
  557. .. code:: python
  558. from datetimerange import DateTimeRange
  559. dtr0 = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  560. dtr1 = DateTimeRange("2015-03-22T10:05:00+0900", "2015-03-22T10:15:00+0900")
  561. dtr0.encompass(dtr1)
  562. :Output:
  563. .. parsed-literal::
  564. 2015-03-22T10:00:00+0900 - 2015-03-22T10:15:00+0900
  565. """
  566. self.validate_time_inversion()
  567. x.validate_time_inversion()
  568. return DateTimeRange(
  569. start_datetime=min(self.start_datetime, x.start_datetime),
  570. end_datetime=max(self.end_datetime, x.end_datetime),
  571. start_time_format=self.start_time_format,
  572. end_time_format=self.end_time_format,
  573. )
  574. def truncate(self, percentage):
  575. """
  576. Truncate ``percentage`` / 2 [%] of whole time from first and last time.
  577. :param float percentage: Percentage of truncate.
  578. :Sample Code:
  579. .. code:: python
  580. from datetimerange import DateTimeRange
  581. time_range = DateTimeRange(
  582. "2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  583. time_range.is_output_elapse = True
  584. print(time_range)
  585. time_range.truncate(10)
  586. print(time_range)
  587. :Output:
  588. .. parsed-literal::
  589. 2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900 (0:10:00)
  590. 2015-03-22T10:00:30+0900 - 2015-03-22T10:09:30+0900 (0:09:00)
  591. """
  592. self.validate_time_inversion()
  593. if percentage < 0:
  594. raise ValueError("discard_percent must be greater or equal to zero: " + str(percentage))
  595. if percentage == 0:
  596. return
  597. discard_time = self.timedelta // int(100) * int(percentage / 2)
  598. self.__start_datetime += discard_time
  599. self.__end_datetime -= discard_time
  600. def split(self, separator: Union[str, datetime.datetime]) -> List["DateTimeRange"]:
  601. """
  602. Split the DateTimerange in two DateTimerange at a specifit datetime.
  603. :param Union[str, datetime.datetime] separator:
  604. Date and time to split the DateTimeRange.
  605. This value will be included for both of the ranges after split.
  606. :Sample Code:
  607. .. code:: python
  608. from datetimerange import DateTimeRange
  609. dtr = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
  610. dtr.split("2015-03-22T10:05:00+0900")
  611. :Output:
  612. .. parsed-literal::
  613. [2015-03-22T10:00:00+0900 - 2015-03-22T10:05:00+0900,
  614. 2015-03-22T10:05:00+0900 - 2015-03-22T10:10:00+0900]
  615. """
  616. self.validate_time_inversion()
  617. separatingseparation = self.__normalize_datetime_value(separator, timezone=None)
  618. if (separatingseparation not in self) or (
  619. separatingseparation in (self.start_datetime, self.end_datetime)
  620. ):
  621. return [
  622. DateTimeRange(
  623. start_datetime=self.start_datetime,
  624. end_datetime=self.end_datetime,
  625. start_time_format=self.start_time_format,
  626. end_time_format=self.end_time_format,
  627. )
  628. ]
  629. return [
  630. DateTimeRange(
  631. start_datetime=self.start_datetime,
  632. end_datetime=separatingseparation,
  633. start_time_format=self.start_time_format,
  634. end_time_format=self.end_time_format,
  635. ),
  636. DateTimeRange(
  637. start_datetime=separatingseparation,
  638. end_datetime=self.end_datetime,
  639. start_time_format=self.start_time_format,
  640. end_time_format=self.end_time_format,
  641. ),
  642. ]
  643. def __normalize_datetime_value(self, value, timezone):
  644. if value is None:
  645. return None
  646. try:
  647. return typepy.type.DateTime(
  648. value, strict_level=typepy.StrictLevel.MIN, timezone=timezone
  649. ).convert()
  650. except typepy.TypeConversionError as e:
  651. raise ValueError(e)
  652. @classmethod
  653. def from_range_text(
  654. cls,
  655. range_text: str,
  656. separator: str = "-",
  657. start_time_format: Optional[str] = None,
  658. end_time_format: Optional[str] = None,
  659. ) -> "DateTimeRange":
  660. """Create a ``DateTimeRange`` instance from a datetime range text.
  661. :param str range_text:
  662. Input text that includes datetime range.
  663. e.g. ``2021-01-23T10:00:00+0400 - 2021-01-232T10:10:00+0400``
  664. :param str separator:
  665. Text that separating the ``range_text``.
  666. :return: DateTimeRange
  667. Created instance.
  668. """
  669. dattime_ranges = re.split(r"\s+{}\s+".format(re.escape(separator)), range_text.strip())
  670. if len(dattime_ranges) != 2:
  671. raise ValueError("range_text should include two datetime that separated by hyphen")
  672. start, end = dattime_ranges
  673. kwargs = {
  674. "start_datetime": start,
  675. "end_datetime": end,
  676. }
  677. if start_time_format:
  678. kwargs["start_time_format"] = start_time_format
  679. if end_time_format:
  680. kwargs["end_time_format"] = end_time_format
  681. return DateTimeRange(**kwargs)