Fix find_next_time_expression_time (#58894)

* Better tests

* Fix find_next_time_expression_time

* Add tests for Nov 7th 2021, Chicago transtion

* Update event tests

* Update test_event.py

* small performance improvement

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Otto Winter 2021-11-01 17:45:13 +01:00 committed by GitHub
parent 43ccf1d967
commit a9c0f89c09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 489 additions and 64 deletions

View File

@ -272,7 +272,8 @@ def find_next_time_expression_time(
return None
return arr[left]
result = now.replace(microsecond=0)
# Reset microseconds and fold; fold (for ambiguous DST times) will be handled later
result = now.replace(microsecond=0, fold=0)
# Match next second
if (next_second := _lower_bound(seconds, result.second)) is None:
@ -309,40 +310,58 @@ def find_next_time_expression_time(
result = result.replace(hour=next_hour)
if result.tzinfo in (None, UTC):
# Using UTC, no DST checking needed
return result
if _datetime_ambiguous(result):
# This happens when we're leaving daylight saving time and local
# clocks are rolled back. In this case, we want to trigger
# on both the DST and non-DST time. So when "now" is in the DST
# use the DST-on time, and if not, use the DST-off time.
fold = 1 if now.dst() else 0
if result.fold != fold:
result = result.replace(fold=fold)
if not _datetime_exists(result):
# This happens when we're entering daylight saving time and local
# clocks are rolled forward, thus there are local times that do
# not exist. In this case, we want to trigger on the next time
# that *does* exist.
# In the worst case, this will run through all the seconds in the
# time shift, but that's max 3600 operations for once per year
# When entering DST and clocks are turned forward.
# There are wall clock times that don't "exist" (an hour is skipped).
# -> trigger on the next time that 1. matches the pattern and 2. does exist
# for example:
# on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour
# with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day)
# instead run at 02:30 the next day
# We solve this edge case by just iterating one second until the result exists
# (max. 3600 operations, which should be fine for an edge case that happens once a year)
return find_next_time_expression_time(
result + dt.timedelta(seconds=1), seconds, minutes, hours
)
# Another edge-case when leaving DST:
# When now is in DST and ambiguous *and* the next trigger time we *should*
# trigger is ambiguous and outside DST, the excepts above won't catch it.
# For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST)
# we should trigger next on 28.10.2018 2:30 (out of DST), but our
# algorithm above would produce 29.10.2018 2:30 (out of DST)
if _datetime_ambiguous(now):
now_is_ambiguous = _datetime_ambiguous(now)
result_is_ambiguous = _datetime_ambiguous(result)
# When leaving DST and clocks are turned backward.
# Then there are wall clock times that are ambiguous i.e. exist with DST and without DST
# The logic above does not take into account if a given pattern matches _twice_
# in a day.
# Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour
if now_is_ambiguous and result_is_ambiguous:
# `now` and `result` are both ambiguous, so the next match happens
# _within_ the current fold.
# Examples:
# 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00
# 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00
return result.replace(fold=now.fold)
if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous:
# `now` is in the first fold, but result is not ambiguous (meaning it no longer matches
# within the fold).
# -> Check if result matches in the next fold. If so, emit that match
# Turn back the time by the DST offset, effectively run the algorithm on the first fold
# If it matches on the first fold, that means it will also match on the second one.
# Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00
check_result = find_next_time_expression_time(
now + _dst_offset_diff(now), seconds, minutes, hours
)
if _datetime_ambiguous(check_result):
return check_result
return check_result.replace(fold=1)
return result

View File

@ -3399,9 +3399,19 @@ async def test_periodic_task_entering_dst(hass):
dt_util.set_default_time_zone(timezone)
specific_runs = []
now = dt_util.utcnow()
# DST starts early morning March 27th 2022
yy = 2022
mm = 3
dd = 27
# There's no 2022-03-27 02:30, the event should not fire until 2022-03-28 02:30
time_that_will_not_match_right_away = datetime(
now.year + 1, 3, 25, 2, 31, 0, tzinfo=timezone
yy, mm, dd, 1, 28, 0, tzinfo=timezone, fold=0
)
# Make sure we enter DST during the test
assert (
time_that_will_not_match_right_away.utcoffset()
!= (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset()
)
with patch(
@ -3416,25 +3426,25 @@ async def test_periodic_task_entering_dst(hass):
)
async_fire_time_changed(
hass, datetime(now.year + 1, 3, 25, 1, 50, 0, 999999, tzinfo=timezone)
hass, datetime(yy, mm, dd, 1, 50, 0, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 0
async_fire_time_changed(
hass, datetime(now.year + 1, 3, 25, 3, 50, 0, 999999, tzinfo=timezone)
hass, datetime(yy, mm, dd, 3, 50, 0, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 0
async_fire_time_changed(
hass, datetime(now.year + 1, 3, 26, 1, 50, 0, 999999, tzinfo=timezone)
hass, datetime(yy, mm, dd + 1, 1, 50, 0, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 0
async_fire_time_changed(
hass, datetime(now.year + 1, 3, 26, 2, 50, 0, 999999, tzinfo=timezone)
hass, datetime(yy, mm, dd + 1, 2, 50, 0, 999999, tzinfo=timezone)
)
await hass.async_block_till_done()
assert len(specific_runs) == 1
@ -3448,10 +3458,19 @@ async def test_periodic_task_leaving_dst(hass):
dt_util.set_default_time_zone(timezone)
specific_runs = []
now = dt_util.utcnow()
# DST ends early morning Ocotber 30th 2022
yy = 2022
mm = 10
dd = 30
time_that_will_not_match_right_away = datetime(
now.year + 1, 10, 28, 2, 28, 0, tzinfo=timezone, fold=1
yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0
)
# Make sure we leave DST during the test
assert (
time_that_will_not_match_right_away.utcoffset()
!= time_that_will_not_match_right_away.replace(fold=1).utcoffset()
)
with patch(
@ -3465,38 +3484,134 @@ async def test_periodic_task_leaving_dst(hass):
second=0,
)
# The task should not fire yet
async_fire_time_changed(
hass, datetime(now.year + 1, 10, 28, 2, 5, 0, 999999, tzinfo=timezone, fold=0)
hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0)
)
await hass.async_block_till_done()
assert len(specific_runs) == 0
# The task should fire
async_fire_time_changed(
hass, datetime(now.year + 1, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=0)
hass, datetime(yy, mm, dd, 2, 30, 0, 999999, tzinfo=timezone, fold=0)
)
await hass.async_block_till_done()
assert len(specific_runs) == 1
# The task should not fire again
async_fire_time_changed(
hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0)
)
await hass.async_block_till_done()
assert len(specific_runs) == 1
# DST has ended, the task should not fire yet
async_fire_time_changed(
hass,
datetime(now.year + 2, 10, 28, 2, 45, 0, 999999, tzinfo=timezone, fold=1),
datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1),
)
await hass.async_block_till_done()
assert len(specific_runs) == 1
# The task should fire
async_fire_time_changed(
hass,
datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1),
)
await hass.async_block_till_done()
assert len(specific_runs) == 2
# The task should not fire again
async_fire_time_changed(
hass,
datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1),
datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1),
)
await hass.async_block_till_done()
assert len(specific_runs) == 2
# The task should fire again the next day
async_fire_time_changed(
hass, datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1)
hass, datetime(yy, mm, dd + 1, 2, 55, 0, 999999, tzinfo=timezone, fold=1)
)
await hass.async_block_till_done()
assert len(specific_runs) == 3
unsub()
async def test_periodic_task_leaving_dst_2(hass):
"""Test periodic task behavior when leaving dst."""
timezone = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(timezone)
specific_runs = []
# DST ends early morning Ocotber 30th 2022
yy = 2022
mm = 10
dd = 30
time_that_will_not_match_right_away = datetime(
yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0
)
# Make sure we leave DST during the test
assert (
time_that_will_not_match_right_away.utcoffset()
!= time_that_will_not_match_right_away.replace(fold=1).utcoffset()
)
with patch(
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
):
unsub = async_track_time_change(
hass,
callback(lambda x: specific_runs.append(x)),
minute=30,
second=0,
)
# The task should not fire yet
async_fire_time_changed(
hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0)
)
await hass.async_block_till_done()
assert len(specific_runs) == 0
# The task should fire
async_fire_time_changed(
hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0)
)
await hass.async_block_till_done()
assert len(specific_runs) == 1
# DST has ended, the task should not fire yet
async_fire_time_changed(
hass, datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1)
)
await hass.async_block_till_done()
assert len(specific_runs) == 1
# The task should fire
async_fire_time_changed(
hass, datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1)
)
await hass.async_block_till_done()
assert len(specific_runs) == 2
# The task should not fire again
async_fire_time_changed(
hass,
datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1),
)
await hass.async_block_till_done()
assert len(specific_runs) == 2
# The task should fire again the next hour
async_fire_time_changed(
hass, datetime(yy, mm, dd, 3, 55, 0, 999999, tzinfo=timezone, fold=0)
)
await hass.async_block_till_done()
assert len(specific_runs) == 3
unsub()

