Add template as_timedelta (#71801)

This commit is contained in:
Kevin Stillhammer 2022-05-23 19:32:22 +02:00 committed by GitHub
parent 92582beeff
commit 90e5d69184
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 0 deletions

View File

@ -1535,6 +1535,11 @@ def as_datetime(value):
return dt_util.parse_datetime(value)
def as_timedelta(value: str) -> timedelta | None:
"""Parse a ISO8601 duration like 'PT10M' to a timedelta."""
return dt_util.parse_duration(value)
def strptime(string, fmt, default=_SENTINEL):
"""Parse a time string to datetime."""
try:
@ -1902,6 +1907,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.filters["atan2"] = arc_tangent2
self.filters["sqrt"] = square_root
self.filters["as_datetime"] = as_datetime
self.filters["as_timedelta"] = as_timedelta
self.filters["as_timestamp"] = forgiving_as_timestamp
self.filters["today_at"] = today_at
self.filters["as_local"] = dt_util.as_local
@ -1947,6 +1953,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["float"] = forgiving_float
self.globals["as_datetime"] = as_datetime
self.globals["as_local"] = dt_util.as_local
self.globals["as_timedelta"] = as_timedelta
self.globals["as_timestamp"] = forgiving_as_timestamp
self.globals["today_at"] = today_at
self.globals["relative_time"] = relative_time

View File

@ -28,6 +28,49 @@ DATETIME_RE = re.compile(
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
)
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
STANDARD_DURATION_RE = re.compile(
r"^"
r"(?:(?P<days>-?\d+) (days?, )?)?"
r"(?P<sign>-?)"
r"((?:(?P<hours>\d+):)(?=\d+:\d+))?"
r"(?:(?P<minutes>\d+):)?"
r"(?P<seconds>\d+)"
r"(?:[\.,](?P<microseconds>\d{1,6})\d{0,6})?"
r"$"
)
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
ISO8601_DURATION_RE = re.compile(
r"^(?P<sign>[-+]?)"
r"P"
r"(?:(?P<days>\d+([\.,]\d+)?)D)?"
r"(?:T"
r"(?:(?P<hours>\d+([\.,]\d+)?)H)?"
r"(?:(?P<minutes>\d+([\.,]\d+)?)M)?"
r"(?:(?P<seconds>\d+([\.,]\d+)?)S)?"
r")?"
r"$"
)
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
POSTGRES_INTERVAL_RE = re.compile(
r"^"
r"(?:(?P<days>-?\d+) (days? ?))?"
r"(?:(?P<sign>[-+])?"
r"(?P<hours>\d+):"
r"(?P<minutes>\d\d):"
r"(?P<seconds>\d\d)"
r"(?:\.(?P<microseconds>\d{1,6}))?"
r")?$"
)
def set_default_time_zone(time_zone: dt.tzinfo) -> None:
"""Set a default time zone to be used when none is specified.
@ -171,6 +214,35 @@ def parse_date(dt_str: str) -> dt.date | None:
return None
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
def parse_duration(value: str) -> dt.timedelta | None:
"""Parse a duration string and return a datetime.timedelta.
Also supports ISO 8601 representation and PostgreSQL's day-time interval
format.
"""
match = (
STANDARD_DURATION_RE.match(value)
or ISO8601_DURATION_RE.match(value)
or POSTGRES_INTERVAL_RE.match(value)
)
if match:
kws = match.groupdict()
sign = -1 if kws.pop("sign", "+") == "-" else 1
if kws.get("microseconds"):
kws["microseconds"] = kws["microseconds"].ljust(6, "0")
time_delta_args: dict[str, float] = {
k: float(v.replace(",", ".")) for k, v in kws.items() if v is not None
}
days = dt.timedelta(float(time_delta_args.pop("days", 0.0) or 0.0))
if match.re == ISO8601_DURATION_RE:
days *= sign
return days + sign * dt.timedelta(**time_delta_args)
return None
def parse_time(time_str: str) -> dt.time | None:
"""Parse a time string (00:20:00) into Time object.

View File

@ -3388,6 +3388,18 @@ def test_urlencode(hass):
assert tpl.async_render() == "the%20quick%20brown%20fox%20%3D%20true"
def test_as_timedelta(hass: HomeAssistant) -> None:
"""Test the as_timedelta function/filter."""
tpl = template.Template("{{ as_timedelta('PT10M') }}", hass)
assert tpl.async_render() == "0:10:00"
tpl = template.Template("{{ 'PT10M' | as_timedelta }}", hass)
assert tpl.async_render() == "0:10:00"
tpl = template.Template("{{ 'T10M' | as_timedelta }}", hass)
assert tpl.async_render() is None
def test_iif(hass: HomeAssistant) -> None:
"""Test the immediate if function/filter."""
tpl = template.Template("{{ (1 == 1) | iif }}", hass)

View File

@ -1,4 +1,6 @@
"""Test Home Assistant date util methods."""
from __future__ import annotations
from datetime import datetime, timedelta
import pytest
@ -142,6 +144,32 @@ def test_parse_datetime_returns_none_for_incorrect_format():
assert dt_util.parse_datetime("not a datetime string") is None
@pytest.mark.parametrize(
"duration_string,expected_result",
[
("PT10M", timedelta(minutes=10)),
("PT0S", timedelta(0)),
("P10DT11H11M01S", timedelta(days=10, hours=11, minutes=11, seconds=1)),
(
"4 1:20:30.111111",
timedelta(days=4, hours=1, minutes=20, seconds=30, microseconds=111111),
),
("4 1:2:30", timedelta(days=4, hours=1, minutes=2, seconds=30)),
("3 days 04:05:06", timedelta(days=3, hours=4, minutes=5, seconds=6)),
("P1YT10M", None),
("P1MT10M", None),
("1MT10M", None),
("P1MT100M", None),
("P1234", None),
],
)
def test_parse_duration(
duration_string: str, expected_result: timedelta | None
) -> None:
"""Test that parse_duration returns the expected result."""
assert dt_util.parse_duration(duration_string) == expected_result
def test_get_age():
"""Test get_age."""
diff = dt_util.now() - timedelta(seconds=0)