Fixed #28103 -- Added quarter extract, truncation, and lookup.

Thanks Mariusz Felisiak, Tim Graham, and Adam Johnson for review.
This commit is contained in:
Mads Jensen 2017-06-08 21:15:29 +02:00 committed by Tim Graham
parent f6bd00131e
commit c7f6ffbdcf
9 changed files with 202 additions and 17 deletions

View File

@ -39,6 +39,10 @@ class DatabaseOperations(BaseDatabaseOperations):
if lookup_type in fields:
format_str = fields[lookup_type]
return "CAST(DATE_FORMAT(%s, '%s') AS DATE)" % (field_name, format_str)
elif lookup_type == 'quarter':
return "MAKEDATE(YEAR(%s), 1) + INTERVAL QUARTER(%s) QUARTER - INTERVAL 1 QUARTER" % (
field_name, field_name
)
else:
return "DATE(%s)" % (field_name)
@ -64,6 +68,12 @@ class DatabaseOperations(BaseDatabaseOperations):
fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
if lookup_type == 'quarter':
return (
"CAST(DATE_FORMAT(MAKEDATE(YEAR({field_name}), 1) + "
"INTERVAL QUARTER({field_name}) QUARTER - " +
"INTERVAL 1 QUARTER, '%%Y-%%m-01 00:00:00') AS DATETIME)"
).format(field_name=field_name)
try:
i = fields.index(lookup_type) + 1
except ValueError:

View File

@ -67,6 +67,8 @@ END;
elif lookup_type == 'week':
# IW = ISO week number
return "TO_CHAR(%s, 'IW')" % field_name
elif lookup_type == 'quarter':
return "TO_CHAR(%s, 'Q')" % field_name
else:
# https://docs.oracle.com/database/121/SQLRF/functions067.htm#SQLRF00639
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
@ -81,6 +83,8 @@ END;
# https://docs.oracle.com/database/121/SQLRF/functions271.htm#SQLRF52058
if lookup_type in ('year', 'month'):
return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
elif lookup_type == 'quarter':
return "TRUNC(%s, 'Q')" % field_name
else:
return "TRUNC(%s)" % field_name
@ -117,6 +121,8 @@ END;
# https://docs.oracle.com/database/121/SQLRF/functions271.htm#SQLRF52058
if lookup_type in ('year', 'month'):
sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
elif lookup_type == 'quarter':
sql = "TRUNC(%s, 'Q')" % field_name
elif lookup_type == 'day':
sql = "TRUNC(%s)" % field_name
elif lookup_type == 'hour':

View File

