How to Validate Date and Time Objects

Equality Validation

You can compare date and datetime objects just as you would any other quantitative value:

1
2
3
4
5
6
from datetime import date, datetime
from datatest import validate

validate(date(2020, 12, 25), date(2020, 12, 25))

validate(datetime(2020, 12, 25, 9, 30), datetime(2020, 12, 25, 9, 30))

Compare mappings of date objects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from datetime import date
from datatest import validate

data = {
    'A': date(2020, 12, 24),
    'B': date(2020, 12, 25),
    'C': date(2020, 12, 26),
}

requirement = {
    'A': date(2020, 12, 24),
    'B': date(2020, 12, 25),
    'C': date(2020, 12, 26),
}

validate(data, requirement)

Interval Validation

We can use validate.interval() to check that date and datetime values are within a given range.

Check that dates fall within the month of December 2020:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from datetime import date
from datatest import validate

data = [
    date(2020, 12, 4),
    date(2020, 12, 11),
    date(2020, 12, 18),
    date(2020, 12, 25),
]

validate.interval(data, min=date(2020, 12, 1), max=date(2020, 12, 31))

Check that dates are one week old or newer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from datetime import date, timedelta
from datatest import validate

data = {
    'A': date(2020, 12, 24),
    'B': date(2020, 12, 25),
    'C': date(2020, 12, 26),
}

one_week_ago = date.today() - timedelta(days=7)
validate.interval(data, min=one_week_ago, msg='one week old or newer')

Failures and Acceptances

When validation fails, a Deviation is generated containing a timedelta that represents the difference between two dates or times. You can accept time differences with accepted.tolerance().

Failed Equality

1
2
3
4
from datetime import date
from datatest import validate

validate(date(2020, 12, 27), date(2020, 12, 25))
Traceback (most recent call last):
  File "example.py", line 4, in <module>
    validate(date(2020, 12, 27), date(2020, 12, 25))
datatest.ValidationError: does not satisfy `datetime.date(2020, 12, 25)` (1 difference): [
    Deviation(timedelta(days=+2), date(2020, 12, 25)),
]

Failed Interval

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from datetime import date
from datatest import validate

data = [
    date(2020, 11, 26),
    date(2020, 12, 11),
    date(2020, 12, 25),
    date(2021, 1, 4),
]

validate.interval(data, min=date(2020, 12, 1), max=date(2020, 12, 31))
Traceback (most recent call last):
  File "example.py", line 11, in <module>
    validate.interval(data, min=date(2020, 12, 1), max=date(2020, 12, 31))
datatest.ValidationError: elements `x` do not satisfy `datetime.date(2020,
12, 1) <= x <= datetime.date(2020, 12, 31)` (2 differences): [
    Deviation(timedelta(days=-5), date(2020, 12, 1)),
    Deviation(timedelta(days=+4), date(2020, 12, 31)),
]

Failed Mapping Equalities

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from datetime import datetime
from datatest import validate

remote_log = {
    'job165': datetime(2020, 11, 27, hour=3, minute=29, second=55),
    'job382': datetime(2020, 11, 27, hour=3, minute=36, second=33),
    'job592': datetime(2020, 11, 27, hour=3, minute=42, second=14),
    'job720': datetime(2020, 11, 27, hour=3, minute=50, second=7),
    'job826': datetime(2020, 11, 27, hour=3, minute=54, second=12),
}

master_log = {
    'job165': datetime(2020, 11, 27, hour=3, minute=28, second=1),
    'job382': datetime(2020, 11, 27, hour=3, minute=36, second=51),
    'job592': datetime(2020, 11, 27, hour=3, minute=40, second=18),
    'job720': datetime(2020, 11, 27, hour=3, minute=49, second=39),
    'job826': datetime(2020, 11, 27, hour=3, minute=55, second=20),
}

validate(remote_log, master_log)
Traceback (most recent call last):
  File "example.py", line 20, in <module>
    validate(remote_log, master_log)
datatest.ValidationError: does not satisfy mapping requirements (5 differences): {
    'job165': Deviation(timedelta(seconds=+114), datetime(2020, 11, 27, 3, 28, 1)),
    'job382': Deviation(timedelta(seconds=-18), datetime(2020, 11, 27, 3, 36, 51)),
    'job592': Deviation(timedelta(seconds=+116), datetime(2020, 11, 27, 3, 40, 18)),
    'job720': Deviation(timedelta(seconds=+28), datetime(2020, 11, 27, 3, 49, 39)),
    'job826': Deviation(timedelta(seconds=-68), datetime(2020, 11, 27, 3, 55, 20)),
}

Note

The Deviation’s repr (its printable representation) does some tricks with datetime objects to improve readability and help users understand their data errors. Compare the representations of the following datetime objects against their representations when included inside a Deviation:

>>> from datetime import timedelta, date
>>> from datatest import Deviation

>>> timedelta(days=2)
datetime.timedelta(days=2)

>>> date(2020, 12, 25)
datetime.date(2020, 12, 25)

>>> Deviation(timedelta(days=2), date(2020, 12, 25))
Deviation(timedelta(days=+2), date(2020, 12, 25))

And below, we see a negative-value timedelta with a particularly surprising native repr. The Deviation repr modifies this to be more readable:

>>> from datetime import timedelta, datetime
>>> from datatest import Deviation

>>> timedelta(seconds=-3)
datetime.timedelta(days=-1, seconds=86397)

>>> datetime(2020, 12, 25, 9, 30)
datetime.datetime(2020, 12, 25, 9, 30)

>>> Deviation(timedelta(seconds=-3), datetime(2020, 12, 25, 9, 30))
Deviation(timedelta(seconds=-3), datetime(2020, 12, 25, 9, 30))

While the timedelta reprs do differ, they are legitimate constructors for creating objects of equal value:

>>> from datetime import timedelta

>>> timedelta(days=+2) == timedelta(days=2)
True

>>> timedelta(seconds=-3) == timedelta(days=-1, seconds=86397)
True