View File

@ -224,120 +224,411 @@ def test_find_next_time_expression_time_dst():
tz = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(tz)
def find(dt, hour, minute, second):
def find(dt, hour, minute, second) -> datetime:
"""Call test_find_next_time_expression_time."""
seconds = dt_util.parse_time_expression(second, 0, 59)
minutes = dt_util.parse_time_expression(minute, 0, 59)
hours = dt_util.parse_time_expression(hour, 0, 23)
return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours)
local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours)
return dt_util.as_utc(local)
# Entering DST, clocks are rolled forward
assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find(
datetime(2018, 3, 25, 1, 50, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find(
datetime(2018, 3, 25, 3, 50, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find(
datetime(2018, 3, 26, 1, 50, 0, tzinfo=tz), 2, 30, 0
)
# Leaving DST, clocks are rolled back
assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find(
assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find(
datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0
)
assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find(
assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find(
datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0
)
assert datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0) == find(
assert dt_util.as_utc(datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0)) == find(
datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0
)
assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0
)
assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0
)
# DST begins on 2021.03.28 2:00, clocks were turned forward 1h; 2:00-3:00 time does not exist
@pytest.mark.parametrize(
"now_dt, expected_dt",
[
# 00:00 -> 2:30
(
datetime(2021, 3, 28, 0, 0, 0),
datetime(2021, 3, 29, 2, 30, 0),
),
],
)
def test_find_next_time_expression_entering_dst(now_dt, expected_dt):
"""Test entering daylight saving time for find_next_time_expression_time."""
tz = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(tz)
# match on 02:30:00 every day
pattern_seconds = dt_util.parse_time_expression(0, 0, 59)
pattern_minutes = dt_util.parse_time_expression(30, 0, 59)
pattern_hours = dt_util.parse_time_expression(2, 0, 59)
now_dt = now_dt.replace(tzinfo=tz)
expected_dt = expected_dt.replace(tzinfo=tz)
res_dt = dt_util.find_next_time_expression_time(
now_dt, pattern_seconds, pattern_minutes, pattern_hours
)
assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt)
# DST ends on 2021.10.31 2:00, clocks were turned backward 1h; 2:00-3:00 time is ambiguous
@pytest.mark.parametrize(
"now_dt, expected_dt",
[
# 00:00 -> 2:30
(
datetime(2021, 10, 31, 0, 0, 0),
datetime(2021, 10, 31, 2, 30, 0, fold=0),
),
# 02:00(0) -> 2:30(0)
(
datetime(2021, 10, 31, 2, 0, 0, fold=0),
datetime(2021, 10, 31, 2, 30, 0, fold=0),
),
# 02:15(0) -> 2:30(0)
(
datetime(2021, 10, 31, 2, 15, 0, fold=0),
datetime(2021, 10, 31, 2, 30, 0, fold=0),
),
# 02:30:00(0) -> 2:30(1)
(
datetime(2021, 10, 31, 2, 30, 0, fold=0),
datetime(2021, 10, 31, 2, 30, 0, fold=0),
),
# 02:30:01(0) -> 2:30(1)
(
datetime(2021, 10, 31, 2, 30, 1, fold=0),
datetime(2021, 10, 31, 2, 30, 0, fold=1),
),
# 02:45(0) -> 2:30(1)
(
datetime(2021, 10, 31, 2, 45, 0, fold=0),
datetime(2021, 10, 31, 2, 30, 0, fold=1),
),
# 02:00(1) -> 2:30(1)
(
datetime(2021, 10, 31, 2, 0, 0, fold=1),
datetime(2021, 10, 31, 2, 30, 0, fold=1),
),
# 02:15(1) -> 2:30(1)
(
datetime(2021, 10, 31, 2, 15, 0, fold=1),
datetime(2021, 10, 31, 2, 30, 0, fold=1),
),
# 02:30:00(1) -> 2:30(1)
(
datetime(2021, 10, 31, 2, 30, 0, fold=1),
datetime(2021, 10, 31, 2, 30, 0, fold=1),
),
# 02:30:01(1) -> 2:30 next day
(
datetime(2021, 10, 31, 2, 30, 1, fold=1),
datetime(2021, 11, 1, 2, 30, 0),
),
# 02:45(1) -> 2:30 next day
(
datetime(2021, 10, 31, 2, 45, 0, fold=1),
datetime(2021, 11, 1, 2, 30, 0),
),
# 08:00(1) -> 2:30 next day
(
datetime(2021, 10, 31, 8, 0, 1),
datetime(2021, 11, 1, 2, 30, 0),
),
],
)
def test_find_next_time_expression_exiting_dst(now_dt, expected_dt):
"""Test exiting daylight saving time for find_next_time_expression_time."""
tz = dt_util.get_time_zone("Europe/Vienna")
dt_util.set_default_time_zone(tz)
# match on 02:30:00 every day
pattern_seconds = dt_util.parse_time_expression(0, 0, 59)
pattern_minutes = dt_util.parse_time_expression(30, 0, 59)
pattern_hours = dt_util.parse_time_expression(2, 0, 59)
now_dt = now_dt.replace(tzinfo=tz)
expected_dt = expected_dt.replace(tzinfo=tz)
res_dt = dt_util.find_next_time_expression_time(
now_dt, pattern_seconds, pattern_minutes, pattern_hours
)
assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt)
def test_find_next_time_expression_time_dst_chicago():
"""Test daylight saving time for find_next_time_expression_time."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
def find(dt, hour, minute, second):
def find(dt, hour, minute, second) -> datetime:
"""Call test_find_next_time_expression_time."""
seconds = dt_util.parse_time_expression(second, 0, 59)
minutes = dt_util.parse_time_expression(minute, 0, 59)
hours = dt_util.parse_time_expression(hour, 0, 23)
return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours)
local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours)
return dt_util.as_utc(local)
# Entering DST, clocks are rolled forward
assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find(
datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find(
datetime(2021, 3, 14, 3, 50, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find(
datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz)) == find(
datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 3, 30, 0
)
# Leaving DST, clocks are rolled back
assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find(
assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find(
datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0
)
assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz)) == find(
datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find(
assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find(
datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2021, 11, 7, 2, 10, 0, tzinfo=tz), 2, 30, 0
)
assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0), 2, 30, 0
)
assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0
)
assert datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0) == find(
assert dt_util.as_utc(datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0)) == find(
datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0
)
assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find(
assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find(
datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0
)
assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz) == find(
assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz)) == find(
datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0
)
def _get_matches(hours, minutes, seconds):
matching_hours = dt_util.parse_time_expression(hours, 0, 23)
matching_minutes = dt_util.parse_time_expression(minutes, 0, 59)
matching_seconds = dt_util.parse_time_expression(seconds, 0, 59)
return matching_hours, matching_minutes, matching_seconds
def test_find_next_time_expression_day_before_dst_change_the_same_time():
"""Test the day before DST to establish behavior without DST."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
# Not in DST yet
hour_minute_second = (12, 30, 1)
test_time = datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0)
matching_hours, matching_minutes, matching_seconds = _get_matches(
*hour_minute_second
)
next_time = dt_util.find_next_time_expression_time(
test_time, matching_seconds, matching_minutes, matching_hours
)
assert next_time == datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0)
assert next_time.fold == 0
assert dt_util.as_utc(next_time) == datetime(
2021, 10, 7, 17, 30, 1, tzinfo=dt_util.UTC
)
def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_30_s():
"""Test leaving daylight saving time for find_next_time_expression_time 30s into the future."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
# Leaving DST, clocks are rolled back
# Move ahead 30 seconds not folded yet
hour_minute_second = (1, 30, 31)
test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0)
matching_hours, matching_minutes, matching_seconds = _get_matches(
*hour_minute_second
)
next_time = dt_util.find_next_time_expression_time(
test_time, matching_seconds, matching_minutes, matching_hours
)
assert next_time == datetime(2021, 11, 7, 1, 30, 31, tzinfo=tz, fold=0)
assert dt_util.as_utc(next_time) == datetime(
2021, 11, 7, 6, 30, 31, tzinfo=dt_util.UTC
)
assert next_time.fold == 0
def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_same_time():
"""Test leaving daylight saving time for find_next_time_expression_time with the same time."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
# Leaving DST, clocks are rolled back
# Move to the same time not folded yet
hour_minute_second = (0, 30, 1)
test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0)
matching_hours, matching_minutes, matching_seconds = _get_matches(
*hour_minute_second
)
next_time = dt_util.find_next_time_expression_time(
test_time, matching_seconds, matching_minutes, matching_hours
)
assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0)
assert dt_util.as_utc(next_time) == datetime(
2021, 11, 7, 5, 30, 1, tzinfo=dt_util.UTC
)
assert next_time.fold == 0
def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_same_time():
"""Test leaving daylight saving time for find_next_time_expression_time."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
# Leaving DST, clocks are rolled back
# Find the same time inside the fold
hour_minute_second = (1, 30, 1)
test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0)
matching_hours, matching_minutes, matching_seconds = _get_matches(
*hour_minute_second
)
next_time = dt_util.find_next_time_expression_time(
test_time, matching_seconds, matching_minutes, matching_hours
)
assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1)
assert next_time.fold == 0
assert dt_util.as_utc(next_time) == datetime(
2021, 11, 7, 6, 30, 1, tzinfo=dt_util.UTC
)
def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_ahead_1_hour_10_min():
"""Test leaving daylight saving time for find_next_time_expression_time."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
# Leaving DST, clocks are rolled back
# Find 1h 10m after into the fold
# Start at 01:30:01 fold=0
# Reach to 01:20:01 fold=1
hour_minute_second = (1, 20, 1)
test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0)
matching_hours, matching_minutes, matching_seconds = _get_matches(
*hour_minute_second
)
next_time = dt_util.find_next_time_expression_time(
test_time, matching_seconds, matching_minutes, matching_hours
)
assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1)
assert next_time.fold == 1 # time is ambiguous
assert dt_util.as_utc(next_time) == datetime(
2021, 11, 7, 7, 20, 1, tzinfo=dt_util.UTC
)
def test_find_next_time_expression_time_leave_dst_chicago_inside_the_fold_ahead_10_min():
"""Test leaving daylight saving time for find_next_time_expression_time."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
# Leaving DST, clocks are rolled back
# Find 10m later while we are in the fold
# Start at 01:30:01 fold=0
# Reach to 01:40:01 fold=1
hour_minute_second = (1, 40, 1)
test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=1)
matching_hours, matching_minutes, matching_seconds = _get_matches(
*hour_minute_second
)
next_time = dt_util.find_next_time_expression_time(
test_time, matching_seconds, matching_minutes, matching_hours
)
assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1)
assert next_time.fold == 1 # time is ambiguous
assert dt_util.as_utc(next_time) == datetime(
2021, 11, 7, 7, 40, 1, tzinfo=dt_util.UTC
)
def test_find_next_time_expression_time_leave_dst_chicago_past_the_fold_ahead_2_hour_10_min():
"""Test leaving daylight saving time for find_next_time_expression_time."""
tz = dt_util.get_time_zone("America/Chicago")
dt_util.set_default_time_zone(tz)
# Leaving DST, clocks are rolled back
# Find 1h 10m after into the fold
# Start at 01:30:01 fold=0
# Reach to 02:20:01 past the fold
hour_minute_second = (2, 20, 1)
test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0)
matching_hours, matching_minutes, matching_seconds = _get_matches(
*hour_minute_second
)
next_time = dt_util.find_next_time_expression_time(
test_time, matching_seconds, matching_minutes, matching_hours
)
assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1)
assert next_time.fold == 0 # Time is no longer ambiguous
assert dt_util.as_utc(next_time) == datetime(
2021, 11, 7, 8, 20, 1, tzinfo=dt_util.UTC
)