import datetime
import typing
import pytz
from . import exceptions
__all__ = [
"convert_sp_and_date_to_utc",
"convert_utc_to_sp_and_date",
"number_of_periods_in_timedelta",
]
# Time zones implemented
UTC_TZ = "UTC"
GB_TZ = "Europe/London"
def _to_timezone(local_time: datetime.datetime, timezone_str: str) -> datetime.datetime:
"""
Converts an aware datetime to another time zone.
"""
timezone = pytz.timezone(timezone_str)
# Rebasing to local time zone
rebased_local_time = local_time.astimezone(timezone)
# Normalizing for daylight saving rules
return timezone.normalize(rebased_local_time)
def _get_first_delivery_time(
date: datetime.date, timezone_str: str, is_wholesale: bool
) -> datetime.datetime:
"""
Return an aware datetime for the start of the first settlement period on a given date.
"""
midnight = datetime.datetime(date.year, date.month, date.day)
offset_midnight = midnight - datetime.timedelta(hours=1)
# First settlement period
cases = {
# UTC: first delivery starts at 00:00:00 UTC on the day
UTC_TZ: midnight,
# GB retail: first delivery starts at 00:00:00 Europe/London on the day
# GB wholesale: first delivery starts at 23:00:00 Europe/London on the previous day
GB_TZ: offset_midnight if is_wholesale else midnight,
}
# Time zone aware
try:
return pytz.timezone(timezone_str).localize(cases[timezone_str])
except AttributeError:
raise exceptions.SettlementPeriodError("Time zone not implemented")
def _get_delivery_date(
local_time: datetime.datetime, timezone_str: str, is_wholesale: bool
) -> datetime.date:
"""
Return the date of the settlement period relative to a tz-aware datetime.
"""
period_date = local_time.date()
offset_period_date = period_date
if local_time.hour >= 23:
offset_period_date += datetime.timedelta(days=1)
# Date of the first settlement period
cases = {
# UTC: first delivery starts at 00:00:00 UTC on the day
UTC_TZ: period_date,
# GB retail: first delivery starts at 00:00:00 Europe/London on the day
# GB wholesale: first delivery starts at 23:00:00 Europe/London on the previous day
GB_TZ: offset_period_date if is_wholesale else period_date,
}
# Time zone aware
try:
return cases[timezone_str]
except AttributeError:
raise exceptions.SettlementPeriodError("Time zone not implemented")
def _round_local_down_to_hh(local_time: datetime.datetime) -> datetime.datetime:
if local_time.minute < 30:
return local_time - datetime.timedelta(minutes=local_time.minute)
else:
return local_time - datetime.timedelta(minutes=local_time.minute - 30)
def convert_sp_and_date_to_local(
sp: int, date: datetime.date, timezone_str: str, is_wholesale: bool
) -> datetime.datetime:
"""
Return an aware datetime for the start of a given settlement period.
"""
if sp not in range(1, 51):
raise exceptions.SettlementPeriodError("Settlement period not valid")
# First settlement period in the time zone
first_period_start = _get_first_delivery_time(date, timezone_str, is_wholesale)
# Start of the settlement period in local time
local_time = first_period_start + datetime.timedelta(minutes=30 * (sp - 1))
# Normalizing for daylight saving rules
return pytz.timezone(timezone_str).normalize(local_time)
[docs]
def convert_sp_and_date_to_utc(
sp: int, date: datetime.date, timezone_str: str = GB_TZ, is_wholesale: bool = False
) -> datetime.datetime:
"""
Return an UTC-aware datetime for the start of a given settlement period.
"""
local_time = convert_sp_and_date_to_local(sp, date, timezone_str, is_wholesale)
return _to_timezone(local_time, UTC_TZ).replace(tzinfo=datetime.timezone.utc)
def convert_local_to_sp_and_date(
local_time: datetime.datetime, is_wholesale: bool = False
) -> typing.Tuple[int, datetime.date]:
"""
Return the date and settlement period from a given tz-aware datetime.
"""
try:
timezone_str = str(local_time.tzinfo)
except AttributeError:
raise exceptions.SettlementPeriodError("Not a tz-aware datetime")
# Round to the nearest half hour
half_hourly_time = _round_local_down_to_hh(local_time)
# Date of the settlement period in the time zone
delivery_date = _get_delivery_date(half_hourly_time, timezone_str, is_wholesale)
# First settlement period in the time zone
first_delivery_time = _get_first_delivery_time(
delivery_date, timezone_str, is_wholesale
)
# Fetch settlement period
delta = half_hourly_time - first_delivery_time
settlement_period = ((int(delta.total_seconds()) // 60) + 30) // 30
return settlement_period, delivery_date
[docs]
def convert_utc_to_sp_and_date(
utc_time: datetime.datetime, timezone_str: str = GB_TZ, is_wholesale: bool = False
) -> typing.Tuple[int, datetime.date]:
"""
Return the local date and settlement period from a given UTC-aware datetime.
"""
local_time = _to_timezone(utc_time, timezone_str)
return convert_local_to_sp_and_date(local_time, is_wholesale)
[docs]
def number_of_periods_in_timedelta(delta: datetime.timedelta) -> float:
"""
Return the number of half-hourly settlement periods in the given timedelta
"""
return (delta.total_seconds() / 60) / 30