test_time_utils.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. from __future__ import annotations
  2. from datetime import datetime, timedelta, timezone
  3. from isodate import duration_isoformat as original_duration_isoformat
  4. import pandas as pd
  5. import pytz
  6. import pytest
  7. from flexmeasures.utils.time_utils import (
  8. duration_isoformat,
  9. server_now,
  10. naturalized_datetime_str,
  11. get_most_recent_clocktime_window,
  12. apply_offset_chain,
  13. to_utc_timestamp,
  14. )
  15. @pytest.mark.parametrize(
  16. "td, iso",
  17. [
  18. (timedelta(hours=1), "PT1H"),
  19. (timedelta(hours=14), "PT14H"),
  20. (timedelta(hours=24), "PT24H"),
  21. (timedelta(days=1), "PT24H"),
  22. (timedelta(days=1, seconds=22), "PT24H22S"),
  23. (timedelta(days=1, seconds=122), "PT24H2M2S"),
  24. ],
  25. )
  26. def test_duration_isoformat(td: timedelta, iso: str):
  27. assert duration_isoformat(td) == iso
  28. @pytest.mark.parametrize(
  29. "td, iso",
  30. [
  31. (timedelta(hours=1), "PT1H"),
  32. (timedelta(hours=14), "PT14H"),
  33. # todo: if the following test cases fail, we can start using isodate.duration_isoformat again (see #459)
  34. (timedelta(hours=24), "P1D"),
  35. (timedelta(days=1), "P1D"),
  36. (timedelta(days=1, seconds=22), "P1DT22S"),
  37. (timedelta(days=1, seconds=122), "P1DT2M2S"),
  38. ],
  39. )
  40. def test_original_duration_isoformat(td: timedelta, iso: str):
  41. assert original_duration_isoformat(td) == iso
  42. @pytest.mark.parametrize(
  43. "dt_tz, now, server_tz, delta_in_h, exp_result",
  44. # there can be two results depending of today's date, due to humanize.
  45. # Monekypatching was too hard.
  46. [
  47. (None, pd.Timestamp.utcnow(), "UTC", 3, "3 hours ago"),
  48. (None, pd.Timestamp.utcnow().tz_convert("Asia/Seoul"), "UTC", 3, "3 hours ago"),
  49. (None, datetime.now(timezone.utc), "UTC", 3, "3 hours ago"),
  50. (
  51. None,
  52. datetime.now(timezone.utc).replace(tzinfo=None),
  53. "UTC",
  54. 3,
  55. "3 hours ago",
  56. ),
  57. (
  58. None,
  59. datetime(2021, 5, 17, 3),
  60. "Europe/Amsterdam",
  61. 48,
  62. ("May 15", "May 15 2021"),
  63. ),
  64. ("Asia/Seoul", "server_now", "Europe/Amsterdam", 1, "an hour ago"),
  65. (
  66. "UTC",
  67. datetime(2021, 5, 17, 3),
  68. "Asia/Seoul",
  69. 24 * 7,
  70. ("May 10", "May 10 2021"),
  71. ),
  72. ("UTC", datetime(2021, 5, 17, 3), "Asia/Seoul", None, "never"),
  73. ],
  74. )
  75. def test_naturalized_datetime_str(
  76. app,
  77. monkeypatch,
  78. dt_tz,
  79. now,
  80. server_tz,
  81. delta_in_h,
  82. exp_result,
  83. ):
  84. monkeypatch.setitem(app.config, "FLEXMEASURES_TIMEZONE", server_tz)
  85. if now == "server_now":
  86. now = server_now() # done this way as it needs (patched) app context
  87. if now.tzinfo is None:
  88. now = now.replace(tzinfo=pytz.utc) # assuming UTC
  89. if delta_in_h is not None:
  90. h_ago = now - timedelta(hours=delta_in_h)
  91. if dt_tz is not None:
  92. h_ago = h_ago.astimezone(pytz.timezone(dt_tz))
  93. else:
  94. h_ago = None
  95. if isinstance(exp_result, tuple):
  96. assert naturalized_datetime_str(h_ago, now=now) in exp_result
  97. else:
  98. assert naturalized_datetime_str(h_ago, now=now) == exp_result
  99. @pytest.mark.parametrize(
  100. "window_size, now, exp_start, exp_end, grace_period",
  101. [
  102. (
  103. 5,
  104. datetime(2021, 4, 30, 15, 1),
  105. datetime(2021, 4, 30, 14, 55),
  106. datetime(2021, 4, 30, 15),
  107. 0,
  108. ),
  109. (
  110. 15,
  111. datetime(2021, 4, 30, 3, 36),
  112. datetime(2021, 4, 30, 3, 15),
  113. datetime(2021, 4, 30, 3, 30),
  114. 0,
  115. ),
  116. (
  117. 5,
  118. datetime(2021, 4, 30, 9, 15, 16),
  119. datetime(2021, 4, 30, 9, 10),
  120. datetime(2021, 4, 30, 9, 15),
  121. 0,
  122. ),
  123. (
  124. 10,
  125. datetime(2021, 4, 30, 0, 5),
  126. datetime(2021, 4, 29, 23, 50),
  127. datetime(2021, 4, 30, 0, 0),
  128. 0,
  129. ),
  130. (
  131. 5,
  132. datetime(
  133. 2021, 5, 20, 10, 5, 34
  134. ), # less than grace period into the new window
  135. datetime(2021, 5, 20, 9, 55),
  136. datetime(2021, 5, 20, 10, 0),
  137. 60,
  138. ),
  139. (
  140. 5,
  141. datetime(2021, 5, 20, 10, 7, 4),
  142. datetime(2021, 5, 20, 9, 55),
  143. datetime(2021, 5, 20, 10, 0),
  144. 130, # grace period > 2 minutes
  145. ),
  146. (
  147. 60,
  148. datetime(2021, 1, 1, 0, 4), # new year
  149. datetime(2020, 12, 31, 23, 00),
  150. datetime(2021, 1, 1, 0, 0),
  151. 0,
  152. ),
  153. (
  154. 60,
  155. datetime(2021, 3, 28, 3, 10), # DST transition
  156. datetime(2021, 3, 28, 2),
  157. datetime(2021, 3, 28, 3),
  158. 0,
  159. ),
  160. ],
  161. )
  162. def test_recent_clocktime_window(window_size, now, exp_start, exp_end, grace_period):
  163. start, end = get_most_recent_clocktime_window(
  164. window_size, now=now, grace_period_in_seconds=grace_period
  165. )
  166. assert start == exp_start
  167. assert end == exp_end
  168. def test_recent_clocktime_window_invalid_window():
  169. with pytest.raises(AssertionError):
  170. get_most_recent_clocktime_window(25, now=datetime(2021, 4, 30, 3, 36))
  171. get_most_recent_clocktime_window(120, now=datetime(2021, 4, 30, 3, 36))
  172. get_most_recent_clocktime_window(0, now=datetime(2021, 4, 30, 3, 36))
  173. @pytest.mark.parametrize(
  174. "input_date, offset_chain, output_date",
  175. [
  176. (datetime(2023, 5, 17, 10, 15), ",,, , , ", datetime(2023, 5, 17, 10, 15)),
  177. (datetime(2023, 5, 17, 10, 15), "", datetime(2023, 5, 17, 10, 15)),
  178. (datetime(2023, 5, 17, 10, 15), ",,hb, , , ", datetime(2023, 5, 17, 10)),
  179. (datetime(2023, 5, 17, 10, 15), "DB", datetime(2023, 5, 17)),
  180. (datetime(2023, 5, 17, 10, 15), "2D,DB", datetime(2023, 5, 19)),
  181. (datetime(2023, 5, 17, 10, 15), "-2D,DB", datetime(2023, 5, 15)),
  182. # The day of this moment started on May 17th in Amsterdam (but started on May 16th in UTC).
  183. (
  184. pytz.timezone("Europe/Amsterdam").localize(datetime(2023, 5, 17, 0, 15)),
  185. "DB",
  186. pytz.timezone("Europe/Amsterdam").localize(datetime(2023, 5, 17)),
  187. ),
  188. # Check Pandas structure, too.
  189. (
  190. pd.Timestamp("2023-05-17T00:15", tz="Europe/Amsterdam"),
  191. "DB",
  192. pd.Timestamp("2023-05-17", tz="Europe/Amsterdam"),
  193. ),
  194. ],
  195. )
  196. def test_apply_offset_chain(
  197. input_date: pd.Timestamp | datetime,
  198. offset_chain: str,
  199. output_date: pd.Timestamp | datetime,
  200. ):
  201. assert apply_offset_chain(input_date, offset_chain) == output_date
  202. @pytest.mark.parametrize(
  203. "input_value, expected_output",
  204. [
  205. (datetime(2024, 4, 28, 8, 55, 58), 1714294558.0),
  206. (pd.Timestamp("2024-04-28 08:55:58", tz="UTC").to_pydatetime(), 1714294558.0),
  207. ("Sun, 28 Apr 2024 08:55:58 GMT", 1714294558.0),
  208. ("Invalid date string", None),
  209. (None, None),
  210. ],
  211. )
  212. def test_to_utc_timestamp(input_value, expected_output):
  213. result = to_utc_timestamp(input_value)
  214. if expected_output is None:
  215. assert result is None
  216. else:
  217. assert result == pytest.approx(expected_output)