From 50c6cbd2e8ad0992195535c89cab9d1d98b72239 Mon Sep 17 00:00:00 2001 From: Stan Lagun Date: Mon, 7 Sep 2015 03:07:09 +0300 Subject: [PATCH] DateTime and TimeSpan functions This commit introduces set of functions/methods/properties to work with date-times and time intervals. Also adds Integer and DateTime helper smart-types Change-Id: I3b0571ff522a7c6624afab0d3c008830098b3080 --- requirements.txt | 2 +- yaql/__init__.py | 6 +- yaql/language/specs.py | 4 +- yaql/language/yaqltypes.py | 22 ++ yaql/standard_library/date_time.py | 431 +++++++++++++++++++++++++++++ yaql/tests/test_datetime.py | 161 +++++++++++ 6 files changed, 622 insertions(+), 4 deletions(-) create mode 100644 yaql/standard_library/date_time.py create mode 100644 yaql/tests/test_datetime.py diff --git a/requirements.txt b/requirements.txt index b74f53b..276b77b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pbr>=0.11,<2.0 Babel>=1.3 - +python-dateutil>=2.4.2 ply<=3.6 six>=1.9.0 diff --git a/yaql/__init__.py b/yaql/__init__.py index 7483298..68b6a6e 100644 --- a/yaql/__init__.py +++ b/yaql/__init__.py @@ -25,6 +25,7 @@ from yaql.standard_library import boolean as std_boolean from yaql.standard_library import branching as std_branching from yaql.standard_library import collections as std_collections from yaql.standard_library import common as std_common +from yaql.standard_library import date_time as std_datetime from yaql.standard_library import math as std_math from yaql.standard_library import queries as std_queries from yaql.standard_library import regex as std_regex @@ -68,7 +69,7 @@ def create_context(data=utils.NO_VALUE, context=None, system=True, math=True, collections=True, queries=True, regex=True, branching=True, no_sets=False, finalizer=None, delegates=False, - convention=None): + convention=None, datetime=True): context = _setup_context(data, context, finalizer, convention) if system: @@ -91,6 +92,9 @@ def create_context(data=utils.NO_VALUE, context=None, system=True, std_regex.register(context) if branching: std_branching.register(context) + if datetime: + std_datetime.register(context) + return context YaqlFactory = factory.YaqlFactory diff --git a/yaql/language/specs.py b/yaql/language/specs.py index dafeace..bef1b88 100644 --- a/yaql/language/specs.py +++ b/yaql/language/specs.py @@ -495,10 +495,10 @@ def meta(name, value): return wrapper -def yaql_property(python_type): +def yaql_property(source_type): def decorator(func): @name('#property#{0}'.format(get_function_definition(func).name)) - @parameter('obj', yaqltypes.PythonType(python_type, False)) + @parameter('obj', source_type) def wrapper(obj): return func(obj) return wrapper diff --git a/yaql/language/yaqltypes.py b/yaql/language/yaqltypes.py index a045fcd..1b2a3f3 100644 --- a/yaql/language/yaqltypes.py +++ b/yaql/language/yaqltypes.py @@ -13,7 +13,9 @@ # under the License. import collections +import datetime +from dateutil import tz import six from yaql.language import exceptions @@ -138,6 +140,26 @@ class String(PythonType): return None if value is None else six.text_type(value) +class Integer(PythonType): + def __init__(self, nullable=False): + super(Integer, self).__init__(six.integer_types, nullable=nullable) + + +class DateTime(PythonType): + utctz = tz.tzutc() + + def __init__(self, nullable=False): + super(DateTime, self).__init__(datetime.datetime, nullable=nullable) + + def convert(self, value, *args, **kwargs): + if isinstance(value, datetime.datetime): + if value.tzinfo is None: + return value.replace(tzinfo=self.utctz) + else: + return value + return super(DateTime, self).convert(value, *args, **kwargs) + + class Iterable(PythonType): def __init__(self, validators=None): super(Iterable, self).__init__( diff --git a/yaql/standard_library/date_time.py b/yaql/standard_library/date_time.py new file mode 100644 index 0000000..5f6e221 --- /dev/null +++ b/yaql/standard_library/date_time.py @@ -0,0 +1,431 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import time as python_time + +from yaql.language import specs +from yaql.language import yaqltypes + +from dateutil import parser +from dateutil import tz + + +DATETIME_TYPE = datetime.datetime +TIMESPAN_TYPE = datetime.timedelta +ZERO_TIMESPAN = datetime.timedelta() +UTCTZ = yaqltypes.DateTime.utctz + + +def _get_tz(offset): + if offset is None: + return None + if offset == ZERO_TIMESPAN: + return UTCTZ + return tz.tzoffset(None, seconds(offset)) + + +@specs.name('datetime') +@specs.parameter('year', int) +@specs.parameter('month', int) +@specs.parameter('day', int) +@specs.parameter('hour', int) +@specs.parameter('minute', int) +@specs.parameter('second', int) +@specs.parameter('microsecond', int) +@specs.parameter('offset', TIMESPAN_TYPE) +def build_datetime(year, month, day, hour=0, minute=0, second=0, + microsecond=0, offset=ZERO_TIMESPAN): + zone = _get_tz(offset) + return DATETIME_TYPE(year, month, day, hour, minute, second, + microsecond, zone) + + +@specs.name('datetime') +@specs.parameter('timestamp', yaqltypes.Number()) +@specs.parameter('offset', TIMESPAN_TYPE) +def datetime_from_timestamp(timestamp, offset=ZERO_TIMESPAN): + zone = _get_tz(offset) + return datetime.datetime.fromtimestamp(timestamp, tz=zone) + + +@specs.name('datetime') +@specs.parameter('string', yaqltypes.String()) +@specs.parameter('format__', yaqltypes.String(True)) +def datetime_from_string(string, format__=None): + if not format__: + result = parser.parse(string) + else: + result = DATETIME_TYPE.strptime(string, format__) + if not result.tzinfo: + return result.replace(tzinfo=UTCTZ) + return result + + +@specs.name('timespan') +@specs.parameter('days', int) +@specs.parameter('hours', int) +@specs.parameter('minutes', int) +@specs.parameter('seconds', yaqltypes.Integer()) +@specs.parameter('milliseconds', yaqltypes.Integer()) +@specs.parameter('microseconds', yaqltypes.Integer()) +def build_timespan(days=0, hours=0, minutes=0, seconds=0, + milliseconds=0, microseconds=0): + return TIMESPAN_TYPE( + days=days, hours=hours, minutes=minutes, seconds=seconds, + milliseconds=milliseconds, microseconds=microseconds) + + +@specs.yaql_property(TIMESPAN_TYPE) +def microseconds(timespan): + return (86400000000 * timespan.days + + 1000000 * timespan.seconds + + timespan.microseconds) + + +@specs.yaql_property(TIMESPAN_TYPE) +def milliseconds(timespan): + return microseconds(timespan) / 1000.0 + + +@specs.yaql_property(TIMESPAN_TYPE) +def seconds(timespan): + return microseconds(timespan) / 1000000.0 + + +@specs.yaql_property(TIMESPAN_TYPE) +def minutes(timespan): + return microseconds(timespan) / 60000000.0 + + +@specs.yaql_property(TIMESPAN_TYPE) +def hours(timespan): + return microseconds(timespan) / 3600000000.0 + + +@specs.yaql_property(TIMESPAN_TYPE) +def days(timespan): + return microseconds(timespan) / 86400000000.0 + + +def now(offset=ZERO_TIMESPAN): + zone = _get_tz(offset) + return DATETIME_TYPE.now(tz=zone) + + +def localtz(): + if python_time.daylight: + return TIMESPAN_TYPE(seconds=-python_time.altzone) + else: + return TIMESPAN_TYPE(seconds=-python_time.timezone) + + +def utctz(): + return ZERO_TIMESPAN + + +@specs.name('#operator_+') +@specs.parameter('dt', yaqltypes.DateTime()) +@specs.parameter('ts', TIMESPAN_TYPE) +def datetime_plus_timespan(dt, ts): + return dt + ts + + +@specs.name('#operator_+') +@specs.parameter('ts', TIMESPAN_TYPE) +@specs.parameter('dt', yaqltypes.DateTime()) +def timespan_plus_datetime(ts, dt): + return ts + dt + + +@specs.name('#operator_-') +@specs.parameter('dt', yaqltypes.DateTime()) +@specs.parameter('ts', TIMESPAN_TYPE) +def datetime_minus_timespan(dt, ts): + return dt - ts + + +@specs.name('#operator_-') +@specs.parameter('dt1', yaqltypes.DateTime()) +@specs.parameter('dt2', yaqltypes.DateTime()) +def datetime_minus_datetime(dt1, dt2): + return dt1 - dt2 + + +@specs.name('#operator_+') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_plus_timespan(ts1, ts2): + return ts1 + ts2 + + +@specs.name('#operator_-') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_minus_timespan(ts1, ts2): + return ts1 - ts2 + + +@specs.name('#operator_>') +@specs.parameter('dt1', yaqltypes.DateTime()) +@specs.parameter('dt2', yaqltypes.DateTime()) +def datetime_gt_datetime(dt1, dt2): + return dt1 > dt2 + + +@specs.name('#operator_>=') +@specs.parameter('dt1', yaqltypes.DateTime()) +@specs.parameter('dt2', yaqltypes.DateTime()) +def datetime_gte_datetime(dt1, dt2): + return dt1 >= dt2 + + +@specs.name('#operator_<') +@specs.parameter('dt1', yaqltypes.DateTime()) +@specs.parameter('dt2', yaqltypes.DateTime()) +def datetime_lt_datetime(dt1, dt2): + return dt1 < dt2 + + +@specs.name('#operator_<=') +@specs.parameter('dt1', yaqltypes.DateTime()) +@specs.parameter('dt2', yaqltypes.DateTime()) +def datetime_lte_datetime(dt1, dt2): + return dt1 <= dt2 + + +@specs.name('*equal') +@specs.parameter('dt1', yaqltypes.DateTime()) +@specs.parameter('dt2', yaqltypes.DateTime()) +def datetime_eq_datetime(dt1, dt2): + return dt1 == dt2 + + +@specs.name('*not_equal') +@specs.parameter('dt1', yaqltypes.DateTime()) +@specs.parameter('dt2', yaqltypes.DateTime()) +def datetime_neq_datetime(dt1, dt2): + return dt1 != dt2 + + +@specs.name('#operator_>') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_gt_timespan(ts1, ts2): + return ts1 > ts2 + + +@specs.name('#operator_>=') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_gte_timespan(ts1, ts2): + return ts1 >= ts2 + + +@specs.name('#operator_<') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_lt_timespan(ts1, ts2): + return ts1 < ts2 + + +@specs.name('#operator_<=') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_lte_timespan(ts1, ts2): + return ts1 <= ts2 + + +@specs.name('*equal') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_eq_timespan(ts1, ts2): + return ts1 == ts2 + + +@specs.name('*not_equal') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def timespan_neq_timespan(ts1, ts2): + return ts1 != ts2 + + +@specs.name('#operator_*') +@specs.parameter('ts', TIMESPAN_TYPE) +@specs.parameter('n', yaqltypes.Number()) +def timespan_by_num(ts, n): + return TIMESPAN_TYPE(microseconds=(microseconds(ts) * n)) + + +@specs.name('#operator_*') +@specs.parameter('n', yaqltypes.Number()) +@specs.parameter('ts', TIMESPAN_TYPE) +def num_by_timespan(n, ts): + return TIMESPAN_TYPE(microseconds=(microseconds(ts) * n)) + + +@specs.name('#operator_/') +@specs.parameter('ts1', TIMESPAN_TYPE) +@specs.parameter('ts2', TIMESPAN_TYPE) +def div_timespans(ts1, ts2): + return (0.0 + microseconds(ts1)) / microseconds(ts2) + + +@specs.name('#operator_/') +@specs.parameter('ts', TIMESPAN_TYPE) +@specs.parameter('n', yaqltypes.Number()) +def div_timespan_by_num(ts, n): + return TIMESPAN_TYPE(microseconds=(microseconds(ts) / n)) + + +@specs.name('#unary_operator_-') +@specs.parameter('ts', TIMESPAN_TYPE) +def negative_timespan(ts): + return -ts + + +@specs.name('#unary_operator_+') +@specs.parameter('ts', TIMESPAN_TYPE) +def positive_timespan(ts): + return ts + + +@specs.yaql_property(DATETIME_TYPE) +def year(dt): + return dt.year + + +@specs.yaql_property(DATETIME_TYPE) +def month(dt): + return dt.month + + +@specs.yaql_property(DATETIME_TYPE) +def day(dt): + return dt.day + + +@specs.yaql_property(DATETIME_TYPE) +def hour(dt): + return dt.hour + + +@specs.yaql_property(DATETIME_TYPE) +def minute(dt): + return dt.minute + + +@specs.yaql_property(DATETIME_TYPE) +def second(dt): + return dt.second + + +@specs.yaql_property(DATETIME_TYPE) +def microsecond(dt): + return dt.microsecond + + +@specs.yaql_property(yaqltypes.DateTime()) +def date(dt): + return DATETIME_TYPE( + year=dt.year, month=dt.month, day=dt.day, tzinfo=dt.tzinfo) + + +@specs.yaql_property(yaqltypes.DateTime()) +def time(dt): + return dt - date(dt) + + +@specs.yaql_property(DATETIME_TYPE) +def weekday(dt): + return dt.weekday() + + +@specs.yaql_property(yaqltypes.DateTime()) +def utc(dt): + return dt - dt.utcoffset() + + +@specs.yaql_property(DATETIME_TYPE) +def offset(dt): + return dt.utcoffset() or ZERO_TIMESPAN + + +@specs.yaql_property(DATETIME_TYPE) +def timestamp(dt): + return (utc(dt) - DATETIME_TYPE(1970, 1, 1, tzinfo=UTCTZ)).total_seconds() + + +@specs.method +@specs.parameter('dt', yaqltypes.DateTime()) +@specs.parameter('year', int) +@specs.parameter('month', int) +@specs.parameter('day', int) +@specs.parameter('hour', int) +@specs.parameter('minute', int) +@specs.parameter('second', int) +@specs.parameter('microsecond', int) +@specs.parameter('offset', TIMESPAN_TYPE) +def replace(dt, year=None, month=None, day=None, hour=None, minute=None, + second=None, microsecond=None, offset=None): + replacements = {} + if year is not None: + replacements['year'] = year + if month is not None: + replacements['month'] = month + if day is not None: + replacements['day'] = day + if hour is not None: + replacements['hour'] = hour + if minute is not None: + replacements['minute'] = minute + if second is not None: + replacements['second'] = second + if microsecond is not None: + replacements['microsecond'] = microsecond + if offset is not None: + replacements['tzinfo'] = _get_tz(offset) + + return dt.replace(**replacements) + + +@specs.method +@specs.parameter('dt', yaqltypes.DateTime()) +@specs.parameter('format__', yaqltypes.String()) +def format_(dt, format__): + return dt.strftime(format__) + + +def register(context): + functions = ( + build_datetime, build_timespan, datetime_from_timestamp, + datetime_from_string, now, localtz, utctz, utc, + days, hours, minutes, seconds, milliseconds, microseconds, + datetime_plus_timespan, timespan_plus_datetime, + datetime_minus_timespan, datetime_minus_datetime, + timespan_plus_timespan, timespan_minus_timespan, + datetime_gt_datetime, datetime_gte_datetime, + datetime_lt_datetime, datetime_lte_datetime, + datetime_eq_datetime, datetime_neq_datetime, + timespan_gt_timespan, timespan_gte_timespan, + timespan_lt_timespan, timespan_lte_timespan, + timespan_eq_timespan, timespan_neq_timespan, + negative_timespan, positive_timespan, + timespan_by_num, num_by_timespan, div_timespans, div_timespan_by_num, + year, month, day, hour, minute, second, microsecond, weekday, + offset, timestamp, date, time, replace, format_ + ) + + for func in functions: + context.register_function(func) diff --git a/yaql/tests/test_datetime.py b/yaql/tests/test_datetime.py new file mode 100644 index 0000000..e489d2e --- /dev/null +++ b/yaql/tests/test_datetime.py @@ -0,0 +1,161 @@ +# Copyright (c) 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import time + +from dateutil import tz +from testtools import matchers + +import yaql.tests + +DT = datetime.datetime +TS = datetime.timedelta + + +class TestDatetime(yaql.tests.TestCase): + def test_build_datetime_components(self): + dt = DT(2015, 8, 29, tzinfo=tz.tzutc()) + self.assertEqual( + dt, self.eval('datetime(2015, 8, 29)')) + self.assertEqual( + dt, self.eval('datetime(year => 2015, month => 8, day => 29,' + 'hour => 0, minute => 0, second => 0, ' + 'microsecond => 0)')) + + def test_build_datetime_iso(self): + self.assertEqual( + DT(2015, 8, 29, tzinfo=tz.tzutc()), + self.eval('datetime("2015-8-29")') + ) + self.assertEqual( + DT(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tz.tzutc()), + self.eval('datetime("2008-09-03T20:56:35.450686")') + ) + self.assertEqual( + DT(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tz.tzutc()), + self.eval('datetime("2008-09-03T20:56:35.450686Z")') + ) + self.assertEqual( + DT(2008, 9, 3, 0, 0, tzinfo=tz.tzutc()), + self.eval('datetime("20080903")') + ) + dt = self.eval('datetime("2008-09-03T20:56:35.450686+03:00")') + self.assertEqual( + DT(2008, 9, 3, 20, 56, 35, 450686), + dt.replace(tzinfo=None) + ) + self.assertEqual(TS(hours=3), dt.utcoffset()) + + def test_build_datetime_string(self): + self.assertEqual( + DT(2006, 11, 21, 16, 30, tzinfo=tz.tzutc()), + self.eval('datetime("Tuesday, 21. November 2006 04:30PM", ' + '"%A, %d. %B %Y %I:%M%p")') + ) + + def test_datetime_fields(self): + dt = DT(2006, 11, 21, 16, 30, tzinfo=tz.tzutc()) + self.assertEqual(2006, self.eval('$.year', dt)) + self.assertEqual(11, self.eval('$.month', dt)) + self.assertEqual(21, self.eval('$.day', dt)) + self.assertEqual(16, self.eval('$.hour', dt)) + self.assertEqual(30, self.eval('$.minute', dt)) + self.assertEqual(0, self.eval('$.second', dt)) + self.assertEqual(0, self.eval('$.microsecond', dt)) + self.assertEqual(1164126600, self.eval('$.timestamp', dt)) + self.assertEqual(1, self.eval('$.weekday', dt)) + self.assertEqual(TS(), self.eval('$.offset', dt)) + self.assertEqual(TS(hours=16, minutes=30), self.eval('$.time', dt)) + self.assertEqual(dt.replace(hour=0, minute=0), self.eval('$.date', dt)) + self.assertEqual(dt, self.eval('$.utc', dt)) + + def test_build_timespan(self): + self.assertEqual(TS(0), self.eval('timespan()')) + self.assertEqual( + TS(1, 7384, 5006), + self.eval('timespan(days => 1, hours => 2, minutes => 3, ' + 'seconds => 4, milliseconds => 5, microseconds => 6)')) + self.assertEqual( + TS(1, 7384, 4994), + self.eval('timespan(days => 1, hours => 2, minutes => 3, ' + 'seconds =>4, milliseconds => 5, microseconds => -6)')) + + self.assertEqual( + TS(microseconds=-1000), self.eval('timespan(milliseconds => -1)')) + + def test_datetime_from_timestamp(self): + dt = DT(2006, 11, 21, 16, 30, tzinfo=tz.tzutc()) + self.assertEqual(dt, self.eval('datetime(1164126600)')) + + def test_replace(self): + dt = DT(2006, 11, 21, 16, 30, tzinfo=tz.tzutc()) + self.assertEqual( + DT(2009, 11, 21, 16, 40, tzinfo=tz.tzutc()), + self.eval('$.replace(year => 2009, minute => 40)', dt)) + + def test_timespan_fields(self): + ts = TS(1, 51945, 5000) + self.assertAlmostEqual(1.6, self.eval('$.days', ts), places=2) + self.assertAlmostEqual(38.43, self.eval('$.hours', ts), places=2) + self.assertAlmostEqual(2305.75, self.eval('$.minutes', ts), places=2) + self.assertAlmostEqual(138345, self.eval('$.seconds', ts), places=1) + self.assertEqual(138345005, self.eval('$.milliseconds', ts)) + self.assertEqual(138345005000, self.eval('$.microseconds', ts)) + + def test_now(self): + self.assertIsInstance(self.eval('now()'), DT) + self.assertIsInstance(self.eval('now(utctz())'), DT) + self.assertIsInstance(self.eval('now(localtz())'), DT) + self.assertThat( + self.eval('now(utctz()) - now()'), + matchers.LessThan(TS(seconds=1)) + ) + self.assertTrue(self.eval('now(localtz()).offset = localtz()')) + + def test_datetime_math(self): + self.context['dt1'] = self.eval('now()') + time.sleep(0.1) + self.context['dt2'] = self.eval('now()') + delta = TS(milliseconds=120) + self.assertIsInstance(self.eval('$dt2 - $dt1'), TS) + self.assertThat(self.eval('$dt2 - $dt1'), matchers.LessThan(delta)) + self.assertTrue(self.eval('($dt2 - $dt1) + $dt1 = $dt2')) + self.assertTrue(self.eval('$dt1 + ($dt2 - $dt1) = $dt2')) + self.assertThat( + self.eval('($dt2 - $dt1) * 2'), matchers.LessThan(2 * delta)) + self.assertThat( + self.eval('2.1 * ($dt2 - $dt1)'), matchers.LessThan(2 * delta)) + self.assertTrue(self.eval('-($dt1 - $dt2) = +($dt2 - $dt1)')) + self.assertTrue(self.eval('$dt2 > $dt1')) + self.assertTrue(self.eval('$dt2 >= $dt1')) + self.assertTrue(self.eval('$dt2 != $dt1')) + self.assertTrue(self.eval('$dt1 = $dt1')) + self.assertTrue(self.eval('$dt1 < $dt2')) + self.assertTrue(self.eval('$dt1 <= $dt2')) + self.assertEqual(-1, self.eval('($dt2 - $dt1) / ($dt1 - $dt2)')) + self.assertTrue(self.eval('$dt2 - ($dt2 - $dt1) = $dt1')) + self.assertEqual( + 0, self.eval('($dt2 - $dt1) - ($dt2 - $dt1)').total_seconds()) + + delta2 = self.eval('($dt2 - $dt1) / 2.1') + self.assertThat(delta2, matchers.LessThan(delta / 2)) + self.assertTrue(self.eval('$dt1 + $ < $dt2', delta2)) + self.assertTrue(self.eval('$ + $dt1 < $dt2', delta2)) + self.assertTrue(self.eval('$dt2 - $dt1 > $', delta2)) + self.assertTrue(self.eval('$dt2 - $dt1 >= $', delta2)) + self.assertTrue(self.eval('$dt2 - $dt1 != $', delta2)) + self.assertFalse(self.eval('$dt2 - $dt1 < $', delta2)) + self.assertFalse(self.eval('$dt2 - $dt1 <= $', delta2)) + self.assertTrue(self.eval('($dt2 - $dt1) + $ > $', delta2))