Skip to content

Commit

Permalink
add time_utils.get_recent_clock_time_window() function (#135)
Browse files Browse the repository at this point in the history
* add time_utils.get_recent_clock_time_window() function

* add more tests, better docstring and use server time if now not given
  • Loading branch information
nhoening committed May 20, 2021
1 parent bc01359 commit af342e2
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 1 deletion.
2 changes: 2 additions & 0 deletions documentation/changelog.rst
Expand Up @@ -24,6 +24,8 @@ Infrastructure / Support
* For weather forecasts, switch from Dark Sky (closed from Aug 1, 2021) to OpenWeatherMap API [see `PR #113 <http://www.github.com/SeitaBV/flexmeasures/pull/113>`_]
* Re-use the database between automated tests, if possible. This shaves 2/3rd off of the time it takes for the FlexMeasures test suite to run [see `PR #115 <http://www.github.com/SeitaBV/flexmeasures/pull/115>`_]
* Let CLI package and plugins use Marshmallow Field definitions [see `PR #125 <http://www.github.com/SeitaBV/flexmeasures/pull/125>`_]
* add time_utils.get_recent_clock_time_window() function [see `PR #135 <http://www.github.com/SeitaBV/flexmeasures/pull/135>`_]



v0.4.1 | May 7, 2021
Expand Down
55 changes: 55 additions & 0 deletions flexmeasures/utils/tests/test_time_utils.py
Expand Up @@ -6,6 +6,7 @@
from flexmeasures.utils.time_utils import (
server_now,
naturalized_datetime_str,
get_most_recent_clocktime_window,
)


Expand Down Expand Up @@ -41,3 +42,57 @@ def test_naturalized_datetime_str(
h_ago = None
print(h_ago)
assert naturalized_datetime_str(h_ago, now=now) == exp_result


@pytest.mark.parametrize(
"window_size,now,exp_start,exp_end",
[
(
5,
datetime(2021, 4, 30, 15, 1),
datetime(2021, 4, 30, 14, 55),
datetime(2021, 4, 30, 15),
),
(
15,
datetime(2021, 4, 30, 3, 36),
datetime(2021, 4, 30, 3, 15),
datetime(2021, 4, 30, 3, 30),
),
(
10,
datetime(2021, 4, 30, 0, 5),
datetime(2021, 4, 29, 23, 50),
datetime(2021, 4, 30, 0, 0),
),
(
5,
datetime(2021, 5, 20, 10, 5, 34), # boundary condition
datetime(2021, 5, 20, 9, 55),
datetime(2021, 5, 20, 10, 0),
),
(
60,
datetime(2021, 1, 1, 0, 4), # new year
datetime(2020, 12, 31, 23, 00),
datetime(2021, 1, 1, 0, 0),
),
(
60,
datetime(2021, 3, 28, 3, 10), # DST transition
datetime(2021, 3, 28, 2),
datetime(2021, 3, 28, 3),
),
],
)
def test_recent_clocktime_window(window_size, now, exp_start, exp_end):
start, end = get_most_recent_clocktime_window(window_size, now=now)
assert start == exp_start
assert end == exp_end


def test_recent_clocktime_window_invalid_window():
with pytest.raises(AssertionError):
get_most_recent_clocktime_window(25, now=datetime(2021, 4, 30, 3, 36))
get_most_recent_clocktime_window(120, now=datetime(2021, 4, 30, 3, 36))
get_most_recent_clocktime_window(0, now=datetime(2021, 4, 30, 3, 36))
36 changes: 35 additions & 1 deletion flexmeasures/utils/time_utils.py
@@ -1,5 +1,5 @@
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import List, Union, Tuple, Optional

from flask import current_app
from flask_security.core import current_user
Expand Down Expand Up @@ -193,6 +193,40 @@ def get_most_recent_hour() -> datetime:
return now.replace(minute=now.minute - (now.minute % 60), second=0, microsecond=0)


def get_most_recent_clocktime_window(
window_size_in_minutes: int, now: Optional[datetime] = None
) -> Tuple[datetime, datetime]:
"""
Calculate a recent time window, returning a start and end minute so that
a full hour can be filled with such windows, e.g.:
Calling this function at 15:01:xx with window size 5 -> (14:55:00, 15:00:00)
Calling this function at 03:36:xx with window size 15 -> (03:15:00, 03:30:00)
window_size_in_minutes is assumed to > 0 and < = 60, and a divisor of 60 (1, 2, ..., 30, 60).
If now is not given, the current server time is used.
if now / the current time lies within a boundary minute (e.g. 15 when window_size_in_minutes=5),
then the window is not deemed over and the previous one is returned (in this case, [5, 10])
Returns two datetime objects. They'll be in the timezone (if given) of the now parameter,
or in the server timezone (see FLEXMEASURES_TIMEZONE setting).
"""
assert window_size_in_minutes > 0
assert 60 % window_size_in_minutes == 0
if now is None:
now = server_now()
last_full_minute = now.replace(second=0, microsecond=0) - timedelta(minutes=1)
last_round_minute = last_full_minute.minute - (
last_full_minute.minute % window_size_in_minutes
)
begin_time = last_full_minute.replace(minute=last_round_minute) - timedelta(
minutes=window_size_in_minutes
)
end_time = begin_time + timedelta(minutes=window_size_in_minutes)
return begin_time, end_time


def get_default_start_time() -> datetime:
return get_most_recent_quarter() - timedelta(days=1)

Expand Down

0 comments on commit af342e2

Please sign in to comment.