Repository: zeroSteiner/rule-engine

Issue:
Datetime operations and Timedeltas
According to the documentation, there doesn't seem to be any support for timedeltas, or any mathematical operations on datetime objects. This would be super useful for implementing rules such as "older than 30 days" without having to calculate what "30 days before now" is manually.

For example, ideally I could write a rule that looks like this to implement the above:

```
created_at < now - P30D
```

Discussion:
djmattyg007: @zeroSteiner I'm curious if you have any feedback. I should be able to finish updating the documentation over the next couple of days.
djmattyg007: I wonder if I should update `util.py` to be `_util.py`?
djmattyg007: I didn't realise this project still supported EOL versions of Python (going as far back as 3.4). Is this support still necessary? If so, I can adjust the code to be compatible :+1: 

You can see where the tests are failing on old versions here:
https://github.com/djmattyg007/rule-engine/runs/7114102307
zeroSteiner: This looks pretty good at first glance. I'll probably need a week or two to do a more thorough review. Really appreciate that you updated the docs and unit tests too 👍🏻 

Thanks for working on this!
djmattyg007: It's easier to explain this all in a single comment, rather than responding to multiple inline comments.

The gist of it is that I've implemented what is essentially a subset of the ISO 8601 standard for representing durations ("period" is a synonym"). You can find a rundown of what this involves here:

https://en.wikipedia.org/wiki/ISO_8601#Durations

I've omitted support for "years" and "months" in this implementation, because they're ambiguous without additional information. This is also why the Python `timedelta` class doesn't support them.

