Skip to content
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 @@ -22,6 +22,9 @@ Pint Changelog
- Fix return type of `PlainQuantity.to` (#2088)
- Update constants to CODATA 2022 recommended values. (#2049)
- Fixed issue with `.to_compact` and Magnitudes with uncertainties / Quantities with units (PR #2069, issue #2044)
- `Quantity` now converts `datetime.timedelta` objects to seconds or specified units when
initializing a `Quantity` with a `datetime.timedelta` value.
(PR #1978)
- Fixed issue in unit conversion which led to loss of precision when using `decimal`. (#2107)
- Add conductivity dimension. (#2112)
- Add absorbance unit and dimension. (#2114)
Expand Down
4 changes: 4 additions & 0 deletions pint/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ class BehaviorChangeWarning(UserWarning):
log, # noqa: F401
ndarray,
)
from numpy import timedelta64 as np_timedelta64

NUMPY_VER = np.__version__
if HAS_UNCERTAINTIES:
Expand Down Expand Up @@ -327,6 +328,9 @@ class ndarray:
class np_datetime64:
pass

class np_timedelta64:
pass

from math import (
exp, # noqa: F401
log, # noqa: F401
Expand Down
39 changes: 38 additions & 1 deletion pint/facets/numpy/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@

from __future__ import annotations

import datetime
import functools
import math
import warnings
from typing import Any, Generic

from ..._typing import Shape
from ...compat import HAS_NUMPY, _to_magnitude, np
from ...compat import (
HAS_NUMPY,
_to_magnitude,
is_duck_array,
np,
np_timedelta64,
)
from ...errors import DimensionalityError, PintTypeError, UnitStrippedWarning
from ..plain import MagnitudeT, PlainQuantity
from .numpy_func import (
Expand Down Expand Up @@ -304,3 +311,33 @@ def __setitem__(self, key, value):
f"Neither Quantity object nor its magnitude ({self._magnitude}) "
"supports indexing"
) from exc

def _is_timedelta(self, value) -> bool:
return (
isinstance(value, datetime.timedelta)
or isinstance(value, np_timedelta64)
or (is_duck_array(value) and value.dtype.type == np_timedelta64)
)

def _convert_timedelta(self, value: 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(value, datetime.timedelta):
return value.total_seconds(), "seconds"
elif isinstance(value, np_timedelta64) or value.dtype.type == np_timedelta64:
return value.astype(float), _dtype_to_unit[str(value.dtype)]
raise TypeError(f"Cannot convert {value!r} to seconds.")
10 changes: 9 additions & 1 deletion pint/facets/plain/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,16 @@ def __new__(cls, value, units=None):

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

inst = SharedRegistryObject().__new__(cls)

if cls._is_timedelta(cls, value):
m, u = cls._convert_timedelta(cls, 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 @@ -1924,6 +1924,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
Loading