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 3 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
25 changes: 25 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 Down Expand Up @@ -84,6 +85,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 +138,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 +298,26 @@ 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 == np_timedelta64:
return True


def to_seconds(obj: Any) -> float:
"""Convert a timedelta object to seconds."""
if isinstance(obj, datetime.timedelta):
return obj.total_seconds()
elif isinstance(obj, np_timedelta64) or obj.dtype == np_timedelta64:
return obj.astype(float)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not going to work in general: timedelta64 has its own units, which can range from days (or years?) to nanoseconds, and astype(float) is the equivalent of magnitude. I don't have any advice on how to extract that, though, other than parsing the string repr of the 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
12 changes: 11 additions & 1 deletion pint/facets/plain/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@
deprecated,
eq,
is_duck_array_type,
is_timedelta,
is_timedelta_array,
is_upcast_type,
np,
to_seconds,
zero_or_nan,
)
from ...errors import DimensionalityError, OffsetUnitCalculusError, PintTypeError
Expand Down Expand Up @@ -203,7 +206,9 @@ def __new__(cls, value, units=None):
return copy.copy(value)

inst = SharedRegistryObject().__new__(cls)
if units is None:
if units is None and (is_timedelta(value) or is_timedelta_array(value)):
units = inst.UnitsContainer({"s": 1})
elif units is None:
units = inst.UnitsContainer()
else:
if isinstance(units, (UnitsContainer, UnitDefinition)):
Expand All @@ -223,6 +228,11 @@ def __new__(cls, value, units=None):
"units must be of type str, PlainQuantity or "
"UnitsContainer; not {}.".format(type(units))
)
if is_timedelta(value) or is_timedelta_array(value):
inst._magnitude = to_seconds(value)
Copy link
Contributor

@keewis keewis May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to special-case numpy.timedelta64, probably in the numpy facet? That way we could roundtrip exactly

inst._units = inst.UnitsContainer({"s": 1})
return inst.to(units)

if isinstance(value, cls):
magnitude = value.to(units)._magnitude
else:
Expand Down
22 changes: 22 additions & 0 deletions pint/testsuite/test_quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -1884,6 +1884,28 @@ 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

@helpers.requires_numpy
def test_init_quantity_np(self):
td = np.timedelta64(3, "s")
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

td = np.array([3], dtype="timedelta64")
assert self.Q_(td) == np.array([3]) * self.ureg.second
q_hours = self.Q_(td, "hours")
assert q_hours == np.array([3]) * self.ureg.second
assert q_hours.units == self.ureg.hour


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