Repository: BAMWelDX/weldx

Issue:
Time class
We are currently supporting multiple time formats in several places:
- pandas (`DateTimeIndex`, `TimedeltaIndex`, `Timestamp`)
- numpy (time dtypes)
- pint quantities
- xarray structures with "time" and "time_ref" fields

Dealing with time as an additional dimension can actually be quite challenging when trying to enhance, understand or write new code. But also having to deal with those different types turns it into hell. For example, if I just want to compare if the last entry of a time array is less than the first entry of another one I need to figure out if I have relative (time delta) or absolute (dates) values and if they have matching types. Then I need to treat all cases separately. This bloats the code and makes it hard to understand. 

Furthermore, the `time` property of our classes is not consistent regarding its return type. For example, the LCS returns a xarray structure, while the `TimeSeries` returns a `TimedeltaIndex`. The LCS also provides a special method to get time as a quantity to take away the need for type casts from the user.
 
My suggestion is, that we define a `Time` wrapper class, that serves multiple purposes. First, it should be an interface class that is always used when any public function returns a time type. Second, it should be constructable from all the classes that we currently use and provide methods to cast back to all those types like `WXRotation` does for rotations. Lastly, it should provide all the functionalities that we frequently need like calculating time unions and synchronizing reference times.

I would also like to keep time deltas and date times in a single class instead of having two separate classes (possibly three with timestamps). The class method implementations should deal with the compatibility of relative and absolute times so that we do not have to cover it separately every time we deal with time formats.

To make a suggestion of some of the possible interfaces:

~~~ python
class WXTime:
   
    def __init__(
        self,
        time : Union[DateTimeTypes, TimedeltaTypes, TimestampTypes, WXTime],
        time_ref : Union[TimestampTypes, WXTime],
    )

    def to_pandas(self) -> Union[TimedeltaIndex, DateTimeIndex, Timestamp]:

    def to_quantity(self) -> Quantity:

    @property
    def length(self) -> int:

    @property
    def has_reference_time(self) -> bool:

    @property
    def is_timestamp(self) -> bool:
        return self.lengt == 1 and self.has_reference_time

    @property
    def reference_time() -> WXTime:

    @reference_time.setter
    def reference_time( time_ref : Union[TimestampTypes, WXTime]):

    @staticmethod
    def time_union(times = List[WXTime]) -> WXTime:

    def __eq__(self, other:  Union[DateTimeTypes, TimedeltaTypes, TimestampTypes, WXTime]) ->bool:

    def  __le__(self, other:  Union[DateTimeTypes, TimedeltaTypes, TimestampTypes, WXTime]) ->bool:

    def all_close(self, other:  Union[DateTimeTypes, TimedeltaTypes, TimestampTypes, WXTime]) ->bool:

    def overlaps_with(self, other:  Union[DateTimeTypes, TimedeltaTypes, TimestampTypes, WXTime]) ->bool:
~~~

I think this will make our life and of our possible users a lot easier since it bundles all the time-related stuff in one place and unifies the code. What do you think?

Discussion:
pep8speaks: Hello @vhirtham! Thanks for updating this PR. 