@ -2,6 +2,7 @@
SQLite3 backend for the sqlite3 module in the standard library.
"""
import decimal
import math
import re
import warnings
from sqlite3 import dbapi2 as Database
@ -309,6 +310,8 @@ def _sqlite_date_extract(lookup_type, dt):
return (dt.isoweekday() % 7) + 1
elif lookup_type == 'week':
return dt.isocalendar()[1]
elif lookup_type == 'quarter':
return math.ceil(dt.month / 3)
else:
return getattr(dt, lookup_type)
@ -320,6 +323,9 @@ def _sqlite_date_trunc(lookup_type, dt):
return None
if lookup_type == 'year':
return "%i-01-01" % dt.year
elif lookup_type == 'quarter':
month_in_quarter = dt.month - (dt.month - 1) % 3
return '%i-%02i-01' % (dt.year, month_in_quarter)
elif lookup_type == 'month':
return "%i-%02i-01" % (dt.year, dt.month)
elif lookup_type == 'day':
@ -373,6 +379,8 @@ def _sqlite_datetime_extract(lookup_type, dt, tzname):
return (dt.isoweekday() % 7) + 1
elif lookup_type == 'week':
return dt.isocalendar()[1]
elif lookup_type == 'quarter':
return math.ceil(dt.month / 3)
else:
return getattr(dt, lookup_type)
@ -383,6 +391,9 @@ def _sqlite_datetime_trunc(lookup_type, dt, tzname):
return None
if lookup_type == 'year':
return "%i-01-01 00:00:00" % dt.year
elif lookup_type == 'quarter':
month_in_quarter = dt.month - (dt.month - 1) % 3
return '%i-%02i-01 00:00:00' % (dt.year, month_in_quarter)
elif lookup_type == 'month':
return "%i-%02i-01 00:00:00" % (dt.year, dt.month)
elif lookup_type == 'day':

View File

@ -4,9 +4,9 @@ from .base import (
)
from .datetime import (
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
TruncDay, TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime,
TruncYear,
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
TruncQuarter, TruncSecond, TruncTime, TruncYear,
)
__all__ = [
@ -15,7 +15,7 @@ __all__ = [
'Lower', 'Now', 'StrIndex', 'Substr', 'Upper',
# datetime
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractYear',
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
'TruncSecond', 'TruncTime', 'TruncYear',
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay',
'ExtractYear', 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute',
'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncYear',
]

View File

@ -101,6 +101,10 @@ class ExtractWeekDay(Extract):
lookup_name = 'week_day'
class ExtractQuarter(Extract):
lookup_name = 'quarter'
class ExtractHour(Extract):
lookup_name = 'hour'
@ -118,6 +122,7 @@ DateField.register_lookup(ExtractMonth)
DateField.register_lookup(ExtractDay)
DateField.register_lookup(ExtractWeekDay)
DateField.register_lookup(ExtractWeek)
DateField.register_lookup(ExtractQuarter)
TimeField.register_lookup(ExtractHour)
TimeField.register_lookup(ExtractMinute)
@ -179,7 +184,7 @@ class TruncBase(TimezoneMixin, Transform):
field.name, output_field.__class__.__name__ if explicit_output_field else 'DateTimeField'
))
elif isinstance(field, TimeField) and (
isinstance(output_field, DateTimeField) or copy.kind in ('year', 'month', 'day', 'date')):
isinstance(output_field, DateTimeField) or copy.kind in ('year', 'quarter', 'month', 'day', 'date')):
raise ValueError("Cannot truncate TimeField '%s' to %s. " % (
field.name, output_field.__class__.__name__ if explicit_output_field else 'DateTimeField'
))
@ -214,6 +219,10 @@ class TruncYear(TruncBase):
kind = 'year'
class TruncQuarter(TruncBase):
kind = 'quarter'
class TruncMonth(TruncBase):
kind = 'month'

View File

@ -342,6 +342,7 @@ Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
``lookup_name``\s return:
* "year": 2015
* "quarter": 2
* "month": 6
* "day": 15
* "week": 25
@ -428,6 +429,12 @@ Usage example::
.. attribute:: lookup_name = 'week'
.. class:: ExtractQuarter(expression, tzinfo=None, **extra)
.. versionadded:: 2.0
.. attribute:: lookup_name = 'quarter'
These are logically equivalent to ``Extract('date_field', lookup_name)``. Each
class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
as ``__(lookup_name)``, e.g. ``__year``.
@ -438,7 +445,8 @@ that deal with date-parts can be used with ``DateField``::
>>> from datetime import datetime
>>> from django.utils import timezone
>>> from django.db.models.functions import (
... ExtractDay, ExtractMonth, ExtractWeek, ExtractWeekDay, ExtractYear,
... ExtractDay, ExtractMonth, ExtractQuarter, ExtractWeek,
... ExtractWeekDay, ExtractYear,
... )
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
@ -447,14 +455,15 @@ that deal with date-parts can be used with ``DateField``::
... end_datetime=end_2015, end_date=end_2015.date())
>>> Experiment.objects.annotate(
... year=ExtractYear('start_date'),
... quarter=ExtractQuarter('start_date'),
... month=ExtractMonth('start_date'),
... week=ExtractWeek('start_date'),
... day=ExtractDay('start_date'),
... weekday=ExtractWeekDay('start_date'),
... ).values('year', 'month', 'week', 'day', 'weekday').get(
... ).values('year', 'quarter', 'month', 'week', 'day', 'weekday').get(
... end_date__year=ExtractYear('start_date'),
... )
{'year': 2015, 'month': 6, 'week': 25, 'day': 15, 'weekday': 2}
{'year': 2015, 'quarter': 2, 'month': 6, 'week': 25, 'day': 15, 'weekday': 2}
``DateTimeField`` extracts
~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -483,8 +492,9 @@ Each class is also a ``Transform`` registered on ``DateTimeField`` as
>>> from datetime import datetime
>>> from django.utils import timezone
>>> from django.db.models.functions import (
... ExtractDay, ExtractHour, ExtractMinute, ExtractMonth, ExtractSecond,
... ExtractWeek, ExtractWeekDay, ExtractYear,
... ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
... ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay,
... ExtractYear,
... )
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
@ -493,6 +503,7 @@ Each class is also a ``Transform`` registered on ``DateTimeField`` as
... end_datetime=end_2015, end_date=end_2015.date())
>>> Experiment.objects.annotate(
... year=ExtractYear('start_datetime'),
... quarter=ExtractQuarter('start_datetime'),
... month=ExtractMonth('start_datetime'),
... week=ExtractWeek('start_datetime'),
... day=ExtractDay('start_datetime'),
@ -503,8 +514,8 @@ Each class is also a ``Transform`` registered on ``DateTimeField`` as
... ).values(
... 'year', 'month', 'week', 'day', 'weekday', 'hour', 'minute', 'second',
... ).get(end_datetime__year=ExtractYear('start_datetime'))
{'year': 2015, 'month': 6, 'week': 25, 'day': 15, 'weekday': 2, 'hour': 23,
'minute': 30, 'second': 1}
{'year': 2015, 'quarter': 2, 'month': 6, 'week': 25, 'day': 15, 'weekday': 2,
'hour': 23, 'minute': 30, 'second': 1}
When :setting:`USE_TZ` is ``True`` then datetimes are stored in the database
in UTC. If a different timezone is active in Django, the datetime is converted
@ -564,6 +575,7 @@ Given the datetime ``2015-06-15 14:30:50.000321+00:00``, the built-in ``kind``\s
return:
* "year": 2015-01-01 00:00:00+00:00
* "quarter": 2015-04-01 00:00:00+00:00
* "month": 2015-06-01 00:00:00+00:00
* "day": 2015-06-15 00:00:00+00:00
* "hour": 2015-06-15 14:00:00+00:00
@ -576,6 +588,7 @@ The timezone offset for Melbourne in the example date above is +10:00. The
values returned when this timezone is active will be:
* "year": 2015-01-01 00:00:00+11:00
* "quarter": 2015-04-01 00:00:00+10:00
* "month": 2015-06-01 00:00:00+10:00
* "day": 2015-06-16 00:00:00+10:00
* "hour": 2015-06-16 00:00:00+10:00
@ -629,6 +642,12 @@ Usage example::
.. attribute:: kind = 'month'
.. class:: TruncQuarter(expression, output_field=None, tzinfo=None, **extra)
.. versionadded:: 2.0
.. attribute:: kind = 'quarter'
These are logically equivalent to ``Trunc('date_field', kind)``. They truncate
all parts of the date up to ``kind`` which allows grouping or filtering dates
with less precision. ``expression`` can have an ``output_field`` of either

View File

@ -2830,6 +2830,28 @@ When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
current time zone before filtering. This requires :ref:`time zone definitions
in the database <database-time-zone-definitions>`.
.. fieldlookup:: quarter
``quarter``
~~~~~~~~~~~
.. versionadded:: 2.0
For date and datetime fields, a 'quarter of the year' match. Allows chaining
additional field lookups. Takes an integer value between 1 and 4 representing
the quarter of the year.
Example to retrieve entries in the second quarter (April 1 to June 30)::
Entry.objects.filter(pub_date__quarter=2)
(No equivalent SQL code fragment is included for this lookup because
implementation of the relevant query varies among different database engines.)
When :setting:`USE_TZ` is ``True``, datetime fields are converted to the
current time zone before filtering. This requires :ref:`time zone definitions
in the database <database-time-zone-definitions>`.
.. fieldlookup:: time
``time``

View File

@ -227,6 +227,15 @@ Models
from the database. For databases that don't support server-side cursors, it
controls the number of results Django fetches from the database adapter.
* Added the :class:`~django.db.models.functions.datetime.ExtractQuarter`
function to extract the quarter from :class:`~django.db.models.DateField` and
:class:`~django.db.models.DateTimeField`, and exposed it through the
:lookup:`quarter` lookup.
* Added the :class:`~django.db.models.functions.datetime.TruncQuarter`
function to truncate :class:`~django.db.models.DateField` and
:class:`~django.db.models.DateTimeField` to the first day of a quarter.
Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

View File

@ -7,9 +7,9 @@ from django.db import connection
from django.db.models import DateField, DateTimeField, IntegerField, TimeField
from django.db.models.functions import (
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
TruncDay, TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime,
TruncYear,
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
TruncQuarter, TruncSecond, TruncTime, TruncYear,
)
from django.test import TestCase, override_settings
from django.utils import timezone
@ -41,6 +41,11 @@ def truncate_to(value, kind, tzinfo=None):
if isinstance(value, datetime):
return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
return value.replace(day=1)
if kind == 'quarter':
month_in_quarter = value.month - (value.month - 1) % 3
if isinstance(value, datetime):
return value.replace(month=month_in_quarter, day=1, hour=0, minute=0, second=0, microsecond=0)
return value.replace(month=month_in_quarter, day=1)
# otherwise, truncate to year
if isinstance(value, datetime):
return value.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
@ -155,6 +160,11 @@ class DateFunctionTests(TestCase):
[(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
lambda m: (m.start_datetime, m.extracted)
)
self.assertQuerysetEqual(
DTModel.objects.annotate(extracted=Extract('start_datetime', 'quarter')).order_by('start_datetime'),
[(start_datetime, 2), (end_datetime, 2)],
lambda m: (m.start_datetime, m.extracted)
)
self.assertQuerysetEqual(
DTModel.objects.annotate(extracted=Extract('start_datetime', 'month')).order_by('start_datetime'),
[(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
@ -279,6 +289,47 @@ class DateFunctionTests(TestCase):
# both dates are from the same week.
self.assertEqual(DTModel.objects.filter(start_datetime__week=ExtractWeek('start_datetime')).count(), 2)
def test_extract_quarter_func(self):
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
end_datetime = microsecond_support(datetime(2016, 8, 15, 14, 10, 50, 123))
if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
self.create_model(start_datetime, end_datetime)
self.create_model(end_datetime, start_datetime)
self.assertQuerysetEqual(
DTModel.objects.annotate(extracted=ExtractQuarter('start_datetime')).order_by('start_datetime'),
[(start_datetime, 2), (end_datetime, 3)],
lambda m: (m.start_datetime, m.extracted)
)
self.assertQuerysetEqual(
DTModel.objects.annotate(extracted=ExtractQuarter('start_date')).order_by('start_datetime'),
[(start_datetime, 2), (end_datetime, 3)],
lambda m: (m.start_datetime, m.extracted)
)
self.assertEqual(DTModel.objects.filter(start_datetime__quarter=ExtractQuarter('start_datetime')).count(), 2)
def test_extract_quarter_func_boundaries(self):
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
if settings.USE_TZ:
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
last_quarter_2014 = microsecond_support(datetime(2014, 12, 31, 13, 0))
first_quarter_2015 = microsecond_support(datetime(2015, 1, 1, 13, 0))
if settings.USE_TZ:
last_quarter_2014 = timezone.make_aware(last_quarter_2014, is_dst=False)
first_quarter_2015 = timezone.make_aware(first_quarter_2015, is_dst=False)
dates = [last_quarter_2014, first_quarter_2015]
self.create_model(last_quarter_2014, end_datetime)
self.create_model(first_quarter_2015, end_datetime)
qs = DTModel.objects.filter(start_datetime__in=dates).annotate(
extracted=ExtractQuarter('start_datetime'),
).order_by('start_datetime')
self.assertQuerysetEqual(qs, [
(last_quarter_2014, 4),
(first_quarter_2015, 1),
], lambda m: (m.start_datetime, m.extracted))
def test_extract_week_func_boundaries(self):
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
if settings.USE_TZ:
@ -456,12 +507,14 @@ class DateFunctionTests(TestCase):
)
test_date_kind('year')
test_date_kind('quarter')
test_date_kind('month')
test_date_kind('day')
test_time_kind('hour')
test_time_kind('minute')
test_time_kind('second')
test_datetime_kind('year')
test_datetime_kind('quarter')
test_datetime_kind('month')
test_datetime_kind('day')
test_datetime_kind('hour')
@ -503,6 +556,47 @@ class DateFunctionTests(TestCase):
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
list(DTModel.objects.annotate(truncated=TruncYear('start_time', output_field=TimeField())))
def test_trunc_quarter_func(self):
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
end_datetime = truncate_to(microsecond_support(datetime(2016, 10, 15, 14, 10, 50, 123)), 'quarter')
last_quarter_2015 = truncate_to(microsecond_support(datetime(2015, 12, 31, 14, 10, 50, 123)), 'quarter')
first_quarter_2016 = truncate_to(microsecond_support(datetime(2016, 1, 1, 14, 10, 50, 123)), 'quarter')
if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False)
end_datetime = timezone.make_aware(end_datetime, is_dst=False)
last_quarter_2015 = timezone.make_aware(last_quarter_2015, is_dst=False)
first_quarter_2016 = timezone.make_aware(first_quarter_2016, is_dst=False)
self.create_model(start_datetime=start_datetime, end_datetime=end_datetime)
self.create_model(start_datetime=end_datetime, end_datetime=start_datetime)
self.create_model(start_datetime=last_quarter_2015, end_datetime=end_datetime)
self.create_model(start_datetime=first_quarter_2016, end_datetime=end_datetime)
self.assertQuerysetEqual(
DTModel.objects.annotate(extracted=TruncQuarter('start_date')).order_by('start_datetime'),
[
(start_datetime, truncate_to(start_datetime.date(), 'quarter')),
(last_quarter_2015, truncate_to(last_quarter_2015.date(), 'quarter')),
(first_quarter_2016, truncate_to(first_quarter_2016.date(), 'quarter')),
(end_datetime, truncate_to(end_datetime.date(), 'quarter')),
],
lambda m: (m.start_datetime, m.extracted)
)
self.assertQuerysetEqual(
DTModel.objects.annotate(extracted=TruncQuarter('start_datetime')).order_by('start_datetime'),
[
(start_datetime, truncate_to(start_datetime, 'quarter')),
(last_quarter_2015, truncate_to(last_quarter_2015, 'quarter')),
(first_quarter_2016, truncate_to(first_quarter_2016, 'quarter')),
(end_datetime, truncate_to(end_datetime, 'quarter')),
],
lambda m: (m.start_datetime, m.extracted)
)
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
list(DTModel.objects.annotate(truncated=TruncQuarter('start_time')))
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
list(DTModel.objects.annotate(truncated=TruncQuarter('start_time', output_field=TimeField())))
def test_trunc_month_func(self):
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'month')
@ -723,6 +817,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
week=Extract('start_datetime', 'week', tzinfo=melb),
weekday=ExtractWeekDay('start_datetime'),
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
quarter=ExtractQuarter('start_datetime', tzinfo=melb),
hour=ExtractHour('start_datetime'),
hour_melb=ExtractHour('start_datetime', tzinfo=melb),
).order_by('start_datetime')
@ -733,6 +828,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.assertEqual(utc_model.week, 25)
self.assertEqual(utc_model.weekday, 2)
self.assertEqual(utc_model.weekday_melb, 3)
self.assertEqual(utc_model.quarter, 2)
self.assertEqual(utc_model.hour, 23)
self.assertEqual(utc_model.hour_melb, 9)
@ -743,6 +839,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.assertEqual(melb_model.day_melb, 16)
self.assertEqual(melb_model.week, 25)
self.assertEqual(melb_model.weekday, 3)
self.assertEqual(melb_model.quarter, 2)
self.assertEqual(melb_model.weekday_melb, 3)
self.assertEqual(melb_model.hour, 9)
self.assertEqual(melb_model.hour_melb, 9)
@ -836,12 +933,14 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
)
test_date_kind('year')
test_date_kind('quarter')
test_date_kind('month')
test_date_kind('day')
test_time_kind('hour')
test_time_kind('minute')
test_time_kind('second')
test_datetime_kind('year')
test_datetime_kind('quarter')
test_datetime_kind('month')
test_datetime_kind('day')
test_datetime_kind('hour')