mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Add template as_timedelta (#71801)
This commit is contained in:
parent
92582beeff
commit
90e5d69184
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user