Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support timedelta in Q_.init #1978

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Pint Changelog
- Add `dim_sort` function to _formatter_helpers.
- Add `dim_order` and `default_sort_func` properties to FullFormatter.
(PR #1926, fixes Issue #1841)
- `Quantity` now converts `datetime.timedelta` objects to seconds or specified units when
initializing a `Quantity` with a `datetime.timedelta` value.
(PR #1978)


0.23 (2023-12-08)
Expand Down
41 changes: 41 additions & 0 deletions pint/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from __future__ import annotations

import datetime
import math
import sys
from collections.abc import Callable, Iterable, Mapping
Expand All @@ -19,6 +20,7 @@
from typing import (
Any,
NoReturn,
Tuple,
TypeAlias, # noqa
)

Expand Down Expand Up @@ -84,6 +86,7 @@ class BehaviorChangeWarning(UserWarning):
import numpy as np
from numpy import datetime64 as np_datetime64
from numpy import ndarray
from numpy import timedelta64 as np_timedelta64

HAS_NUMPY = True
NUMPY_VER = np.__version__
Expand Down Expand Up @@ -136,6 +139,9 @@ class ndarray:
class np_datetime64:
pass

class np_timedelta64:
pass

HAS_NUMPY = False
NUMPY_VER = "0"
NUMERIC_TYPES = (Number, Decimal)
Expand Down Expand Up @@ -293,6 +299,41 @@ def is_duck_array_type(cls: type) -> bool:
)


def is_timedelta(obj: Any) -> bool:
"""Check if the object is a datetime object."""
return isinstance(obj, datetime.timedelta) or isinstance(obj, np_timedelta64)


def is_timedelta_array(obj: Any) -> bool:
"""Check if the object is a datetime array."""
if isinstance(obj, ndarray) and obj.dtype.type == np_timedelta64:
return True


def convert_timedelta(obj: Any) -> Tuple[float, str]:
"""Convert a timedelta object to magnitude and unit string."""
_dtype_to_unit = {
"timedelta64[Y]": "year",
"timedelta64[M]": "month",
"timedelta64[W]": "week",
"timedelta64[D]": "day",
"timedelta64[h]": "hour",
"timedelta64[m]": "minute",
"timedelta64[s]": "s",
"timedelta64[ms]": "ms",
"timedelta64[us]": "us",
"timedelta64[ns]": "ns",
"timedelta64[ps]": "ps",
"timedelta64[fs]": "fs",
"timedelta64[as]": "as",
}
if isinstance(obj, datetime.timedelta):
return obj.total_seconds(), "s"
elif isinstance(obj, np_timedelta64) or obj.dtype.type == np_timedelta64:
return obj.astype(float), _dtype_to_unit[str(obj.dtype)]
raise TypeError(f"Cannot convert {obj!r} to seconds.")


def is_duck_array(obj: type) -> bool:
"""Check if an object represents a (non-Quantity) duck array type."""
return is_duck_array_type(type(obj))
Expand Down
13 changes: 12 additions & 1 deletion pint/facets/plain/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
from ...compat import (
HAS_NUMPY,
_to_magnitude,
convert_timedelta,
deprecated,
eq,
is_duck_array_type,
is_timedelta,
is_timedelta_array,
is_upcast_type,
np,
zero_or_nan,
Expand Down Expand Up @@ -201,8 +204,16 @@ def __new__(cls, value, units=None):

if units is None and isinstance(value, cls):
return copy.copy(value)

inst = SharedRegistryObject().__new__(cls)

if is_timedelta(value) or is_timedelta_array(value):
m, u = convert_timedelta(value)
inst._magnitude = m
inst._units = inst.UnitsContainer({u: 1})
if units:
inst.ito(units)
return inst

if units is None:
units = inst.UnitsContainer()
else:
Expand Down
60 changes: 60 additions & 0 deletions pint/testsuite/test_quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -1884,6 +1884,66 @@ def test_iadd_isub(self):
with pytest.raises(DimensionalityError):
after -= d

def test_init_quantity(self):
# 608
td = datetime.timedelta(seconds=3)
assert self.Q_(td) == 3 * self.ureg.second
q_hours = self.Q_(td, "hours")
assert q_hours == 3 * self.ureg.second
assert q_hours.units == self.ureg.hour

@pytest.mark.parametrize(
["timedelta_unit", "pint_unit"],
(
pytest.param("s", "second", id="second"),
pytest.param("ms", "millisecond", id="millisecond"),
pytest.param("us", "microsecond", id="microsecond"),
pytest.param("ns", "nanosecond", id="nanosecond"),
pytest.param("m", "minute", id="minute"),
pytest.param("h", "hour", id="hour"),
pytest.param("D", "day", id="day"),
pytest.param("W", "week", id="week"),
pytest.param("M", "month", id="month"),
pytest.param("Y", "year", id="year"),
),
)
@helpers.requires_numpy
def test_init_quantity_np(self, timedelta_unit, pint_unit):
# test init with the timedelta unit
td = np.timedelta64(3, timedelta_unit)
result = self.Q_(td)
expected = self.Q_(3, pint_unit)
helpers.assert_quantity_almost_equal(result, expected)
# check units are same. Use Q_ since Unit(s) != Unit(second)
helpers.assert_quantity_almost_equal(
self.Q_(1, result.units), self.Q_(1, expected.units)
)

# test init with unit specified
result = self.Q_(td, "hours")
expected = self.Q_(3, pint_unit).to("hours")
helpers.assert_quantity_almost_equal(result, expected)
helpers.assert_quantity_almost_equal(
self.Q_(1, result.units), self.Q_(1, expected.units)
)

# test array
td = np.array([3], dtype="timedelta64[{}]".format(timedelta_unit))
result = self.Q_(td)
expected = self.Q_([3], pint_unit)
helpers.assert_quantity_almost_equal(result, expected)
helpers.assert_quantity_almost_equal(
self.Q_(1, result.units), self.Q_(1, expected.units)
)

# test array with unit specified
result = self.Q_(td, "hours")
expected = self.Q_([3], pint_unit).to("hours")
helpers.assert_quantity_almost_equal(result, expected)
helpers.assert_quantity_almost_equal(
self.Q_(1, result.units), self.Q_(1, expected.units)
)


# TODO: do not subclass from QuantityTestCase
class TestCompareNeutral(QuantityTestCase):
Expand Down