I originally toyed with using [`isodate`](https://github.com/gweis/isodate). It does have support for years and months, but it comes with some problems. This support is implemented by dynamically returning a custom `Duration` class that isn't fully compatible with the stdlib `timedelta` class. While it probably wouldn't *add* another dependency (it would *replace* `dateutil` instead), its parsing is stricter than `dateutil`'s, which means it isn't a drop-in replacement.

Therefore I instead modelled the parsing code on what `isodate` does, but left out the parts that would result in ambiguity. I took care to replicate other spec-compliant features that `isodate` implements (which you can see described in the Wikipedia article). This includes supporting both `.` and `,` for decimal places.
djmattyg007: As for resolving the issue of how these should be represented and understood by the parser, I understand why you think they should utilise prefixed strings, rather than being a raw identifier. I think I would prefer `p`, but I still think you should need to include the `P` within the string, so that the string itself is still a valid ISO 8601 duration representation. What do you think?
zeroSteiner: I think since it's a timedelta literal, I'd have an easier time remembering `t` as a prefix. Alternatively maybe the datatype should actually be called a period but I think that maybe more generally confusing when looking at a list of types.  Timedelta is a bit more concise as to its purpose. 

Agreed that it makes sense to keep P as a prefix in the string contents for consistency with the standard but since it's not strictly necessary, it'd be nice if it were optional.

That would make both of these valid literals for 1 week.

* `t"P1W"`
* `t"7D"`

Not supporting years and months makes sense. 
djmattyg007: Sorry for the delay on finishing this up. I've been working on implementing the necessary changes, and after some testing I think my preference would be to not support the `P` prefix being optional.

- It simplifies the implementation
- It reduces the number of test cases required to cover all of the functionality
- It makes the representation of "zero" (`PT`) more obvious

I'll hopefully have this ready for review later today :+1: 

Patch:
diff --git a/Pipfile b/Pipfile
index 934479c..1263a2e 100644
--- a/Pipfile
+++ b/Pipfile
@@ -12,8 +12,8 @@ sphinx = "*"
 sphinx-rtd-theme = "*"
 graphviz = "*"
 coverage = "*"
-ipython = "*"
 
 [scripts]
 tests = 'sh -c "PYTHONPATH=$(pwd)/lib python -m unittest -v tests"'
 tests-coverage = 'sh -c "PYTHONPATH=$(pwd)/lib coverage run -m unittest -v tests && coverage report --include=\"*/rule_engine/*\""'
+
diff --git a/docs/source/attributes.rst b/docs/source/attributes.rst
index 44427ba..2afe831 100644
--- a/docs/source/attributes.rst
+++ b/docs/source/attributes.rst
@@ -12,89 +12,99 @@ Builtin Attributes
 ------------------
 The following attributes are builtin to the default :py:class:`~.Context` object.
 
-+-----------------+-------------------------------------+
-| Attribute Name  | Attribute Type                      |
-+-----------------+-------------------------------------+
-| :py:attr:`~.DataType.ARRAY` **Attributes**            |
-+-----------------+-------------------------------------+
-| ``is_empty``    | :py:attr:`~.DataType.BOOLEAN`       |
-+-----------------+-------------------------------------+
-| ``length``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``to_set``      | :py:attr:`~.DataType.SET`           |
-+-----------------+-------------------------------------+
-| :py:attr:`~.DataType.DATETIME` **Attributes**         |
-+-----------------+-------------------------------------+
-| ``date``        | :py:attr:`~.DataType.DATETIME`      |
-+-----------------+-------------------------------------+
-| ``day``         | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``hour``        | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``microsecond`` | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``millisecond`` | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``minute``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``month``       | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``second``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``to_epoch``    | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``weekday``     | :py:attr:`~.DataType.STRING`        |
-+-----------------+-------------------------------------+
-| ``year``        | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``zone_name``   | :py:attr:`~.DataType.STRING`        |
-+-----------------+-------------------------------------+
-| :py:attr:`~.DataType.FLOAT`   **Attributes** :sup:`1` |
-+-----------------+-------------------------------------+
-| ``ceiling``     | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``floor``       | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``to_str``      | :py:attr:`~.DataType.STRING`        |
-+-----------------+-------------------------------------+
-| :py:attr:`~.DataType.MAPPING` **Attributes**          |
-+-----------------+-------------------------------------+
-| ``is_empty``    | :py:attr:`~.DataType.BOOLEAN`       |
-+-----------------+-------------------------------------+
-| ``keys``        | :py:attr:`~.DataType.ARRAY`         |
-+-----------------+-------------------------------------+
-| ``length``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``values``      | :py:attr:`~.DataType.ARRAY`         |
-+-----------------+-------------------------------------+
-| :py:attr:`~.DataType.SET` **Attributes**              |
-+-----------------+-------------------------------------+
-| ``is_empty``    | :py:attr:`~.DataType.BOOLEAN`       |
-+-----------------+-------------------------------------+
-| ``length``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``to_ary``      | :py:attr:`~.DataType.ARRAY`         |
-+-----------------+-------------------------------------+
-| :py:attr:`~.DataType.STRING` **Attributes**           |
-+-----------------+-------------------------------------+
-| ``as_lower``    | :py:attr:`~.DataType.STRING`        |
-+-----------------+-------------------------------------+
-| ``as_upper``    | :py:attr:`~.DataType.STRING`        |
-+-----------------+-------------------------------------+
-| ``to_ary``      | :py:attr:`~.DataType.ARRAY`         |
-+-----------------+-------------------------------------+
-| ``to_flt``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``to_set``      | :py:attr:`~.DataType.SET`           |
-+-----------------+-------------------------------------+
-| ``to_int``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
-| ``is_empty``    | :py:attr:`~.DataType.BOOLEAN`       |
-+-----------------+-------------------------------------+
-| ``length``      | :py:attr:`~.DataType.FLOAT`         |
-+-----------------+-------------------------------------+
++-------------------+-------------------------------------+
+| Attribute Name    | Attribute Type                      |
++-------------------+-------------------------------------+
+| :py:attr:`~.DataType.ARRAY` **Attributes**              |
++-------------------+-------------------------------------+
+| ``is_empty``      | :py:attr:`~.DataType.BOOLEAN`       |
++-------------------+-------------------------------------+
+| ``length``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``to_set``        | :py:attr:`~.DataType.SET`           |
++-------------------+-------------------------------------+
+| :py:attr:`~.DataType.DATETIME` **Attributes**           |
++-------------------+-------------------------------------+
+| ``date``          | :py:attr:`~.DataType.DATETIME`      |
++-------------------+-------------------------------------+
+| ``day``           | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``hour``          | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``microsecond``   | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``millisecond``   | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``minute``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``month``         | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``second``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``to_epoch``      | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``weekday``       | :py:attr:`~.DataType.STRING`        |
++-------------------+-------------------------------------+
+| ``year``          | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``zone_name``     | :py:attr:`~.DataType.STRING`        |
++-------------------+-------------------------------------+
+| :py:attr:`~.DataType.TIMEDELTA` **Attributes**          |
++-------------------+-------------------------------------+
+| ``days``          | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``seconds``       | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``microseconds``  | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``total_seconds`` | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| :py:attr:`~.DataType.FLOAT`   **Attributes** :sup:`1`   |
++-------------------+-------------------------------------+
+| ``ceiling``       | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``floor``         | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``to_str``        | :py:attr:`~.DataType.STRING`        |
++-------------------+-------------------------------------+
+| :py:attr:`~.DataType.MAPPING` **Attributes**            |
++-------------------+-------------------------------------+
+| ``is_empty``      | :py:attr:`~.DataType.BOOLEAN`       |
++-------------------+-------------------------------------+
+| ``keys``          | :py:attr:`~.DataType.ARRAY`         |
++-------------------+-------------------------------------+
+| ``length``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``values``        | :py:attr:`~.DataType.ARRAY`         |
++-------------------+-------------------------------------+
+| :py:attr:`~.DataType.SET` **Attributes**                |
++-------------------+-------------------------------------+
+| ``is_empty``      | :py:attr:`~.DataType.BOOLEAN`       |
++-------------------+-------------------------------------+
+| ``length``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``to_ary``        | :py:attr:`~.DataType.ARRAY`         |
++-------------------+-------------------------------------+
+| :py:attr:`~.DataType.STRING` **Attributes**             |
++-------------------+-------------------------------------+
+| ``as_lower``      | :py:attr:`~.DataType.STRING`        |
++-------------------+-------------------------------------+
+| ``as_upper``      | :py:attr:`~.DataType.STRING`        |
++-------------------+-------------------------------------+
+| ``to_ary``        | :py:attr:`~.DataType.ARRAY`         |
++-------------------+-------------------------------------+
+| ``to_flt``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``to_set``        | :py:attr:`~.DataType.SET`           |
++-------------------+-------------------------------------+
+| ``to_int``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
+| ``is_empty``      | :py:attr:`~.DataType.BOOLEAN`       |
++-------------------+-------------------------------------+
+| ``length``        | :py:attr:`~.DataType.FLOAT`         |
++-------------------+-------------------------------------+
 
 FLOAT Attributes :sup:`1`
 ^^^^^^^^^^^^^^^^^^^^^^^^^
 Due to the syntax of floating point literals, the attributes must be accessed using parenthesis. For example
-``3.14.to_str`` is invalid while ``(3.14).to_str`` is valid.
\ No newline at end of file
+``3.14.to_str`` is invalid while ``(3.14).to_str`` is valid.
diff --git a/docs/source/rule_engine/ast.rst b/docs/source/rule_engine/ast.rst
index 904f171..22d80a8 100644
--- a/docs/source/rule_engine/ast.rst
+++ b/docs/source/rule_engine/ast.rst
@@ -60,6 +60,12 @@ Left-Operator-Right Expressions
    .. autoattribute:: result_type
       :annotation: = UNDEFINED
 
+.. autoclass:: SubtractExpression
+   :show-inheritance:
+
+   .. autoattribute:: result_type
+      :annotation: = UNDEFINED
+
 .. autoclass:: ArithmeticExpression
    :show-inheritance:
 
diff --git a/docs/source/syntax.rst b/docs/source/syntax.rst
index e40c0c3..afe3814 100644
--- a/docs/source/syntax.rst
+++ b/docs/source/syntax.rst
@@ -27,8 +27,10 @@ The following table outlines all operators that can be used in Rule Engine expre
 | **Arithmetic Operators**                                                                                      |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``+``        | Addition                     | :py:attr:`~.DataType.FLOAT`, :py:attr:`~.DataType.STRING`       |
+|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.TIMEDELTA` |
 +--------------+------------------------------+-----------------------------------------------------------------+
-| ``-``        | Subtraction                  | :py:attr:`~.DataType.FLOAT`                                     |
+| ``-``        | Subtraction                  | :py:attr:`~.DataType.FLOAT`, :py:attr:`~.DataType.DATETIME`,    |
+|              |                              | :py:attr:`~.DataType.TIMEDELTA`                                 |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``*``        | Multiplication               | :py:attr:`~.DataType.FLOAT`                                     |
 +--------------+------------------------------+-----------------------------------------------------------------+
@@ -61,23 +63,27 @@ The following table outlines all operators that can be used in Rule Engine expre
 | **Arithmetic-Comparison Operators**                                                                           |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``>``        | Greater than                 | :py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.BOOLEAN`,     |
-|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.FLOAT`,    |
-|              |                              | :py:attr:`~.DataType.NULL`, :py:attr:`~.DataType.STRING`        |
+|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.TIMEDELTA`,|
+|              |                              | :py:attr:`~.DataType.FLOAT`, :py:attr:`~.DataType.NULL`,        |
+|              |                              | :py:attr:`~.DataType.STRING`                                    |
 |              |                              | :sup:`2`                                                        |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``>=``       | Greater than or equal to     | :py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.BOOLEAN`,     |
-|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.FLOAT`,    |
-|              |                              | :py:attr:`~.DataType.NULL`, :py:attr:`~.DataType.STRING`        |
+|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.TIMEDELTA`,|
+|              |                              | :py:attr:`~.DataType.FLOAT`, :py:attr:`~.DataType.NULL`,        |
+|              |                              | :py:attr:`~.DataType.STRING`                                    |
 |              |                              | :sup:`2`                                                        |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``<``        | Less than                    | :py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.BOOLEAN`,     |
-|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.FLOAT`,    |
-|              |                              | :py:attr:`~.DataType.NULL`, :py:attr:`~.DataType.STRING`        |
+|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.TIMEDELTA`,|
+|              |                              | :py:attr:`~.DataType.FLOAT`, :py:attr:`~.DataType.NULL`,        |
+|              |                              | :py:attr:`~.DataType.STRING`                                    |
 |              |                              | :sup:`2`                                                        |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``<=``       | Less than or equal to        | :py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.BOOLEAN`,     |
-|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.FLOAT`,    |
-|              |                              | :py:attr:`~.DataType.NULL`, :py:attr:`~.DataType.STRING`        |
+|              |                              | :py:attr:`~.DataType.DATETIME`, :py:attr:`~.DataType.TIMEDELTA`,|
+|              |                              | :py:attr:`~.DataType.FLOAT`, :py:attr:`~.DataType.NULL`,        |
+|              |                              | :py:attr:`~.DataType.STRING`                                    |
 |              |                              | :sup:`2`                                                        |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | **Fuzzy-Comparison Operators**                                                                                |
@@ -103,11 +109,12 @@ The following table outlines all operators that can be used in Rule Engine expre
 | **Accessor Operators**                                                                                        |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``.``        | Attribute access             | :py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.DATETIME`,    |
-|              |                              | :py:attr:`~.DataType.MAPPING`, :py:attr:`~.DataType.STRING`     |
+|              |                              | :py:attr:`~.DataType.TIMEDELTA`, :py:attr:`~.DataType.MAPPING`, |
+|              |                              | :py:attr:`~.DataType.STRING`                                    |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``&.``       | Safe attribute access        | :py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.DATETIME`,    |
-|              |                              | :py:attr:`~.DataType.MAPPING`, :py:attr:`~.DataType.NULL`,      |
-|              |                              | :py:attr:`~.DataType.STRING`                                    |
+|              |                              | :py:attr:`~.DataType.TIMEDELTA`, :py:attr:`~.DataType.MAPPING`, |
+|              |                              | :py:attr:`~.DataType.NULL`, :py:attr:`~.DataType.STRING`        |
 +--------------+------------------------------+-----------------------------------------------------------------+
 | ``[``        | Item lookup                  | :py:attr:`~.DataType.ARRAY`, :py:attr:`~.DataType.MAPPING`,     |
 |              |                              | :py:attr:`~.DataType.STRING`                                    |
@@ -238,10 +245,15 @@ standard :py:attr:`~.DataType.STRING` value, while a single ``d`` will specify a
 by :py:meth:`dateutil.parser.isoparse`. :py:attr:`~.DataType.DATETIME` values with no time specified (e.g.
 ``d"2019-09-23"``) will evaluate to a :py:attr:`~.DataType.DATETIME` of the specified day at exactly midnight.
 
+:py:attr:`~.DataType.TIMEDELTA` literals must be specified in a subset of the ISO-8601 format for durations. Everything
+except years and months are supported in `~.DataType.TIMEDELTA` values, to match the underlying representation provided
+by the Python standard library.
+
 Example rules showing equivalent literal expressions:
 
 * ``"foobar" == s"foobar"``
 * ``d"2019-09-23" == d"2019-09-23 00:00:00"``
+* ``t"P1D" == t"PT24H"``
 
 :py:attr:`~.DataType.FLOAT` literals may be expressed in either binary, octal, decimal, or hexadecimal formats. The
 binary, octal and hexadecimal formats use the ``0b``, ``0o``, and ``0x`` prefixes respectively. Values in the decimal
diff --git a/docs/source/types.rst b/docs/source/types.rst
index cc78169..f671256 100644
--- a/docs/source/types.rst
+++ b/docs/source/types.rst
@@ -16,6 +16,8 @@ compatible with. For a information regarding supported operations, see the
 +-------------------------------+-------------------------------+
 | :py:attr:`~DataType.DATETIME` | :py:class:`datetime.date`,    |
 |                               | :py:class:`datetime.datetime` |
++-------------------------------+-------------------------------|
+| :py:attr:`~DataType.TIMEDELTA`| :py:class:`datetime.timedelta`|
 +-------------------------------+-------------------------------+
 | :py:attr:`~DataType.FLOAT`    | :py:class:`int`,              |
 |                               | :py:class:`float`             |
@@ -66,3 +68,18 @@ are converted to Rule Engine ``MAPPING`` values. While in Python the value ``{0.
 have a length of 2 with two unique keys, the same value once converted into a Rule Engine ``MAPPING`` would have a
 length of 1 with a single unique key. For this reason, developers using Rule Engine should take care to not use compound
 data types with a mix of Python :py:class:`float` and :py:class:`~decimal.Decimal` values.
+
+DATETIME and TIMEDELTA
+----------------------
+Version :release:`3.5.0` introduced the ``TIMEDELTA`` datatype, backed by Python's :py:class:`~datetime.timedelta` class.
+This also comes with the ability to perform arithmetic with both `TIMEDELTA` *and* `DATETIME` values. This allows you to
+create rules for things such as "has it been 30 days since this thing happened?" or "how much time passed between two
+events?".
+
+The following mathematical operations are supported:
+
+* adding a timedelta to a datetime (result is a datetime)
+* adding a timedelta to another timedelta (result is a timedelta)
+* subtracting a timedelta from a datetime (result is a datetime)
+* subtracting a datetime from another datetime (result is a timedelta)
+* subtracting a timedelta from another timedelta (result is a timedelta)
diff --git a/examples/database.py b/examples/database.py
index 15c4408..599588b 100644
--- a/examples/database.py
+++ b/examples/database.py
@@ -64,8 +64,8 @@ class Database(object):
 		data = self.data
 		if from_ is not None:
 			data = rule_engine.Rule(from_, context=self._rule_context).evaluate(data)
-		if isinstance(data, collections.abc.Mapping):
-			data = data.values()
+			if isinstance(data, collections.abc.Mapping):
+				data = data.values()
 		if not isiterable(data):
 			raise ValueError('data source is not iterable')
 		rule = rule_engine.Rule(where, context=self._rule_context)
diff --git a/lib/rule_engine/__init__.py b/lib/rule_engine/__init__.py
index 0ca05a1..994b245 100644
--- a/lib/rule_engine/__init__.py
+++ b/lib/rule_engine/__init__.py
@@ -30,7 +30,7 @@
 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #
 
-__version__ = '3.5.0'
+__version__ = '3.4.0'
 
 from .engine import resolve_attribute
 from .engine import resolve_item
diff --git a/lib/rule_engine/ast.py b/lib/rule_engine/ast.py
index ac5db13..303ed66 100644
--- a/lib/rule_engine/ast.py
+++ b/lib/rule_engine/ast.py
@@ -40,6 +40,7 @@ import re
 from . import errors
 from .suggestions import suggest_symbol
 from .types import *
+from .utils import parse_timedelta
 
 import dateutil.parser
 
@@ -237,6 +238,19 @@ class DatetimeExpression(LiteralExpressionBase):
 			dt = dt.replace(tzinfo=context.default_timezone)
 		return cls(context, dt)
 
+class TimedeltaExpression(LiteralExpressionBase):
+	"""
+	Literal timedelta expressions representing an offset from a specific point in time.
+	"""
+	result_type = DataType.TIMEDELTA
+	@classmethod
+	def from_string(cls, context, string):
+		try:
+			dt = parse_timedelta(string)
+		except ValueError:
+			raise errors.TimedeltaSyntaxError('invalid timedelta', string)
+		return cls(context, dt)
+
 class FloatExpression(LiteralExpressionBase):
 	"""Literal float expressions representing numerical values."""
 	result_type = DataType.FLOAT
@@ -312,7 +326,7 @@ class LeftOperatorRightExpressionBase(ExpressionBase):
 	A base class for representing complex expressions composed of a left side and a right side, separated by an
 	operator.
 	"""
-	compatible_types = (DataType.ARRAY, DataType.BOOLEAN, DataType.DATETIME, DataType.FLOAT, DataType.MAPPING, DataType.NULL, DataType.SET, DataType.STRING)
+	compatible_types = (DataType.ARRAY, DataType.BOOLEAN, DataType.DATETIME, DataType.TIMEDELTA, DataType.FLOAT, DataType.MAPPING, DataType.NULL, DataType.SET, DataType.STRING)
 	"""
 	A tuple containing the compatible data types that the left and right expressions must return. This can for example
 	be used to indicate that arithmetic operations are compatible with :py:attr:`~.DataType.FLOAT` but not
@@ -372,27 +386,78 @@ class LeftOperatorRightExpressionBase(ExpressionBase):
 
 class AddExpression(LeftOperatorRightExpressionBase):
 	"""A class for representing addition expressions from the grammar text."""
-	compatible_types = (DataType.FLOAT,DataType.STRING)
+	compatible_types = (DataType.FLOAT, DataType.STRING, DataType.DATETIME, DataType.TIMEDELTA)
 	result_type = DataType.UNDEFINED
 
 	def __init__(self, *args, **kwargs):
 		super(AddExpression, self).__init__(*args, **kwargs)
 		if self.left.result_type != DataType.UNDEFINED and self.right.result_type != DataType.UNDEFINED:
-			if self.left.result_type != self.right.result_type:
+			if self.left.result_type == DataType.DATETIME:
+				if self.right.result_type != DataType.TIMEDELTA:
+					raise errors.EvaluationError('data type mismatch')
+				self.result_type = self.left.result_type
+			elif self.left.result_type == DataType.TIMEDELTA:
+				if self.right.result_type not in (DataType.DATETIME, DataType.TIMEDELTA):
+					raise errors.EvaluationError('data type mismatch')
+				self.result_type = self.right.result_type
+			elif self.left.result_type != self.right.result_type:
 				raise errors.EvaluationError('data type mismatch')
-			self.result_type = self.left.result_type
+			else:
+				self.result_type = self.left.result_type
 
 	def _op_add(self, thing):
 		left_value = self.left.evaluate(thing)
 		right_value = self.right.evaluate(thing)
-		if isinstance(left_value, str) or isinstance(right_value, str):
+		if isinstance(left_value, datetime.datetime):
+			if not isinstance(right_value, datetime.timedelta):
+				raise errors.EvaluationError('data type mismatch (not a timedelta value)')
+		elif isinstance(left_value, datetime.timedelta):
+			if not isinstance(right_value, (datetime.timedelta, datetime.datetime)):
+				raise errors.EvaluationError('data type mismatch (not a datetime or timedelta value)')
+		elif isinstance(left_value, str) or isinstance(right_value, str):
 			_assert_is_string(left_value, right_value)
 		else:
 			_assert_is_numeric(left_value, right_value)
 		return operator.add(left_value, right_value)
 
+class SubtractExpression(LeftOperatorRightExpressionBase):
+	compatible_types = (DataType.FLOAT, DataType.DATETIME, DataType.TIMEDELTA)
+	result_type = DataType.UNDEFINED
+
+	def __init__(self, *args, **kwargs):
+		super(SubtractExpression, self).__init__(*args, **kwargs)
+		if self.left.result_type != DataType.UNDEFINED and self.right.result_type != DataType.UNDEFINED:
+			if self.left.result_type == DataType.DATETIME:
+				if self.right.result_type == DataType.DATETIME:
+					self.result_type = DataType.TIMEDELTA
+				elif self.right.result_type == DataType.TIMEDELTA:
+					self.result_type = DataType.DATETIME
+				else:
+					raise errors.EvaluationError('data type mismatch')
+			elif self.left.result_type == DataType.TIMEDELTA:
+				if self.right.result_type != DataType.TIMEDELTA:
+					raise errors.EvaluationError('data type mismatch')
+				self.result_type = self.left.result_type
+			elif self.left.result_type != self.right.result_type:
+				raise errors.EvaluationError('data type mismatch')
+			else:
+				self.result_type = self.left.result_type
+
+	def _op_sub(self, thing):
+		left_value = self.left.evaluate(thing)
+		right_value = self.right.evaluate(thing)
+		if isinstance(left_value, datetime.datetime):
+			if not isinstance(right_value, (datetime.datetime, datetime.timedelta)):
+				raise errors.EvaluationError('data type mismatch (not a datetime or timedelta value)')
+		elif isinstance(left_value, datetime.timedelta):
+			if not isinstance(right_value, datetime.timedelta):
+				raise errors.EvaluationError('data type mismatch (not a timedelta value)')
+		else:
+			_assert_is_numeric(left_value, right_value)
+		return operator.sub(left_value, right_value)
+
 class ArithmeticExpression(LeftOperatorRightExpressionBase):
-	"""A class for representing arithmetic expressions from the grammar text such as subtraction and division."""
+	"""A class for representing arithmetic expressions from the grammar text such as multiplication and division."""
 	compatible_types = (DataType.FLOAT,)
 	result_type = DataType.FLOAT
 	def __op_arithmetic(self, op, thing):
@@ -402,7 +467,6 @@ class ArithmeticExpression(LeftOperatorRightExpressionBase):
 		_assert_is_numeric(right_value)
 		return op(left_value, right_value)
 
-	_op_sub  = functools.partialmethod(__op_arithmetic, operator.sub)
 	_op_fdiv = functools.partialmethod(__op_arithmetic, operator.floordiv)
 	_op_tdiv = functools.partialmethod(__op_arithmetic, operator.truediv)
 	_op_mod  = functools.partialmethod(__op_arithmetic, operator.mod)
@@ -496,7 +560,7 @@ class ArithmeticComparisonExpression(ComparisonExpression):
 	A class for representing arithmetic comparison expressions from the grammar text such as less-than-or-equal-to and
 	greater-than.
 	"""
-	compatible_types = (DataType.ARRAY, DataType.BOOLEAN, DataType.DATETIME, DataType.FLOAT, DataType.NULL, DataType.STRING)
+	compatible_types = (DataType.ARRAY, DataType.BOOLEAN, DataType.DATETIME, DataType.TIMEDELTA, DataType.FLOAT, DataType.NULL, DataType.STRING)
 	def __init__(self, *args, **kwargs):
 		super(ArithmeticComparisonExpression, self).__init__(*args, **kwargs)
 		if self.left.result_type != DataType.UNDEFINED and self.right.result_type != DataType.UNDEFINED:
@@ -1042,10 +1106,13 @@ class UnaryExpression(ExpressionBase):
 		type_ = type_.lower()
 		self.type = type_
 		self._evaluator = getattr(self, '_op_' + type_)
-		self.result_type = {
-			'not':    DataType.BOOLEAN,
-			'uminus': DataType.FLOAT
-		}[type_]
+		if type_ == 'not':
+			self.result_type = DataType.BOOLEAN
+		elif type_ == 'uminus':
+			self.result_type = right.result_type
+		else:
+			raise errors.EvaluationError('unknown unary expression type')
+
 		self.right = right
 
 	@classmethod
@@ -1065,7 +1132,8 @@ class UnaryExpression(ExpressionBase):
 
 	def __op_arithmetic(self, op, thing):
 		right = self.right.evaluate(thing)
-		_assert_is_numeric(right)
+		if not is_numeric(right) and not isinstance(right, datetime.timedelta):
+			raise errors.EvaluationError('data type mismatch (not a numeric or timedelta value)')
 		return op(right)
 
 	_op_uminus = functools.partialmethod(__op_arithmetic, operator.neg)
@@ -1077,9 +1145,11 @@ class UnaryExpression(ExpressionBase):
 		if type_ == 'not':
 			return BooleanExpression(self.context, self.evaluate(None))
 		elif type_ == 'uminus':
-			if not isinstance(self.right, (FloatExpression,)):
-				raise errors.EvaluationError('data type mismatch (not a float expression)')
-			return FloatExpression(self.context, self.evaluate(None))
+			if isinstance(self.right, FloatExpression):
+				return FloatExpression(self.context, self.evaluate(None))
+			elif isinstance(self.right, TimedeltaExpression):
+				return TimedeltaExpression(self.context, self.evaluate(None))
+			raise errors.EvaluationError('data type mismatch (not a float or timedelta expression)')
 
 	def to_graphviz(self, digraph, *args, **kwargs):
 		digraph.node(str(id(self)), "{}\ntype={!r}".format(self.__class__.__name__, self.type.lower()))
diff --git a/lib/rule_engine/engine.py b/lib/rule_engine/engine.py
index 5bc35a0..0b5c5f4 100644
--- a/lib/rule_engine/engine.py
+++ b/lib/rule_engine/engine.py
@@ -217,6 +217,22 @@ class _AttributeResolver(object):
 	def datetime_zone_name(self, value):
 		return value.tzname()
 
+	@attribute('days', ast.DataType.TIMEDELTA, result_type=ast.DataType.FLOAT)
+	def timedelta_days(self, value):
+		return value.days
+
+	@attribute('seconds', ast.DataType.TIMEDELTA, result_type=ast.DataType.FLOAT)
+	def timedelta_seconds(self, value):
+		return value.seconds
+
+	@attribute('microseconds', ast.DataType.TIMEDELTA, result_type=ast.DataType.FLOAT)
+	def timedelta_microseconds(self, value):
+		return value.microseconds
+
+	@attribute('total_seconds', ast.DataType.TIMEDELTA, result_type=ast.DataType.FLOAT)
+	def timedelta_total_seconds(self, value):
+		return value.total_seconds()
+
 	@attribute('ceiling', ast.DataType.FLOAT, result_type=ast.DataType.FLOAT)
 	def float_ceiling(self, value):
 		return _float_op(value, math.ceil)
diff --git a/lib/rule_engine/errors.py b/lib/rule_engine/errors.py
index 2ef0965..3a03919 100644
--- a/lib/rule_engine/errors.py
+++ b/lib/rule_engine/errors.py
@@ -77,6 +77,17 @@ class DatetimeSyntaxError(SyntaxError):
 		self.value = value
 		"""The datetime value which contains the syntax error which caused this exception to be raised."""
 
+class TimedeltaSyntaxError(SyntaxError):
+	"""An error raised for issues regarding the use of improperly formatted timedelta expressions."""
+	def __init__(self, message, value):
+		"""
+		:param str message: A text description of what error occurred.
+		:param str value: The timedelta value which contains the syntax error which caused this exception to be raised.
+		"""
+		super(TimedeltaSyntaxError, self).__init__(message)
+		self.value = value
+		"""The timedelta value which contains the syntax error which caused this exception to be raised."""
+
 class RegexSyntaxError(SyntaxError):
 	"""An error raised for issues regarding the use of improper regular expression syntax."""
 	def __init__(self, message, error, value):
diff --git a/lib/rule_engine/parser.py b/lib/rule_engine/parser.py
index 4281a3d..4e00fef 100644
--- a/lib/rule_engine/parser.py
+++ b/lib/rule_engine/parser.py
@@ -37,6 +37,7 @@ import types as pytypes
 
 from . import ast
 from . import errors
+from .utils import timedelta_regex
 
 import ply.lex as lex
 import ply.yacc as yacc
@@ -153,7 +154,7 @@ class Parser(ParserBase):
 		'if': 'IF'
 	}
 	tokens = (
-		'DATETIME', 'FLOAT', 'STRING', 'SYMBOL',
+		'DATETIME', 'TIMEDELTA', 'FLOAT', 'STRING', 'SYMBOL',
 		'LPAREN', 'RPAREN', 'QMARK', 'COLON', 'COMMA',
 		'LBRACKET', 'RBRACKET', 'LBRACE', 'RBRACE'
 	) + tuple(set(list(reserved_words.values()) + list(op_names.values())))
@@ -256,6 +257,11 @@ class Parser(ParserBase):
 		t.value = t.value[1:]
 		return t
 
+	def t_TIMEDELTA(self, t):
+		t.value = t.value[2:-1]
+		return t
+	t_TIMEDELTA.__doc__ = r't(?P<quote>["\'])' + timedelta_regex + r'(?P=quote)'
+
 	def t_STRING(self, t):
 		r's?(?P<quote>["\'])([^\\\n]|(\\.))*?(?P=quote)'
 		if t.value[0] == 's':
@@ -307,8 +313,7 @@ class Parser(ParserBase):
 
 	def p_expression_arithmetic(self, p):
 		"""
-		expression : expression SUB    expression
-				   | expression MOD    expression
+		expression : expression MOD    expression
 				   | expression MUL    expression
 				   | expression FDIV   expression
 				   | expression TDIV   expression
@@ -326,6 +331,14 @@ class Parser(ParserBase):
 		op_name = self.op_names[op]
 		p[0] = _DeferredAstNode(ast.AddExpression, args=(self.context, op_name, left, right))
 
+	def p_expression_sub(self, p):
+		"""
+		expression : expression SUB    expression
+		"""
+		left, op, right = p[1:4]
+		op_name = self.op_names[op]
+		p[0] = _DeferredAstNode(ast.SubtractExpression, args=(self.context, op_name, left, right))
+
 	def p_expression_bitwise(self, p):
 		"""
 		expression : expression BWAND  expression
@@ -432,6 +445,10 @@ class Parser(ParserBase):
 		'object : DATETIME'
 		p[0] = _DeferredAstNode(ast.DatetimeExpression, args=(self.context, literal_eval(p[1])), method='from_string')
 
+	def p_expression_timedelta(self, p):
+		'object : TIMEDELTA'
+		p[0] = _DeferredAstNode(ast.TimedeltaExpression, args=(self.context, p[1]), method='from_string')
+
 	def p_expression_float(self, p):
 		'expression : FLOAT'
 		p[0] = _DeferredAstNode(ast.FloatExpression, args=(self.context, float(literal_eval(p[1]))))
diff --git a/lib/rule_engine/types.py b/lib/rule_engine/types.py
index 1761cb5..ade3866 100644
--- a/lib/rule_engine/types.py
+++ b/lib/rule_engine/types.py
@@ -363,6 +363,7 @@ class DataType(metaclass=DataTypeMeta):
 	"""
 	BOOLEAN = _DataTypeDef('BOOLEAN', bool)
 	DATETIME = _DataTypeDef('DATETIME', datetime.datetime)
+	TIMEDELTA = _DataTypeDef('TIMEDELTA', datetime.timedelta)
 	FLOAT = _DataTypeDef('FLOAT', decimal.Decimal)
 	MAPPING = _MappingDataTypeDef('MAPPING', dict)
 	"""
@@ -421,6 +422,8 @@ class DataType(metaclass=DataTypeMeta):
 			return cls.BOOLEAN
 		elif python_type is datetime.date or python_type is datetime.datetime:
 			return cls.DATETIME
+		elif python_type is datetime.timedelta:
+			return cls.TIMEDELTA
 		elif python_type in (decimal.Decimal, float, int):
 			return cls.FLOAT
 		elif python_type is dict:
@@ -447,6 +450,8 @@ class DataType(metaclass=DataTypeMeta):
 			return cls.BOOLEAN
 		elif isinstance(python_value, (datetime.date, datetime.datetime)):
 			return cls.DATETIME
+		elif isinstance(python_value, datetime.timedelta):
+			return cls.TIMEDELTA
 		elif isinstance(python_value, (decimal.Decimal, float, int)):
 			return cls.FLOAT
 		elif python_value is None:
diff --git a/lib/rule_engine/utils.py b/lib/rule_engine/utils.py
new file mode 100644
index 0000000..44f2cc9
--- /dev/null
+++ b/lib/rule_engine/utils.py
@@ -0,0 +1,38 @@
+import datetime
+import re
+
+
+_sub_regex = r'[0-9]+([,.][0-9]+)?'
+timedelta_regex = (
+    r'P(?!\b)'
+    r'(?P<weeks>' + _sub_regex + r'W)?'
+    r'(?P<days>' + _sub_regex + r'D)?'
+    r'(T'
+    r'(?P<hours>' + _sub_regex + r'H)?'
+    r'(?P<minutes>' + _sub_regex + r'M)?'
+    r'(?P<seconds>' + _sub_regex + r'S)?'
+    r')?'
+)
+timedelta_re = re.compile("^" + timedelta_regex + "$")
+
+def parse_timedelta(periodstring):
+    if periodstring == "P":
+        raise ValueError('empty timedelta string')
+
+    match = timedelta_re.match(periodstring)
+    if not match:
+        raise ValueError('invalid timedelta string')
+
+    groups = match.groupdict()
+    for key, val in groups.items():
+        if val is None:
+            val = "0n"
+        groups[key] = float(val[:-1].replace(',', '.'))
+
+    return datetime.timedelta(
+        weeks=groups['weeks'],
+        days=groups['days'],
+        hours=groups['hours'],
+        minutes=groups['minutes'],
+        seconds=groups['seconds'],
+    )


Test patch:
diff --git a/tests/ast/__init__.py b/tests/ast/__init__.py
index eff7cd4..9f7725a 100644
--- a/tests/ast/__init__.py
+++ b/tests/ast/__init__.py
@@ -30,6 +30,7 @@
 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #
 
+import datetime
 import unittest
 
 from .expression import *
@@ -86,11 +87,16 @@ class AstTests(unittest.TestCase):
 		statement = parser_.parse('false and not true', self.context)
 		self.assertFalse(statement.evaluate(None))
 
-	def test_ast_evaluates_unary_uminus(self):
+	def test_ast_evaluates_unary_uminus_float(self):
 		parser_ = parser.Parser()
 		statement = parser_.parse('-(2 * 5)', self.context)
 		self.assertEqual(statement.evaluate(None), -10)
 
+	def test_ast_evaluates_unary_uminus_timedelta(self):
+		parser_ = parser.Parser()
+		statement = parser_.parse('-(t"PT1H" + t"PT6M")', self.context)
+		self.assertEqual(statement.evaluate(None), datetime.timedelta(days=-1, seconds=82440))
+
 	def test_ast_raises_type_mismatch_arithmetic_comparisons(self):
 		parser_ = parser.Parser()
 		statement = parser_.parse('symbol < 1', self.context)
@@ -170,6 +176,53 @@ class AstTests(unittest.TestCase):
 		self.assertIsInstance(statement.expression, ast.AddExpression)
 		self.assertEqual(statement.evaluate(thing), 'LukeSkywalker')
 
+	def test_ast_reduces_add_timedelta(self):
+		thing = {'first': datetime.timedelta(seconds=5), 'last': datetime.timedelta(minutes=1)}
+		parser_ = parser.Parser()
+
+		statement = parser_.parse('t"PT5S" + t"PT1M"', self.context)
+		self.assertIsInstance(statement.expression, ast.TimedeltaExpression)
+		self.assertEqual(statement.evaluate(None), datetime.timedelta(minutes=1, seconds=5))
+
+		statement = parser_.parse('first + t"PT1M"', self.context)
+		self.assertIsInstance(statement.expression, ast.AddExpression)
+		self.assertEqual(statement.evaluate(thing), datetime.timedelta(minutes=1, seconds=5))
+
+		statement = parser_.parse('t"PT5S" + last', self.context)
+		self.assertIsInstance(statement.expression, ast.AddExpression)
+		self.assertEqual(statement.evaluate(thing), datetime.timedelta(minutes=1, seconds=5))
+
+	def test_ast_reduces_subtract_float(self):
+		thing = {'one': 1, 'two': 2}
+		parser_ = parser.Parser()
+		statement = parser_.parse('2 - 1', self.context)
+		self.assertIsInstance(statement.expression, ast.FloatExpression)
+		self.assertEqual(statement.evaluate(None), 1)
+
+		statement = parser_.parse('two - 1', self.context)
+		self.assertIsInstance(statement.expression, ast.SubtractExpression)
+		self.assertEqual(statement.evaluate(thing), 1)
+
+		statement = parser_.parse('1 - two', self.context)
+		self.assertIsInstance(statement.expression, ast.SubtractExpression)
+		self.assertEqual(statement.evaluate(thing), -1)
+
+	def test_ast_reduces_subtract_timedelta(self):
+		thing = {'first': datetime.timedelta(seconds=5), 'last': datetime.timedelta(minutes=1)}
+		parser_ = parser.Parser()
+
+		statement = parser_.parse('t"PT1M" - t"PT5S"', self.context)
+		self.assertIsInstance(statement.expression, ast.TimedeltaExpression)
+		self.assertEqual(statement.evaluate(None), datetime.timedelta(seconds=55))
+
+		statement = parser_.parse('first - t"PT1M"', self.context)
+		self.assertIsInstance(statement.expression, ast.SubtractExpression)
+		self.assertEqual(statement.evaluate(thing), -datetime.timedelta(seconds=55))
+
+		statement = parser_.parse('t"PT5S" - last', self.context)
+		self.assertIsInstance(statement.expression, ast.SubtractExpression)
+		self.assertEqual(statement.evaluate(thing), -datetime.timedelta(seconds=55))
+
 	def test_ast_reduces_arithmetic(self):
 		thing = {'two': 2, 'four': 4}
 		parser_ = parser.Parser()
@@ -214,12 +267,35 @@ class AstTests(unittest.TestCase):
 		self.assertIsInstance(statement.expression, ast.FloatExpression)
 		self.assertEqual(statement.evaluate(None), 1)
 
+	def test_ast_reduces_unary_uminus_float(self):
+		parser_ = parser.Parser()
+
+		statement = parser_.parse('-1.0', self.context)
+		self.assertIsInstance(statement.expression, ast.FloatExpression)
+		self.assertEqual(statement.evaluate(None), -1)
+
+		statement = parser_.parse('-one', self.context)
+		self.assertIsInstance(statement.expression, ast.UnaryExpression)
+		self.assertEqual(statement.evaluate({'one': 1}), -1)
+
+	def test_ast_reduces_unary_uminus_timedelta(self):
+		parser_ = parser.Parser()
+
+		statement = parser_.parse('-t"P1D"', self.context)
+		self.assertIsInstance(statement.expression, ast.TimedeltaExpression)
+		self.assertEqual(statement.evaluate(None), datetime.timedelta(days=-1))
+
+		statement = parser_.parse('-day', self.context)
+		self.assertIsInstance(statement.expression, ast.UnaryExpression)
+		self.assertEqual(statement.evaluate({'day': datetime.timedelta(days=1)}), datetime.timedelta(days=-1))
+
 	def test_ast_type_hints(self):
 		parser_ = parser.Parser()
 		cases = (
 			# type,             type_is,             type_is_not
 			('symbol << 1',     ast.DataType.FLOAT,  ast.DataType.STRING),
 			('symbol + 1',      ast.DataType.FLOAT,  ast.DataType.STRING),
+			('symbol - 1',      ast.DataType.FLOAT,  ast.DataType.STRING),
 			('symbol[1]',       ast.DataType.STRING, ast.DataType.FLOAT),
 			('symbol[1]',       ast.DataType.ARRAY,  ast.DataType.FLOAT),
 			('symbol =~ "foo"', ast.DataType.STRING, ast.DataType.FLOAT),
diff --git a/tests/ast/expression/attribute.py b/tests/ast/expression/attribute.py
index bf70528..2b36d5d 100644
--- a/tests/ast/expression/attribute.py
+++ b/tests/ast/expression/attribute.py
@@ -111,6 +111,20 @@ class GetAttributeExpressionTests(unittest.TestCase):
 			expression = ast.GetAttributeExpression(context, symbol, attribute_name)
 			self.assertEqual(expression.evaluate(None), value, "attribute {} failed".format(attribute_name))
 
+	def test_ast_expression_timedelta_attributes(self):
+		timedelta = datetime.timedelta(weeks=7, days=6, hours=5, minutes=4, seconds=3, milliseconds=2, microseconds=1)
+		symbol = ast.TimedeltaExpression(context, timedelta)
+
+		attributes = {
+			'days': 55,
+			'seconds': 18243,
+			'microseconds': 2001,
+			'total_seconds': decimal.Decimal('4770243.002001'),
+		}
+		for attribute_name, value in attributes.items():
+			expression = ast.GetAttributeExpression(context, symbol, attribute_name)
+			self.assertEqual(expression.evaluate(None), value, "attribute {} failed".format(attribute_name))
+
 	def test_ast_expression_float_attributes(self):
 		flt = decimal.Decimal('3.14159')
 		symbol = ast.SymbolExpression(context, 'flt')
diff --git a/tests/ast/expression/left_operator_right.py b/tests/ast/expression/left_operator_right.py
index ecc9df6..0a11c65 100644
--- a/tests/ast/expression/left_operator_right.py
+++ b/tests/ast/expression/left_operator_right.py
@@ -42,6 +42,8 @@ import rule_engine.errors as errors
 __all__ = (
 	'ArithmeticExpressionTests',
 	'AddExpressionTests',
+	'AddDatetimeExpressionTests',
+	'SubtractExpressionTests',
 	'BitwiseExpressionTests',
 	'BitwiseExpressionSetTests',
 	'BitwiseShiftExpressionTests',
@@ -80,7 +82,6 @@ class ArithmeticExpressionTests(LeftOperatorRightExpresisonTestsBase):
 	left_value = two = ast.FloatExpression(context, 2.0)
 	right_value = four = ast.FloatExpression(context, 4.0)
 	def test_ast_expression_left_operator_right_arithmetic(self):
-		self.assertExpressionTests('sub', equals_value=-2.0)
 		self.assertExpressionTests('fdiv', equals_value=0.0)
 		self.assertExpressionTests('tdiv', equals_value=0.5)
 		self.assertExpressionTests('mod', equals_value=2.0)
@@ -88,7 +89,7 @@ class ArithmeticExpressionTests(LeftOperatorRightExpresisonTestsBase):
 		self.assertExpressionTests('pow', equals_value=16.0)
 
 	def test_ast_expression_left_operator_right_arithmetic_type_errors(self):
-		for operation in ('sub', 'fdiv', 'tdiv', 'mod', 'mul', 'pow'):
+		for operation in ('fdiv', 'tdiv', 'mod', 'mul', 'pow'):
 			with self.assertRaises(errors.EvaluationError):
 				self.assertExpressionTests(operation, ast.FloatExpression(context, 2.0), ast.StringExpression(context, '4.0'))
 			with self.assertRaises(errors.EvaluationError):
@@ -120,6 +121,171 @@ class AddExpressionTests(LeftOperatorRightExpresisonTestsBase):
 			self.assertExpressionTests('add', ast.BooleanExpression(context, True), ast.StringExpression(context, 'b'))
 		with self.assertRaises(errors.EvaluationError):
 			self.assertExpressionTests('add', ast.StringExpression(context, 'b'), ast.BooleanExpression(context, True))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.DatetimeExpression(context, datetime.datetime.now()), ast.StringExpression(context, 'abc'))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.DatetimeExpression(context, datetime.datetime.now()), ast.FloatExpression(context, 6.0))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.DatetimeExpression(context, datetime.datetime.now()), ast.BooleanExpression(context, False))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.DatetimeExpression(context, datetime.datetime.now()), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.StringExpression(context, 'abc'), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.FloatExpression(context, 6.0), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.BooleanExpression(context, False), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.TimedeltaExpression(context, datetime.timedelta()), ast.StringExpression(context, 'abc'))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.TimedeltaExpression(context, datetime.timedelta()), ast.FloatExpression(context, 6.0))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.TimedeltaExpression(context, datetime.timedelta()), ast.BooleanExpression(context, False))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.StringExpression(context, 'abc'), ast.TimedeltaExpression(context, datetime.timedelta()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.FloatExpression(context, 6.0), ast.TimedeltaExpression(context, datetime.timedelta()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('add', ast.BooleanExpression(context, False), ast.TimedeltaExpression(context, datetime.timedelta()))
+
+class AddDatetimeExpressionTests(LeftOperatorRightExpresisonTestsBase):
+	ExpressionClass = ast.AddExpression
+	def test_add_datetime_to_timedelta(self):
+		start_datetime = datetime.datetime(year=2022, month=6, day=28, hour=1, minute=2, second=3)
+		start_datetime_expr = ast.DatetimeExpression(context, start_datetime)
+		assert_func = functools.partial(self.assertExpressionTests, 'add')
+		td_expr_func = functools.partial(ast.TimedeltaExpression, context)
+
+		assert_func(
+			left_value=start_datetime_expr,
+			right_value=td_expr_func(datetime.timedelta(hours=3, minutes=2, seconds=1)),
+			equals_value=start_datetime.replace(hour=4, minute=4, second=4),
+		)
+		assert_func(
+			left_value=td_expr_func(datetime.timedelta(days=1, minutes=30, seconds=5)),
+			right_value=start_datetime_expr,
+			equals_value=start_datetime.replace(day=29, minute=32, second=8),
+		)
+		assert_func(
+			left_value=start_datetime_expr,
+			right_value=ast.TimedeltaExpression(context, datetime.timedelta()),
+			equals_value=start_datetime,
+		)
+
+	def test_add_timedeltas(self):
+		assert_func = functools.partial(self.assertExpressionTests, 'add')
+		td_expr_func = functools.partial(ast.TimedeltaExpression, context)
+
+		assert_func(
+			left_value=td_expr_func(datetime.timedelta(weeks=6, days=5, hours=4, minutes=3, seconds=2)),
+			right_value=td_expr_func(datetime.timedelta(seconds=4)),
+			equals_value=datetime.timedelta(weeks=6, days=5, hours=4, minutes=3, seconds=6),
+		)
+		assert_func(
+			left_value=td_expr_func(datetime.timedelta()),
+			right_value=td_expr_func(datetime.timedelta(days=6, minutes=25, seconds=42)),
+			equals_value=datetime.timedelta(days=6, minutes=25, seconds=42),
+		)
+		assert_func(
+			left_value=td_expr_func(datetime.timedelta(hours=4)),
+			right_value=td_expr_func(datetime.timedelta(days=1, seconds=54)),
+			equals_value=datetime.timedelta(days=1, hours=4, seconds=54),
+		)
+
+class SubtractExpressionTests(LeftOperatorRightExpresisonTestsBase):
+	ExpressionClass = ast.SubtractExpression
+	false_value = 0.0
+	left_value = ten = ast.FloatExpression(context, 10.0)
+	right_value = five = ast.FloatExpression(context, 5.0)
+	def test_ast_expression_left_operator_right_subtract(self):
+		self.assertExpressionTests('sub', equals_value=5.0)
+		self.assertExpressionTests('sub', left_value=self.right_value, right_value=self.left_value, equals_value=-5.0)
+
+	def test_ast_expression_left_operator_right_subtract_type_errors(self):
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.FloatExpression(context, 12.0), ast.StringExpression(context, "abc"))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.StringExpression(context, "def"), ast.FloatExpression(context, 4.0))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.FloatExpression(context, 14.5), ast.BooleanExpression(context, True))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.BooleanExpression(context, False), ast.FloatExpression(context, 9.9))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.DatetimeExpression(context, datetime.datetime.now()), ast.StringExpression(context, "ghi"))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.DatetimeExpression(context, datetime.datetime.now()), ast.FloatExpression(context, 8.4))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.DatetimeExpression(context, datetime.datetime.now()), ast.BooleanExpression(context, True))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.StringExpression(context, "jkl"), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.FloatExpression(context, 7.7), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.BooleanExpression(context, False), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.TimedeltaExpression(context, datetime.timedelta()), ast.DatetimeExpression(context, datetime.datetime.now()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.TimedeltaExpression(context, datetime.timedelta()), ast.StringExpression(context, "ghi"))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.TimedeltaExpression(context, datetime.timedelta()), ast.FloatExpression(context, 8.4))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.TimedeltaExpression(context, datetime.timedelta()), ast.BooleanExpression(context, True))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.StringExpression(context, "jkl"), ast.TimedeltaExpression(context, datetime.timedelta()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.FloatExpression(context, 7.7), ast.TimedeltaExpression(context, datetime.timedelta()))
+		with self.assertRaises(errors.EvaluationError):
+			self.assertExpressionTests('sub', ast.BooleanExpression(context, False), ast.TimedeltaExpression(context, datetime.timedelta()))
+
+class SubtractDatetimeExpressionTests(LeftOperatorRightExpresisonTestsBase):
+	ExpressionClass = ast.SubtractExpression
+	def test_subtract_datetime_from_datetime(self):
+		dt_expr_func = functools.partial(ast.DatetimeExpression, context)
+		start_datetime_expr = dt_expr_func(datetime.datetime(year=2022, month=3, day=15, hour=13, minute=6, second=12))
+		assert_func = functools.partial(self.assertExpressionTests, 'sub')
+
+		assert_func(
+			left_value=start_datetime_expr,
+			right_value=dt_expr_func(datetime.datetime(year=2022, month=3, day=12, hour=9, minute=34, second=11)),
+			equals_value=datetime.timedelta(days=3, seconds=12721),
+		)
+		assert_func(
+			left_value=start_datetime_expr,
+			right_value=dt_expr_func(datetime.datetime(year=2022, month=4, day=2, hour=3, minute=56, second=22)),
+			equals_value=datetime.timedelta(days=-18, seconds=32990),
+		)
+
+	def test_subtract_timedelta_from_datetime(self):
+		start_datetime = datetime.datetime(year=2022, month=1, day=24, hour=16, minute=19, second=44)
+		start_datetime_expr = ast.DatetimeExpression(context, start_datetime)
+		assert_func = functools.partial(self.assertExpressionTests, 'sub')
+		td_expr_func = functools.partial(ast.TimedeltaExpression, context)
+
+		assert_func(
+			left_value=start_datetime_expr,
+			right_value=td_expr_func(datetime.timedelta(days=21, hours=2)),
+			equals_value=start_datetime.replace(day=3, hour=14),
+		)
+		assert_func(
+			left_value=start_datetime_expr,
+			right_value=td_expr_func(-datetime.timedelta(hours=10, minutes=39, seconds=20)),
+			equals_value=start_datetime.replace(day=25, hour=2, minute=59, second=4),
+		)
+
+	def test_subtract_timedelta_from_timedelta(self):
+		assert_func = functools.partial(self.assertExpressionTests, 'sub')
+		td_expr_func = functools.partial(ast.TimedeltaExpression, context)
+
+		assert_func(
+			left_value=td_expr_func(datetime.timedelta(days=8, minutes=44, seconds=12)),
+			right_value=td_expr_func(datetime.timedelta(seconds=23)),
+			equals_value=datetime.timedelta(days=8, seconds=2629),
+		)
+		assert_func(
+			left_value=td_expr_func(datetime.timedelta(hours=15, minutes=35)),
+			right_value=td_expr_func(datetime.timedelta(minutes=41, seconds=45)),
+			equals_value=datetime.timedelta(seconds=53595),
+		)
 
 class BitwiseExpressionTests(LeftOperatorRightExpresisonTestsBase):
 	ExpressionClass = ast.BitwiseExpression
@@ -247,6 +413,14 @@ class ArithmeticComparisonExpressionTests(LeftOperatorRightExpresisonTestsBase):
 		self.assertExpressionTests('le', past_date, now, True)
 		self.assertExpressionTests('lt', past_date, now, True)
 
+	def test_ast_expression_left_operator_right_arithmeticcomparison_timedelta(self):
+		smaller_period = ast.TimedeltaExpression(context, datetime.timedelta(seconds=1))
+		larger_period = ast.TimedeltaExpression(context, datetime.timedelta(minutes=1))
+		self.assertExpressionTests('ge', smaller_period, larger_period, False)
+		self.assertExpressionTests('gt', smaller_period, larger_period, False)
+		self.assertExpressionTests('le', smaller_period, larger_period, True)
+		self.assertExpressionTests('lt', smaller_period, larger_period, True)
+
 	def test_ast_expression_left_operator_right_arithmeticcomparison_float(self):
 		neg_one = ast.FloatExpression(context, -1.0)
 		zero = ast.FloatExpression(context, 0.0)
diff --git a/tests/ast/expression/literal.py b/tests/ast/expression/literal.py
index 062bf95..86f5e77 100644
--- a/tests/ast/expression/literal.py
+++ b/tests/ast/expression/literal.py
@@ -48,6 +48,7 @@ context.builtins = engine.Builtins.from_defaults(
 falseish = (
 	ast.ArrayExpression(context, tuple()),
 	ast.BooleanExpression(context, False),
+	ast.TimedeltaExpression(context, datetime.timedelta()),
 	ast.FloatExpression(context, 0.0),
 	ast.NullExpression(context),
 	ast.StringExpression(context, '')
@@ -58,6 +59,7 @@ trueish = (
 	ast.ArrayExpression(context, tuple((ast.FloatExpression(context, 1.0),))),
 	ast.BooleanExpression(context, True),
 	ast.DatetimeExpression(context, datetime.datetime.now()),
+	ast.TimedeltaExpression(context, datetime.timedelta(seconds=1)),
 	ast.FloatExpression(context, float('-inf')),
 	ast.FloatExpression(context, -1.0),
 	ast.FloatExpression(context, 1.0),
@@ -87,6 +89,7 @@ class LiteralExpressionTests(unittest.TestCase):
 			(ast.ArrayExpression, ()),
 			(ast.BooleanExpression, False),
 			(ast.DatetimeExpression, datetime.datetime(2020, 1, 1)),
+			(ast.TimedeltaExpression, datetime.timedelta(seconds=42)),
 			(ast.FloatExpression, 0),
 			(ast.MappingExpression, {}),
 			(ast.SetExpression, set()),
diff --git a/tests/ast/expression/miscellaneous.py b/tests/ast/expression/miscellaneous.py
index a6933e3..70a8e6a 100644
--- a/tests/ast/expression/miscellaneous.py
+++ b/tests/ast/expression/miscellaneous.py
@@ -359,14 +359,14 @@ class UnaryExpressionTests(unittest.TestCase):
 
 	def test_ast_expression_unary_uminus(self):
 		for value in trueish:
-			if not isinstance(value, ast.FloatExpression):
+			if not isinstance(value, (ast.FloatExpression, ast.TimedeltaExpression)):
 				continue
 			unary = ast.UnaryExpression(context, 'uminus', value)
 			result = unary.evaluate(None)
 			self.assertTrue(result)
 			self.assertNotEqual(result, value.value)
 		for value in falseish:
-			if not isinstance(value, ast.FloatExpression):
+			if not isinstance(value, (ast.FloatExpression, ast.TimedeltaExpression)):
 				continue
 			unary = ast.UnaryExpression(context, 'uminus', value)
 			result = unary.evaluate(None)
@@ -375,7 +375,7 @@ class UnaryExpressionTests(unittest.TestCase):
 
 	def test_ast_expresison_unary_minus_type_errors(self):
 		for value in trueish + falseish:
-			if isinstance(value, ast.FloatExpression):
+			if isinstance(value, (ast.FloatExpression, ast.TimedeltaExpression)):
 				continue
 			unary = ast.UnaryExpression(context, 'uminus', value)
 			with self.assertRaises(errors.EvaluationError):
diff --git a/tests/engine.py b/tests/engine.py
index daf8646..f025477 100644
--- a/tests/engine.py
+++ b/tests/engine.py
@@ -123,6 +123,10 @@ class EngineTests(unittest.TestCase):
 		today = builtins['today']
 		self.assertIsInstance(today, datetime.date)
 
+		self.assertIn('now', builtins)
+		now = builtins['now']
+		self.assertIsInstance(now, datetime.datetime)
+
 		# test that builtins have correct type hints
 		builtins = engine.Builtins.from_defaults(
 			{'name': 'Alice'},
@@ -235,5 +239,58 @@ class EngineRuleTests(unittest.TestCase):
 		self.test_engine_rule_matches(rule=debug_rule)
 		self.test_engine_rule_filter(rule=debug_rule)
 
+
+class EngineDatetimeRuleTests(unittest.TestCase):
+	def test_add_timedeltas(self):
+		rule = engine.Rule("t'P4DT2H31S' + t'P1WT45M17S' == t'P1W4DT2H45M48S'")
+		self.assertTrue(rule.evaluate({}))
+
+	def test_add_empty_timedelta(self):
+		rule = engine.Rule("t'P1DT3S' + t'PT' == t'P1DT3S'")
+		self.assertTrue(rule.evaluate({}))
+
+	def test_add_to_today(self):
+		rule = engine.Rule("$today + t'PT' == $today")
+		self.assertTrue(rule.evaluate({}))
+
+	def test_add_datetime_to_timedelta(self):
+		rule = engine.Rule("d'2022-05-23 08:23' + t'PT4H3M2S' == d'2022-05-23 12:26:02'")
+		self.assertTrue(rule.evaluate({}))
+
+		rule = engine.Rule("start + t'PT1H' == end")
+		self.assertTrue(rule.evaluate({
+			"start": datetime.datetime(year=2022, month=2, day=28, hour=23, minute=32, second=56),
+			"end": datetime.datetime(year=2022, month=3, day=1, hour=0, minute=32, second=56),
+		}))
+
+	def test_subtract_timedeltas(self):
+		rule = engine.Rule("t'P4DT2H31S' - t'P1DT45S' == t'P3DT1H59M46S'")
+		self.assertTrue(rule.evaluate({}))
+
+		rule = engine.Rule("t'P4DT2H31S' - t'P1WT45M17S' == -t'P2DT22H44M46S'")
+		self.assertTrue(rule.evaluate({}))
+
+	def test_subtract_empty_timedelta(self):
+		rule = engine.Rule("t'P1DT3S' - t'PT' == t'P1DT3S'")
+		self.assertTrue(rule.evaluate({}))
+
+	def test_subtract_from_today(self):
+		rule = engine.Rule("$today - t'PT' == $today")
+		self.assertTrue(rule.evaluate({}))
+
+	def test_subtract_datetime_from_datetime(self):
+		rule = engine.Rule("d'2022-05-23 14:12' - d'2022-05-23 12:15' == t'PT1H57M'")
+		self.assertTrue(rule.evaluate({}))
+
+		rule = engine.Rule("end - t'PT1H' == start")
+		self.assertTrue(rule.evaluate({
+			"start": datetime.datetime(year=2022, month=2, day=28, hour=23, minute=32, second=56),
+			"end": datetime.datetime(year=2022, month=3, day=1, hour=0, minute=32, second=56),
+		}))
+
+	def test_subtract_timedelta_from_datetime(self):
+		rule = engine.Rule("d'2022-06-12' - t'P1D' == d'2022-06-11'")
+		self.assertTrue(rule.evaluate({}))
+
 if __name__ == '__main__':
 	unittest.main()
diff --git a/tests/parser.py b/tests/parser.py
index 26dba31..3cf3760 100644
--- a/tests/parser.py
+++ b/tests/parser.py
@@ -182,7 +182,6 @@ class ParserLeftOperatorRightTests(ParserTestsBase):
 
 	def test_parser_arithmetic_expressions(self):
 		expressions = (
-			'left - right',
 			'left / right',
 			'left // right',
 			'left % right',
@@ -199,6 +198,13 @@ class ParserLeftOperatorRightTests(ParserTestsBase):
 		for expression in expressions:
 			self.assertStatementType(expression, ast.AddExpression)
 
+	def test_parser_subtract_expressions(self):
+		expressions = (
+			'left - right',
+		)
+		for expression in expressions:
+			self.assertStatementType(expression, ast.SubtractExpression)
+
 	def test_parser_bitwise_expressions(self):
 		expressions = (
 			'left & right',
@@ -302,6 +308,18 @@ class ParserLiteralTests(ParserTestsBase):
 		else:
 			self.fail('DatetimeSyntaxError was not raised')
 
+	def test_parse_timedelta(self):
+		self.assertLiteralStatementEqual('t"P1W"', ast.TimedeltaExpression, datetime.timedelta(weeks=1))
+		self.assertLiteralStatementEqual('t"P7W6DT5H4M3S"', ast.TimedeltaExpression, datetime.timedelta(weeks=7, days=6, hours=5, minutes=4, seconds=3))
+		self.assertLiteralStatementEqual('t"PT3H2S"', ast.TimedeltaExpression, datetime.timedelta(hours=3, seconds=2))
+		self.assertLiteralStatementEqual('t"PT"', ast.TimedeltaExpression, datetime.timedelta())
+
+	def test_parse_timedelta_attributes(self):
+		self.assertLiteralStatementEvalEqual('t"P7W6DT5H4M3S".days', decimal.Decimal('55'))
+		self.assertLiteralStatementEvalEqual('t"P7W6DT5H4M3S".seconds', decimal.Decimal('18243'))
+		self.assertLiteralStatementEvalEqual('t"P7W6DT5H4M3S".microseconds', decimal.Decimal('0'))
+		self.assertLiteralStatementEvalEqual('t"P7W6DT5H4M3S".total_seconds', decimal.Decimal('4770243'))
+
 	def test_parse_float(self):
 		self.assertLiteralStatementEqual('3.14', ast.FloatExpression, decimal.Decimal('3.14'))
 		self.assertLiteralStatementEqual('3.140', ast.FloatExpression, decimal.Decimal('3.140'))
diff --git a/tests/types.py b/tests/types.py
index a6b69a2..5b7501a 100644
--- a/tests/types.py
+++ b/tests/types.py
@@ -68,6 +68,7 @@ class DataTypeTests(unittest.TestCase):
 		self.assertIs(DataType.from_name('ARRAY'), DataType.ARRAY)
 		self.assertIs(DataType.from_name('BOOLEAN'), DataType.BOOLEAN)
 		self.assertIs(DataType.from_name('DATETIME'), DataType.DATETIME)
+		self.assertIs(DataType.from_name('TIMEDELTA'), DataType.TIMEDELTA)
 		self.assertIs(DataType.from_name('FLOAT'), DataType.FLOAT)
 		self.assertIs(DataType.from_name('MAPPING'), DataType.MAPPING)
 		self.assertIs(DataType.from_name('NULL'), DataType.NULL)
@@ -86,6 +87,7 @@ class DataTypeTests(unittest.TestCase):
 		self.assertIs(DataType.from_type(bool), DataType.BOOLEAN)
 		self.assertIs(DataType.from_type(datetime.date), DataType.DATETIME)
 		self.assertIs(DataType.from_type(datetime.datetime), DataType.DATETIME)
+		self.assertIs(DataType.from_type(datetime.timedelta), DataType.TIMEDELTA)
 		self.assertIs(DataType.from_type(float), DataType.FLOAT)
 		self.assertIs(DataType.from_type(int), DataType.FLOAT)
 		self.assertIs(DataType.from_type(dict), DataType.MAPPING)
@@ -138,6 +140,7 @@ class DataTypeTests(unittest.TestCase):
 		self.assertEqual(DataType.from_value(False), DataType.BOOLEAN)
 		self.assertEqual(DataType.from_value(datetime.date.today()), DataType.DATETIME)
 		self.assertEqual(DataType.from_value(datetime.datetime.now()), DataType.DATETIME)
+		self.assertEqual(DataType.from_value(datetime.timedelta()), DataType.TIMEDELTA)
 		self.assertEqual(DataType.from_value(0), DataType.FLOAT)
 		self.assertEqual(DataType.from_value(0.0), DataType.FLOAT)
 		self.assertEqual(DataType.from_value(None), DataType.NULL)
@@ -148,7 +151,7 @@ class DataTypeTests(unittest.TestCase):
 			DataType.from_value(self._UnsupportedType())
 
 	def test_data_type_definitions_describe_themselves(self):
-		for name in ('ARRAY', 'BOOLEAN', 'DATETIME', 'FLOAT', 'MAPPING', 'NULL', 'SET', 'STRING', 'UNDEFINED'):
+		for name in ('ARRAY', 'BOOLEAN', 'DATETIME', 'TIMEDELTA', 'FLOAT', 'MAPPING', 'NULL', 'SET', 'STRING', 'UNDEFINED'):
 			data_type = getattr(DataType, name)
 			self.assertRegex(repr(data_type), 'name=' + name)