* In the file [`weldx/time.py`](https://github.com/BAMWelDX/weldx/blob/b482a03747e68c5c28dbb01e61a104292033098e/weldx/time.py):

> [Line 81:1](https://github.com/BAMWelDX/weldx/blob/b482a03747e68c5c28dbb01e61a104292033098e/weldx/time.py#L81): [W293](https://duckduckgo.com/?q=pep8%20W293) blank line contains whitespace
> [Line 83:5](https://github.com/BAMWelDX/weldx/blob/b482a03747e68c5c28dbb01e61a104292033098e/weldx/time.py#L83): [E303](https://duckduckgo.com/?q=pep8%20E303) too many blank lines (2)




codecov[bot]: # [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/433?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) Report
> Merging [#433](https://codecov.io/gh/BAMWelDX/weldx/pull/433?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (c841ffe) into [master](https://codecov.io/gh/BAMWelDX/weldx/commit/699e4e21c97b162fbab9995f5a7a9267741efe4c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (699e4e2) will **decrease** coverage by `1.01%`.
> The diff coverage is `46.36%`.

[![Impacted file tree graph](https://codecov.io/gh/BAMWelDX/weldx/pull/433/graphs/tree.svg?width=650&height=150&src=pr&token=wdof1qQTsn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)](https://codecov.io/gh/BAMWelDX/weldx/pull/433?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)

```diff
@@            Coverage Diff             @@
##           master     #433      +/-   ##
==========================================
- Coverage   97.26%   96.25%   -1.02%     
==========================================
  Files          87       88       +1     
  Lines        5411     5520     +109     
==========================================
+ Hits         5263     5313      +50     
- Misses        148      207      +59     
```


| [Impacted Files](https://codecov.io/gh/BAMWelDX/weldx/pull/433?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) | Coverage Δ | |
|---|---|---|
| [weldx/time.py](https://codecov.io/gh/BAMWelDX/weldx/pull/433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdGltZS5weQ==) | `41.58% <41.58%> (ø)` | |
| [weldx/\_\_init\_\_.py](https://codecov.io/gh/BAMWelDX/weldx/pull/433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [weldx/types.py](https://codecov.io/gh/BAMWelDX/weldx/pull/433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdHlwZXMucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/433?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/433?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Last update [699e4e2...c841ffe](https://codecov.io/gh/BAMWelDX/weldx/pull/433?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX).

vhirtham: Minor thing:

Should the `__init__` support something like this?

~~~ python
Time(["2000", "2001", "2002"])
~~~

I think this can be passed directly to `DatetimeIndex` and should just take 2 lines at max. But --> reaaaaaally low priority
CagtayFabry: > Minor thing:
> 
> Should the `__init__` support something like this?
> 
> ```python
> Time(["2000", "2001", "2002"])
> ```
> 
> I think this can be passed directly to `DatetimeIndex` and should just take 2 lines at max. But --> reaaaaaally low priority

it is currently supported via the existing utility functions so I would suggest yes
(also works for Timedeltas: `Time(["2s", "3s", "5s"])`)
vhirtham: Follow up:

Do we want to allow this?

~~~ python
Time("10s")
~~~

Makes it easier to use but might conflict with:

~~~ python
Time("2002-01-01")
~~~

But nothing that a `try` `except` block can't handle.

vhirtham: > > Minor thing:
> > Should the `__init__` support something like this?
> > ```python
> > Time(["2000", "2001", "2002"])
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > I think this can be passed directly to `DatetimeIndex` and should just take 2 lines at max. But --> reaaaaaally low priority
> 
> it is currently supported via the existing utility functions so I would suggest yes
> (also works for Timedeltas: `Time(["2s", "3s", "5s"])`)

Okay, then I have to add it as a testcase
review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/BAMWelDX/weldx/pull/433"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
vhirtham: > this looks ready to me for an initial implementation 👍

I will add some more info to the docstrings and merge later this day
vhirtham: > > this looks ready to me for an initial implementation 👍
> 
> I will add some more info to the docstrings and merge later this day

Have to postpone the merge to monday.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6aff72f..0f85202 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@
 
 - added "units" (exact) and "dimensionality" (dimensionality compatible) checking options
   to `util.xr_check_coords` [[#442]](https://github.com/BAMWelDX/weldx/pull/442)
+- `Time` class that can be initialized from several other time types and provides time related utility functions 
+  [[#433]](https://github.com/BAMWelDX/weldx/pull/433)
 
 ### removed
 
diff --git a/doc/api.rst b/doc/api.rst
index 57cf46e..ac1f1a6 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -15,6 +15,7 @@ API
     weldx.measurement
     weldx.transformations
     weldx.util
+    weldx.time
     weldx.types
     weldx.visualization
     weldx.welding
diff --git a/doc/nitpick_ignore b/doc/nitpick_ignore
index d642835..9ea4673 100644
--- a/doc/nitpick_ignore
+++ b/doc/nitpick_ignore
@@ -10,12 +10,14 @@ py:class networkx.classes.digraph.DiGraph
 py:class numpy.ndarray
 
 # pandas
+py:class pandas._libs.tslibs.timedeltas.Timedelta
 py:class pandas._libs.tslibs.timestamps.Timestamp
 py:class pandas.core.indexes.datetimes.DatetimeIndex
 py:class pandas.core.indexes.timedeltas.TimedeltaIndex
 py:class pandas.DatetimeIndex
 py:class pandas.TimedeltaIndex
 py:class pandas.Timestamp
+py:class pandas.Timedelta
 
 # pint
 py:class pint.Quantity
diff --git a/weldx/__init__.py b/weldx/__init__.py
index 69d3cc2..ebe55dc 100644
--- a/weldx/__init__.py
+++ b/weldx/__init__.py
@@ -23,6 +23,7 @@ import weldx.transformations
 import weldx.config
 import weldx.geometry
 import weldx.welding
+import weldx.time
 
 # asdf extensions and tags
 import weldx.asdf
@@ -48,6 +49,7 @@ from weldx.transformations import (
 )
 from weldx.welding.processes import GmawProcess
 from weldx.welding.groove.iso_9692_1 import get_groove
+from weldx.time import Time
 
 __all__ = (
     "ArcSegment",
@@ -73,6 +75,7 @@ __all__ = (
     "TimeSeries",
     "LinearHorizontalTraceSegment",
     "Config",
+    "Time",
 )
 
 weldx.config.Config.load_installed_standards()
diff --git a/weldx/core.py b/weldx/core.py
index d1abaf7..eee0dd2 100644
--- a/weldx/core.py
+++ b/weldx/core.py
@@ -13,6 +13,7 @@ import xarray as xr
 import weldx.util as ut
 from weldx.constants import Q_
 from weldx.constants import WELDX_UNIT_REGISTRY as UREG
+from weldx.time import pandas_time_delta_to_quantity
 
 if TYPE_CHECKING:
     import sympy
@@ -475,7 +476,7 @@ class TimeSeries:
             time_q = time
             time_pd = ut.to_pandas_time_index(time)
         elif isinstance(time, pd.TimedeltaIndex):
-            time_q = ut.pandas_time_delta_to_quantity(time, time_unit)
+            time_q = pandas_time_delta_to_quantity(time, time_unit)
             time_pd = time
         else:
             raise ValueError(
@@ -661,7 +662,7 @@ class TimeSeries:
                 axes=axes, data_name=data_name, time_unit=time_unit, **mpl_kwargs
             )
 
-        time = ut.pandas_time_delta_to_quantity(self.time)
+        time = pandas_time_delta_to_quantity(self.time)
         if time_unit is not None:
             time = time.to(time_unit)
 
diff --git a/weldx/time.py b/weldx/time.py
new file mode 100644
index 0000000..c428960
--- /dev/null
+++ b/weldx/time.py
@@ -0,0 +1,483 @@
+"""Contains classes and functions related to time."""
+from __future__ import annotations
+
+from functools import reduce
+from typing import List, Union
+
+import numpy as np
+import pandas as pd
+import pint
+import xarray as xr
+from pandas import DatetimeIndex, Timedelta, TimedeltaIndex, Timestamp
+from pandas.api.types import is_object_dtype
+from xarray import DataArray
+
+from weldx.types import types_time_like, types_timestamp_like
+
+from .constants import Q_
+
+__all__ = ["Time"]
+
+# list of types that are supported to be stored in Time._time
+_data_base_types = (pd.Timedelta, pd.Timestamp, pd.DatetimeIndex, pd.TimedeltaIndex)
+
+
+def pandas_time_delta_to_quantity(
+    time: pd.TimedeltaIndex, unit: str = "s"
+) -> pint.Quantity:
+    """Convert a `pandas.TimedeltaIndex` into a corresponding `pint.Quantity`.
+
+    Parameters
+    ----------
+    time : pandas.TimedeltaIndex
+        Instance of `pandas.TimedeltaIndex`
+    unit :
+        String that specifies the desired time unit.
+
+    Returns
+    -------
+    pint.Quantity :
+        Converted time quantity
+
+    """
+    # from pandas Timedelta documentation: "The .value attribute is always in ns."
+    # https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas
+    # .Timedelta.html
+    nanoseconds = time.values.astype(np.int64)
+    if len(nanoseconds) == 1:
+        nanoseconds = nanoseconds[0]
+    return Q_(nanoseconds, "ns").to(unit)
+
+
+class Time:
+    """Provides a unified interface for time related operations.
+
+    The purpose of this class is to provide a unified interface for all operations
+    related to time. This is important because time can have multiple representations.
+    When working with time, some difficulties that might arise are the following:
+
+        - we can have absolute times in form of dates and relative times in form of
+          quantities
+        - conversion factors between different time quantities differ. An hour consists
+          of 60 minutes, but a day has only 24 hours
+        - there are multiple time data types available in python like the ones provided
+          by numpy or pandas. If you have to work with time classes from multiple
+          libraries you might have to do a lot of conversions to perform simple tasks as
+          calculating a time delta between two dates.
+
+    This class solves the mentioned problems for many cases. It can be created
+    from many different data types and offers methods to convert back to one of the
+    supported types. Most of its methods also support the date types that can be used to
+    create an instance of this class. Therefore, you do not need to perform any
+    conversions yourself.
+
+    You can create the class from the following time representations:
+
+        - other instances of the ``Time`` class
+        - numpy: ``datetime64`` and ``timedelta64``
+        - pandas: ``Timedelta``, ``Timestamp``, ``TimedeltaIndex``, ``DatetimeIndex``
+        - `pint.Quantity`
+        - strings representing a date (``"2001-01-23 14:23:11"``) or a timedelta
+          (``23s``)
+
+    Parameters
+    ----------
+    time :
+        A supported class that represents either absolute or relative times. The
+        data must be in ascending order.
+    time_ref :
+        An absolute reference point in time (timestamp). The return values of all
+        accessors that return relative times will be calculated towards this
+        reference time.
+        This will turn the data of this class into absolute time values if relative
+        times are passed as ``time`` parameter. In case ``time`` already contains
+        absolute values and this parameter is set to ``None``, the first value of
+        the data will be used as reference time.
+
+    Examples
+    --------
+    Creation from a quantity:
+
+    >>> from weldx import Q_, Time
+    >>>
+    >>> quantity = Q_("10s")
+    >>> t_rel = Time(quantity)
+
+    Since a quantity is not an absolute time like a date, the ``is_absolute`` property
+    is ``False``:
+
+    >>> t_rel.is_absolute
+    False
+
+    To create an absolute value, just add a time stamp as ``time_ref`` parameter:
+
+    >>> from pandas import Timestamp
+    >>>
+    >>> timestamp = Timestamp("2042-01-01 13:37")
+    >>> t_abs = Time(quantity, timestamp)
+    >>> t_abs.is_absolute
+    True
+
+    Or use an absolute time type:
+
+    >>> t_abs = Time(timestamp)
+    >>> t_abs.is_absolute
+    True
+
+    >>> from pandas import DatetimeIndex
+    >>>
+    >>> dti = DatetimeIndex(["2001", "2002"])
+    >>> t_abs = Time(dti)
+    >>> t_abs.is_absolute
+    True
+
+    If you want to create a ``Time`` instance without importing anything else, just use
+    strings:
+
+    >>> # relative times
+    >>> t_rel = Time("1h")
+    >>> t_rel = Time(["3s","3h","3d"])
+    >>>
+    >>> # absolute times
+    >>> t_abs = Time(["1s","2s","3s"],"2010-10-05 12:00:00")
+    >>> t_abs = Time("3h", "2010-08-11")
+    >>> t_abs = Time("2014-07-23")
+    >>> t_abs = Time(["2000","2001","2002"])
+
+    As long as one of the operands represents a timedelta, you can add two `Time`
+    instances. If one of the instances is an array, the other one needs to be either a
+    scalar or an array of same length. In the latter case, values are added per index:
+
+    >>> t_res = Time(["1s", "2s"]) + Time("3s")
+    >>> t_res = Time(["1s", "2s"]) + Time(["3s", "4s"])
+    >>>
+    >>> t_res = Time(["1d", "2d"]) + Time("2000-01-01")
+    >>> t_res = Time(["2001-01-01", "2001-01-02"]) + Time(["3d", "4d"])
+
+    `Time` also accepts all other supported types on the right hand side of the ``+``
+    operator:
+
+    >>> t_res = Time(["1s", "2s"]) + Q_("10s")
+    >>> t_res = Time(["1s", "2s"]) + DatetimeIndex(["2001", "2002"])
+    >>> t_res = Time(["1d", "2d"]) + "2000-01-01"
+    >>> t_res = Time(["1s", "2s"]) + ["3s", "4s"]
+
+    Except for the numpy types and `pint.Quantity` other types are also supported on
+    the left hand side:
+
+    >>> t_res = DatetimeIndex(["2001", "2002"]) + Time(["1d", "2d"])
+    >>> t_res = "2000-01-01" + Time(["1d", "2d"])
+    >>> t_res = ["3s", "4s"] + Time(["1s", "2s"])
+
+    You can also compare two instances of `Time`:
+
+    >>> Time(["1s"]) == Time(Q_("1s"))
+    True
+
+    >>> Time("1s") == Time("2s")
+    False
+
+    >>> Time("2000-01-01 17:00:00") == Time("2s")
+    False
+
+    Note that any provided reference time is not taken into account when comparing two
+    absolute time values. Only the values itself are compared:
+
+    >>> dti = DatetimeIndex(["2001", "2002", "2003"])
+    >>> all(Time(dti, "2000") == Time(dti, "2042"))
+    True
+
+    If you want to include the reference times into the comparison, use the `equals`
+    method.
+
+    All supported types can also be used on the right hand side of the ``==`` operator:
+
+    >>> all(Time(["2000", "2001"]) == DatetimeIndex(["2000", "2001"]))
+    True
+
+    >>> all(Time(["1s", "2s"]) == Q_([1, 2],"s"))
+    True
+
+    >>> Time("3s") == "20d"
+    False
+
+    Raises
+    ------
+    ValueError
+        When time values passed are not sorted in monotonic increasing order.
+
+    """
+
+    def __init__(
+        self,
+        time: Union[types_time_like, Time, List[str]],
+        time_ref: Union[types_timestamp_like, Time, None] = None,
+    ):
+        if isinstance(time, Time):
+            time_ref = time_ref if time_ref is not None else time._time_ref
+            time = time._time
+
+        if isinstance(time, _data_base_types):
+            pass
+        elif isinstance(time, pint.Quantity):
+            time = Time._convert_quantity(time)
+        elif isinstance(time, (xr.DataArray, xr.Dataset)):
+            time = Time._convert_xarray(time)
+        else:
+            time = Time._convert_other(time)
+
+        # catch scalar Index-objects
+        if isinstance(time, pd.Index) and len(time) == 1:
+            time = time[0]
+
+        # sanity check
+        if not isinstance(time, _data_base_types):
+            raise TypeError("Could not create pandas time-like object.")
+
+        if time_ref is not None:
+            time_ref = pd.Timestamp(time_ref)
+            if isinstance(time, pd.Timedelta):
+                time = time + time_ref
+
+        if isinstance(time, pd.TimedeltaIndex) & (time_ref is not None):
+            time = time + time_ref
+
+        if isinstance(time, pd.Index) and not time.is_monotonic_increasing:
+            raise ValueError("The time values passed are not monotonic increasing.")
+
+        self._time = time
+        self._time_ref = time_ref
+
+    def __add__(self, other: Union[types_time_like, Time]) -> Time:
+        """Element-wise addition between `Time` object and compatible types."""
+        other = Time(other)
+        time_ref = self.reference_time if self.is_absolute else other.reference_time
+        return Time(self._time + other.as_pandas(), time_ref)
+
+    def __radd__(self, other: Union[types_time_like, Time]) -> Time:
+        """Element-wise addition between `Time` object and compatible types."""
+        return self + other
+
+    def __sub__(self, other: Union[types_time_like, Time]) -> Time:
+        """Element-wise subtraction between `Time` object and compatible types."""
+        other = Time(other)
+        time_ref = self.reference_time if self.is_absolute else other.reference_time
+        return Time(self._time - other.as_pandas(), time_ref)
+
+    def __rsub__(self, other: Union[types_time_like, Time]) -> Time:
+        """Element-wise subtraction between `Time` object and compatible types."""
+        other = Time(other)
+        time_ref = self.reference_time if self.is_absolute else other.reference_time
+        return Time(other.as_pandas() - self._time, time_ref)
+
+    def __eq__(self, other: Union[types_time_like, Time]) -> Union[bool, List[bool]]:
+        """Element-wise comparisons between time object and compatible types.
+
+        See Also
+        --------
+        equals : Check equality of `Time` objects.
+        """
+        return self._time == Time(other).as_pandas()
+
+    def __len__(self):
+        """Return the length of the data."""
+        return self.length
+
+    def equals(self, other: Time) -> bool:
+        """Test for matching ``time`` and ``reference_time`` between objects.
+
+        Parameters
+        ----------
+        other :
+            The compared object
+
+        Returns
+        -------
+        bool :
+            `True` if both objects have the same time values and reference time, `False`
+            otherwise
+
+        """
+        return np.all(self == other) & (self._time_ref == other._time_ref)
+
+    def all_close(self, other: Union[types_time_like, Time]) -> bool:
+        """Return `True` if another object compares equal within a certain tolerance.
+
+        Parameters
+        ----------
+        other :
+            The compared object
+
+        Returns
+        -------
+        bool :
+            `True` if all time values compare equal within a certain tolerance, `False`
+            otherwise
+
+        """
+        # TODO: handle tolerances ?
+        return np.allclose(self._time, Time(other).as_pandas())
+
+    def as_quantity(self) -> pint.Quantity:
+        """Return the data as `pint.Quantity`."""
+        if self.is_absolute:
+            q = pandas_time_delta_to_quantity(self._time - self.reference_time)
+            setattr(q, "time_ref", self.reference_time)  # store time_ref info
+            return q
+        return pandas_time_delta_to_quantity(self._time)
+
+    def as_timedelta(self) -> Union[Timedelta, TimedeltaIndex]:
+        """Return the data as `pandas.TimedeltaIndex`."""
+        if self.is_absolute:
+            return self._time - self.reference_time
+        return self._time
+
+    def as_datetime(self) -> Union[Timestamp, DatetimeIndex]:
+        """Return the data as `pandas.DatetimeIndex`."""
+        if not self.is_absolute:
+            raise TypeError("Cannot convert non absolute Time object to datetime")
+        return self._time
+
+    def as_pandas(
+        self,
+    ) -> Union[pd.Timedelta, pd.TimedeltaIndex, pd.Timestamp, pd.DatetimeIndex]:
+        """Return the underlying pandas time datatype."""
+        return self._time
+
+    def as_pandas_index(self) -> Union[pd.TimedeltaIndex, pd.DatetimeIndex]:
+        """Return a pandas index type regardless of length.
+
+        This is useful when using time as coordinate in xarray types.
+        """
+        if isinstance(self._time, pd.Timestamp):
+            return pd.DatetimeIndex([self._time])
+        if isinstance(self._time, pd.Timedelta):
+            return pd.TimedeltaIndex([self._time])
+        return self._time
+
+    def as_data_array(self) -> DataArray:
+        """Return the data as `xarray.DataArray`."""
+        da = xr.DataArray(self._time, coords={"time": self._time}, dims=["time"])
+        da.time.attrs["time_ref"] = self.reference_time
+        return da
+
+    @property
+    def reference_time(self) -> Union[Timestamp, None]:
+        """Get the reference time."""
+        if isinstance(self._time, DatetimeIndex):
+            return self._time_ref if self._time_ref is not None else self._time[0]
+        if isinstance(self._time, Timestamp):
+            return self._time_ref if self._time_ref is not None else self._time
+        return None
+
+    @reference_time.setter
+    def reference_time(self, time_ref: Union[types_timestamp_like, Time]):
+        """Set the reference time."""
+        self._time_ref = pd.Timestamp(time_ref)
+
+    @property
+    def is_absolute(self) -> bool:
+        """Return `True` if the class has a reference time and `False` otherwise."""
+        return isinstance(self._time, (Timestamp, DatetimeIndex))
+
+    @property
+    def length(self) -> int:
+        """Return the length of the data."""
+        if isinstance(self._time, (pd.TimedeltaIndex, pd.DatetimeIndex)):
+            return len(self._time)
+        return 1
+
+    @property
+    def is_timestamp(self) -> bool:
+        """Return `True` if the data represents a timestamp and `False` otherwise."""
+        return isinstance(self._time, pd.Timestamp)
+
+    def max(self) -> Union[Timedelta, Timestamp]:
+        """Get the maximal time of the data."""
+        if isinstance(self._time, (pd.TimedeltaIndex, pd.DatetimeIndex)):
+            return self._time.max()
+        return self._time
+
+    def min(self) -> Union[Timedelta, Timestamp]:
+        """Get the minimal time of the data."""
+        if isinstance(self._time, (pd.TimedeltaIndex, pd.DatetimeIndex)):
+            return self._time.min()
+        return self._time
+
+    @staticmethod
+    def _convert_quantity(
+        time: pint.Quantity,
+    ) -> Union[pd.TimedeltaIndex, pd.DatetimeIndex]:
+        """Build a time-like pandas.Index from pint.Quantity."""
+        time_ref = getattr(time, "time_ref", None)
+        base = "s"  # using low base unit could cause rounding errors
+        if not np.iterable(time):  # catch zero-dim arrays
+            time = np.expand_dims(time, 0)
+        delta = pd.TimedeltaIndex(data=time.to(base).magnitude, unit=base)
+        if time_ref is not None:
+            delta = delta + time_ref
+        return delta
+
+    @staticmethod
+    def _convert_xarray(
+        time: Union[xr.DataArray, xr.Dataset]
+    ) -> Union[pd.TimedeltaIndex, pd.DatetimeIndex]:
+        """Build a time-like pandas.Index from xarray objects."""
+        if "time" in time.coords:
+            time = time.time
+        time_ref = time.weldx.time_ref
+        time_index = pd.Index(time.values)
+        if time_ref is not None:
+            time_index = time_index + time_ref
+        return time_index
+
+    @staticmethod
+    def _convert_other(time) -> Union[pd.TimedeltaIndex, pd.DatetimeIndex]:
+        """Try autocasting input to time-like pandas index."""
+        _input_type = type(time)
+
+        if (not np.iterable(time) or isinstance(time, str)) and not isinstance(
+            time, np.ndarray
+        ):
+            time = [time]
+
+        time = pd.Index(time)
+
+        if isinstance(time, (pd.DatetimeIndex, pd.TimedeltaIndex)):
+            return time
+        # try manual casting for object dtypes (i.e. strings), should avoid integers
+        # warning: this allows something like ["1","2","3"] which will be ns !!
+        if is_object_dtype(time):
+            for func in (pd.DatetimeIndex, pd.TimedeltaIndex):
+                try:
+                    return func(time)
+                except (ValueError, TypeError):
+                    continue
+
+        raise TypeError(
+            f"Could not convert {_input_type} "
+            f"to pd.DatetimeIndex or pd.TimedeltaIndex"
+        )
+
+    @staticmethod
+    def union(times=List[Union[types_time_like, "Time"]]) -> Time:
+        """Calculate the union of multiple `Time` instances (or supported objects).
+
+        Any reference time information will be dropped.
+
+        Parameters
+        ----------
+        times
+            A list of time class instances
+
+        Returns
+        -------
+        Time
+            The time union
+
+        """
+        pandas_index = reduce(
+            lambda x, y: x.union(y),
+            (Time(time).as_pandas_index() for time in times),
+        )
+        return Time(pandas_index)
diff --git a/weldx/transformations/local_cs.py b/weldx/transformations/local_cs.py
index 36512a9..286c8b2 100644
--- a/weldx/transformations/local_cs.py
+++ b/weldx/transformations/local_cs.py
@@ -22,6 +22,8 @@ from weldx.transformations.types import (
 )
 from weldx.transformations.util import build_time_index, normalize
 
+from ..time import pandas_time_delta_to_quantity
+
 if TYPE_CHECKING:  # pragma: no cover
     import matplotlib.axes
 
@@ -796,7 +798,7 @@ class LocalCoordinateSystem:
             The coordinate systems time as 'pint.Quantity'
 
         """
-        return ut.pandas_time_delta_to_quantity(self.time)
+        return pandas_time_delta_to_quantity(self.time)
 
     @property
     def dataset(self) -> xr.Dataset:
diff --git a/weldx/types.py b/weldx/types.py
index 4db0a81..10b36cc 100644
--- a/weldx/types.py
+++ b/weldx/types.py
@@ -3,12 +3,18 @@ import pathlib
 from io import IOBase
 from typing import Protocol, Union, runtime_checkable
 
+import numpy as np
+from pandas import DatetimeIndex, Timedelta, TimedeltaIndex, Timestamp
+from pint import Quantity
+
 __all__ = [
     "SupportsFileReadOnly",
     "SupportsFileReadWrite",
     "types_file_like",
     "types_path_like",
     "types_path_and_file_like",
+    "types_timestamp_like",
+    "types_time_like",
 ]
 
 
@@ -55,3 +61,18 @@ types_path_like = Union[str, pathlib.Path]
 
 types_path_and_file_like = Union[types_path_like, types_file_like]
 """types to handle paths and file handles."""
+
+types_datetime_like = Union[DatetimeIndex, np.datetime64]
+"""types that define ascending arrays of time stamps."""
+
+types_timestamp_like = Union[Timestamp, str]
+"""types that define timestamps."""
+
+types_timedelta_like = Union[TimedeltaIndex, Quantity, np.timedelta64]
+"""types that define ascending time delta arrays."""
+
+types_time_like = Union[types_datetime_like, types_timedelta_like, types_timestamp_like]
+"""types that represent time."""
+
+types_pandas_times = Union[Timedelta, Timestamp, DatetimeIndex, TimedeltaIndex]
+"""supported pandas time types."""
diff --git a/weldx/util.py b/weldx/util.py
index 95c339d..876317d 100644
--- a/weldx/util.py
+++ b/weldx/util.py
@@ -22,7 +22,6 @@ from pint import DimensionalityError
 from scipy.spatial.transform import Rotation as Rot
 from scipy.spatial.transform import Slerp
 
-from .constants import Q_
 from .constants import WELDX_UNIT_REGISTRY as ureg
 
 
@@ -337,16 +336,18 @@ def to_pandas_time_index(
 
     if isinstance(time, (pd.DatetimeIndex, pd.TimedeltaIndex)):
         return time
-
     if isinstance(time, LocalCoordinateSystem):
         return to_pandas_time_index(time.time)
 
     if isinstance(time, pint.Quantity):
+        time_ref = getattr(time, "time_ref", None)
         base = "s"  # using low base unit could cause rounding errors
         if not np.iterable(time):  # catch zero-dim arrays
             time = np.expand_dims(time, 0)
-        return pd.TimedeltaIndex(data=time.to(base).magnitude, unit=base)
-
+        delta = pd.TimedeltaIndex(data=time.to(base).magnitude, unit=base)
+        if time_ref is not None:
+            return delta + pd.Timestamp(time_ref)
+        return delta
     if isinstance(time, (xr.DataArray, xr.Dataset)):
         if "time" in time.coords:
             time = time.time
@@ -354,14 +355,15 @@ def to_pandas_time_index(
         if is_timedelta64_dtype(time_index) and time.weldx.time_ref:
             time_index = time_index + time.weldx.time_ref
         return time_index
-
-    if not np.iterable(time) or isinstance(time, str):
+    if (not np.iterable(time) or isinstance(time, str)) and not isinstance(
+        time, np.ndarray
+    ):
         time = [time]
+
     time = pd.Index(time)
 
     if isinstance(time, (pd.DatetimeIndex, pd.TimedeltaIndex)):
         return time
-
     # try manual casting for object dtypes (i.e. strings), should avoid integers
     # warning: this allows something like ["1","2","3"] which will be ns !!
     if is_object_dtype(time):
@@ -376,32 +378,6 @@ def to_pandas_time_index(
     )
 
 
-def pandas_time_delta_to_quantity(
-    time: pd.TimedeltaIndex, unit: str = "s"
-) -> pint.Quantity:
-    """Convert a `pandas.TimedeltaIndex` into a corresponding `pint.Quantity`.
-
-    Parameters
-    ----------
-    time : pandas.TimedeltaIndex
-        Instance of `pandas.TimedeltaIndex`
-    unit :
-        String that specifies the desired time unit.
-
-    Returns
-    -------
-    pint.Quantity :
-        Converted time quantity
-
-    """
-    # from pandas Timedelta documentation: "The .value attribute is always in ns."
-    # https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.Timedelta.html
-    nanoseconds = time.values.astype(np.int64)
-    if len(nanoseconds) == 1:
-        nanoseconds = nanoseconds[0]
-    return Q_(nanoseconds, "ns").to(unit)
-
-
 def matrix_is_close(mat_a, mat_b, abs_tol=1e-9) -> bool:
     """Check if a matrix is close or equal to another matrix.
 


Test patch:
diff --git a/weldx/tests/test_time.py b/weldx/tests/test_time.py
new file mode 100644
index 0000000..209561b
--- /dev/null
+++ b/weldx/tests/test_time.py
@@ -0,0 +1,417 @@
+"""Test the `Time` class."""
+
+import math
+from typing import List, Tuple, Type, Union
+
+import numpy as np
+import pandas as pd
+import pytest
+import xarray as xr
+from pandas import DatetimeIndex, Timedelta, TimedeltaIndex, Timestamp
+
+from weldx import Q_
+from weldx.time import Time, pandas_time_delta_to_quantity
+from weldx.types import types_time_like
+
+
+def _initialize_delta_type(cls_type, values, unit):
+    """Initialize the passed time type."""
+    if cls_type is np.timedelta64:
+        if isinstance(values, List):
+            return np.array(values, dtype=f"timedelta64[{unit}]")
+        return np.timedelta64(values, unit)
+    if cls_type is Time:
+        return Time(Q_(values, unit))
+    if cls_type is str:
+        if not isinstance(values, List):
+            return f"{values}{unit}"
+        return [f"{v}{unit}" for v in values]
+    return cls_type(values, unit)
+
+
+def _initialize_datetime_type(cls_type, values):
+    """Initialize the passed datetime type."""
+    if cls_type is np.datetime64:
+        if isinstance(values, List):
+            return np.array(values, dtype="datetime64")
+        return np.datetime64(values)
+    if cls_type is str:
+        return values
+    return cls_type(values)
+
+
+def _initialize_date_time_quantity(timedelta, unit, time_ref):
+    """Initialize a quantity that represents a datetime by adding a ``time_ref``."""
+    quantity = Q_(timedelta, unit)
+    setattr(quantity, "time_ref", Timestamp(time_ref))
+    return quantity
+
+
+def _transform_array(data, is_array, is_scalar):
+    """Transform an array into a scalar, single value array or return in unmodified."""
+    if not is_array:
+        return data[0]
+    if is_scalar:
+        return [data[0]]
+    return data
+
+
+def _initialize_time_type(
+    input_type, delta_val, abs_val, is_timedelta, is_array, is_scalar, unit="s"
+):
+    """Create an instance of the desired input type."""
+    val = delta_val if is_timedelta else abs_val
+    if not is_timedelta and input_type is Q_:
+        val = [v - delta_val[0] for v in delta_val]
+    val = _transform_array(val, is_array=is_array, is_scalar=is_scalar)
+
+    # create the time input ---------------------------------
+    if is_timedelta:
+        return _initialize_delta_type(input_type, val, unit)
+    if input_type is not Q_:
+        return _initialize_datetime_type(input_type, val)
+    return _initialize_date_time_quantity(val, unit, abs_val[0])
+
+
+def _is_timedelta(cls_type):
+    """Return ``True`` if the passed type is a timedelta type."""
+    return cls_type in [TimedeltaIndex, Timedelta, np.timedelta64] or (
+        cls_type is Time and not Time.is_absolute
+    )
+
+
+def _is_datetime(cls_type):
+    """Return ``True`` if the passed type is a datetime type."""
+    return not _is_timedelta(cls_type)
+
+
+class TestTime:
+    """Test the time class."""
+
+    # test_init helper functions -------------------------------------------------------
+
+    @staticmethod
+    def _parse_time_type_test_input(
+        type_input,
+    ) -> Tuple[Union[types_time_like, Time], bool]:
+        """Return the time type and a bool that defines if the returned type is a delta.
+
+        This is mainly used in generalized tests where a type like `Time` itself can
+        represent deltas and absolute times. In this case one can use this function
+        to extract the information from a tuple.
+
+        """
+        if isinstance(type_input, Tuple):
+            # to avoid wrong test setups due to spelling mistakes
+            assert type_input[1] in ["timedelta", "datetime"]
+            time_type = type_input[0]
+            is_timedelta = type_input[1] == "timedelta"
+        else:
+            time_type = type_input
+            is_timedelta = _is_timedelta(type_input)
+        return time_type, is_timedelta
+
+    @classmethod
+    def _get_init_exp_values(
+        cls,
+        is_timedelta,
+        time_ref,
+        data_was_scalar,
+        delta_val,
+        abs_val,
+    ):
+        """Get the expected result values for the `__init__` test."""
+        exp_is_absolute = time_ref is not None or not is_timedelta
+
+        # expected reference time
+        exp_time_ref = None
+        if exp_is_absolute:
+            exp_time_ref = Timestamp(time_ref if time_ref is not None else abs_val[0])
+
+        # expected time delta values
+        val = delta_val
+        if exp_is_absolute:
+            offset = 0
+            if not is_timedelta:
+                if time_ref is not None:
+                    offset = Timestamp(abs_val[0]) - Timestamp(time_ref)
+                    offset = offset.total_seconds()
+                offset -= delta_val[0]
+
+            val = [v + offset for v in delta_val]
+
+        val = val[0] if data_was_scalar else val
+        exp_timedelta = (
+            Timedelta(val, "s") if data_was_scalar else TimedeltaIndex(val, "s")
+        )
+
+        # expected datetime
+        exp_datetime = None
+        if exp_is_absolute:
+            time_ref = Timestamp(time_ref if time_ref is not None else abs_val[0])
+            exp_datetime = time_ref + exp_timedelta
+
+        return dict(
+            is_absolute=exp_is_absolute,
+            time_ref=exp_time_ref,
+            timedelta=exp_timedelta,
+            datetime=exp_datetime,
+        )
+
+    # test_init ------------------------------------------------------------------------
+
+    @pytest.mark.parametrize("scl, arr", [(True, False), (True, True), (False, True)])
+    @pytest.mark.parametrize("set_time_ref", [False, True])
+    @pytest.mark.parametrize(
+        "input_vals",
+        [
+            (str, "timedelta"),
+            (Time, "timedelta"),
+            (Q_, "timedelta"),
+            TimedeltaIndex,
+            Timedelta,
+            np.timedelta64,
+            (str, "datetime"),
+            (Time, "datetime"),
+            (Q_, "datetime"),
+            DatetimeIndex,
+            Timestamp,
+            np.datetime64,
+        ],
+    )
+    def test_init(
+        self,
+        input_vals: Union[Type, Tuple[Type, str]],
+        set_time_ref: bool,
+        scl: bool,
+        arr: bool,
+    ):
+        """Test the `__init__` method of the time class.
+
+        Parameters
+        ----------
+        input_vals :
+            Either a compatible time type or a tuple of two values. The tuple is needed
+            in case the tested time type can either represent relative time values as
+            well as absolute ones. In this case, the first value is the type. The
+            second value is a string specifying if the type represents absolute
+            ("datetime") or relative ("timedelta") values.
+        set_time_ref :
+            If `True`, a reference time will be passed to the `__init__` method
+        scl :
+            If `True`, the data of the passed type consists of a single value.
+        arr :
+            If `True`, the data of the passed type is an array
+
+        """
+        input_type, is_timedelta = self._parse_time_type_test_input(input_vals)
+
+        # skip matrix cases that do not work --------------------
+        if arr and input_type in [Timedelta, Timestamp]:
+            pytest.skip()
+        if not arr and input_type in [DatetimeIndex, TimedeltaIndex]:
+            pytest.skip()
+
+        # create input values -----------------------------------
+        delta_val = [1, 2, 3]
+        abs_val = [f"2000-01-01 16:00:0{v}" for v in delta_val]
+
+        time = _initialize_time_type(
+            input_type, delta_val, abs_val, is_timedelta, arr, scl
+        )
+        time_ref = "2000-01-01 15:00:00" if set_time_ref else None
+
+        # create `Time` instance --------------------------------
+        time_class_instance = Time(time, time_ref)
+
+        # check results -----------------------------------------
+        exp = self._get_init_exp_values(is_timedelta, time_ref, scl, delta_val, abs_val)
+
+        assert time_class_instance.is_absolute == exp["is_absolute"]
+        assert time_class_instance.reference_time == exp["time_ref"]
+        assert np.all(time_class_instance.as_timedelta() == exp["timedelta"])
+        if exp["is_absolute"]:
+            assert np.all(time_class_instance.as_datetime() == exp["datetime"])
+        else:
+            with pytest.raises(TypeError):
+                time_class_instance.as_datetime()
+
+    # todo: issues
+    #   - time parameter can be None
+
+    # test_init_exceptions -------------------------------------------------------------
+
+    @staticmethod
+    @pytest.mark.parametrize(
+        "time, time_ref, raises",
+        [
+            (TimedeltaIndex([3, 2, 1]), None, ValueError),
+            (DatetimeIndex(["2010", "2000"]), None, ValueError),
+            (["2010", "2000"], None, ValueError),
+            (Q_([3, 2, 1], "s"), None, ValueError),
+            (np.array([3, 2, 1], dtype="timedelta64[s]"), None, ValueError),
+        ],
+    )
+    def test_init_exception(time, time_ref, raises):
+        """Test initialization of the `Time` class with all supported types."""
+        with pytest.raises(raises):
+            Time(time, time_ref)
+
+    # test_add_timedelta ---------------------------------------------------------------
+
+    @pytest.mark.parametrize("other_on_rhs", [True, False])
+    @pytest.mark.parametrize("time_class_is_array", [False, True])
+    @pytest.mark.parametrize("other_is_array", [False, True])
+    @pytest.mark.parametrize("unit", ["s", "h"])
+    @pytest.mark.parametrize(
+        "other_type",
+        [
+            (str, "timedelta"),
+            (Time, "timedelta"),
+            (Q_, "timedelta"),
+            TimedeltaIndex,
+            Timedelta,
+            np.timedelta64,
+            (str, "datetime"),
+            (Time, "datetime"),
+            (Q_, "datetime"),
+            DatetimeIndex,
+            Timestamp,
+            np.datetime64,
+        ],
+    )
+    def test_add_timedelta(
+        self,
+        other_type,
+        other_on_rhs: bool,
+        unit: str,
+        time_class_is_array: bool,
+        other_is_array: bool,
+    ):
+        """Test the `__add__` method if the `Time` class represents a time delta.
+
+        Parameters
+        ----------
+        other_type :
+            The type of the other object
+        other_on_rhs :
+            If `True`, the other type is on the rhs of the + sign and on the lhs
+            otherwise
+        unit :
+            The time unit to use
+        time_class_is_array :
+            If `True`, the `Time` instance contains 3 time values and 1 otherwise
+        other_is_array :
+            If `True`, the other time object contains 3 time values and 1 otherwise
+
+        """
+        other_type, is_timedelta = self._parse_time_type_test_input(other_type)
+
+        # skip array cases where the type does not support arrays
+        if other_type in [Timedelta, Timestamp] and other_is_array:
+            pytest.skip()
+        if not other_is_array and other_type in [DatetimeIndex, TimedeltaIndex]:
+            pytest.skip()
+
+        # skip __radd__ cases where we got conflicts with the other types' __add__
+        if not other_on_rhs and other_type in (
+            Q_,
+            np.ndarray,
+            np.timedelta64,
+            np.datetime64,
+        ):
+            pytest.skip()
+
+        # setup rhs
+        delta_val = [4, 6, 8]
+        if unit == "s":
+            abs_val = [f"2000-01-01 10:00:0{v}" for v in delta_val]
+        else:
+            abs_val = [f"2000-01-01 1{v}:00:00" for v in delta_val]
+        other = _initialize_time_type(
+            other_type,
+            delta_val,
+            abs_val,
+            is_timedelta,
+            other_is_array,
+            not other_is_array,
+            unit,
+        )
+
+        # setup lhs
+        time_class_values = [1, 2, 3] if time_class_is_array else [1]
+        time_class = Time(Q_(time_class_values, unit))
+
+        # setup expected values
+        add = delta_val if other_is_array else delta_val[0]
+        exp_val = np.array(time_class_values) + add
+        exp_val += 0 if is_timedelta else time_class_values[0] - exp_val[0]
+
+        exp_time_ref = None if is_timedelta else abs_val[0]
+        exp = Time(Q_(exp_val, unit), exp_time_ref)
+
+        # calculate and evaluate result
+        res = time_class + other if other_on_rhs else other + time_class
+
+        assert res.reference_time == exp.reference_time
+        assert np.all(res.as_timedelta() == exp.as_timedelta())
+        assert np.all(res == exp)
+
+    # test_convert_util ----------------------------------------------------------------
+
+    @staticmethod
+    def test_convert_util():
+        """Test basic conversion functions from/to xarray/pint."""
+        t = pd.date_range("2020", periods=10, freq="1s")
+        ts = t[0]
+
+        arr = xr.DataArray(
+            np.arange(10),
+            dims=["time"],
+            coords={"time": t - ts},
+        )
+        arr.time.weldx.time_ref = ts
+        time = Time(arr)
+
+        assert time.length == len(t)
+        assert time.equals(Time(t))
+
+        time_q = time.as_quantity()
+        assert np.all(time_q == Q_(range(10), "s"))
+        assert time_q.time_ref == ts
+
+        arr2 = time.as_data_array().weldx.time_ref_restore()
+        assert arr.time.identical(arr2.time)
+
+
+# test_pandas_time_delta_to_quantity ---------------------------------------------------
+
+
+def test_pandas_time_delta_to_quantity():
+    """Test the 'pandas_time_delta_to_quantity' utility function."""
+    is_close = np.vectorize(math.isclose)
+
+    def _check_close(t1, t2):
+        assert np.all(is_close(t1.magnitude, t2.magnitude))
+        assert t1.units == t2.units
+
+    time_single = pd.TimedeltaIndex([1], unit="s")
+
+    _check_close(pandas_time_delta_to_quantity(time_single), Q_(1, "s"))
+    _check_close(pandas_time_delta_to_quantity(time_single, "ms"), Q_(1000, "ms"))
+    _check_close(pandas_time_delta_to_quantity(time_single, "us"), Q_(1000000, "us"))
+    _check_close(pandas_time_delta_to_quantity(time_single, "ns"), Q_(1000000000, "ns"))
+
+    time_multi = pd.TimedeltaIndex([1, 2, 3], unit="s")
+    _check_close(pandas_time_delta_to_quantity(time_multi), Q_([1, 2, 3], "s"))
+    _check_close(
+        pandas_time_delta_to_quantity(time_multi, "ms"), Q_([1000, 2000, 3000], "ms")
+    )
+    _check_close(
+        pandas_time_delta_to_quantity(time_multi, "us"),
+        Q_([1000000, 2000000, 3000000], "us"),
+    )
+    _check_close(
+        pandas_time_delta_to_quantity(time_multi, "ns"),
+        Q_([1000000000, 2000000000, 3000000000], "ns"),
+    )
diff --git a/weldx/tests/test_utility.py b/weldx/tests/test_utility.py
index 1fa4dc0..06816b1 100644
--- a/weldx/tests/test_utility.py
+++ b/weldx/tests/test_utility.py
@@ -1,6 +1,5 @@
 """Test the internal utility functions."""
 import copy
-import math
 import unittest
 from typing import Dict, List
 
@@ -157,38 +156,6 @@ def test_to_pandas_time_index_exceptions(arg, exception):
         ut.to_pandas_time_index(arg)
 
 
-def test_pandas_time_delta_to_quantity():
-    """Test the 'pandas_time_delta_to_quantity' utility function."""
-    is_close = np.vectorize(math.isclose)
-
-    def _check_close(t1, t2):
-        assert np.all(is_close(t1.magnitude, t2.magnitude))
-        assert t1.units == t2.units
-
-    time_single = pd.TimedeltaIndex([1], unit="s")
-
-    _check_close(ut.pandas_time_delta_to_quantity(time_single), Q_(1, "s"))
-    _check_close(ut.pandas_time_delta_to_quantity(time_single, "ms"), Q_(1000, "ms"))
-    _check_close(ut.pandas_time_delta_to_quantity(time_single, "us"), Q_(1000000, "us"))
-    _check_close(
-        ut.pandas_time_delta_to_quantity(time_single, "ns"), Q_(1000000000, "ns")
-    )
-
-    time_multi = pd.TimedeltaIndex([1, 2, 3], unit="s")
-    _check_close(ut.pandas_time_delta_to_quantity(time_multi), Q_([1, 2, 3], "s"))
-    _check_close(
-        ut.pandas_time_delta_to_quantity(time_multi, "ms"), Q_([1000, 2000, 3000], "ms")
-    )
-    _check_close(
-        ut.pandas_time_delta_to_quantity(time_multi, "us"),
-        Q_([1000000, 2000000, 3000000], "us"),
-    )
-    _check_close(
-        ut.pandas_time_delta_to_quantity(time_multi, "ns"),
-        Q_([1000000000, 2000000000, 3000000000], "ns"),
-    )
-
-
 class TestXarrayInterpolation:
     """Tests all custom xarray interpolation functions."""