Fixed #28103 -- Added quarter extract, truncation, and lookup.
Thanks Mariusz Felisiak, Tim Graham, and Adam Johnson for review.
This commit is contained in:
parent
f6bd00131e
commit
c7f6ffbdcf
|
@ -39,6 +39,10 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
if lookup_type in fields:
|
if lookup_type in fields:
|
||||||
format_str = fields[lookup_type]
|
format_str = fields[lookup_type]
|
||||||
return "CAST(DATE_FORMAT(%s, '%s') AS DATE)" % (field_name, format_str)
|
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:
|
else:
|
||||||
return "DATE(%s)" % (field_name)
|
return "DATE(%s)" % (field_name)
|
||||||
|
|
||||||
|
@ -64,6 +68,12 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
|
fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
|
||||||
format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
|
format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
|
||||||
format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
|
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:
|
try:
|
||||||
i = fields.index(lookup_type) + 1
|
i = fields.index(lookup_type) + 1
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
|
@ -67,6 +67,8 @@ END;
|
||||||
elif lookup_type == 'week':
|
elif lookup_type == 'week':
|
||||||
# IW = ISO week number
|
# IW = ISO week number
|
||||||
return "TO_CHAR(%s, 'IW')" % field_name
|
return "TO_CHAR(%s, 'IW')" % field_name
|
||||||
|
elif lookup_type == 'quarter':
|
||||||
|
return "TO_CHAR(%s, 'Q')" % field_name
|
||||||
else:
|
else:
|
||||||
# https://docs.oracle.com/database/121/SQLRF/functions067.htm#SQLRF00639
|
# https://docs.oracle.com/database/121/SQLRF/functions067.htm#SQLRF00639
|
||||||
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
|
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
|
# https://docs.oracle.com/database/121/SQLRF/functions271.htm#SQLRF52058
|
||||||
if lookup_type in ('year', 'month'):
|
if lookup_type in ('year', 'month'):
|
||||||
return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
|
return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
|
||||||
|
elif lookup_type == 'quarter':
|
||||||
|
return "TRUNC(%s, 'Q')" % field_name
|
||||||
else:
|
else:
|
||||||
return "TRUNC(%s)" % field_name
|
return "TRUNC(%s)" % field_name
|
||||||
|
|
||||||
|
@ -117,6 +121,8 @@ END;
|
||||||
# https://docs.oracle.com/database/121/SQLRF/functions271.htm#SQLRF52058
|
# https://docs.oracle.com/database/121/SQLRF/functions271.htm#SQLRF52058
|
||||||
if lookup_type in ('year', 'month'):
|
if lookup_type in ('year', 'month'):
|
||||||
sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
|
sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
|
||||||
|
elif lookup_type == 'quarter':
|
||||||
|
sql = "TRUNC(%s, 'Q')" % field_name
|
||||||
elif lookup_type == 'day':
|
elif lookup_type == 'day':
|
||||||
sql = "TRUNC(%s)" % field_name
|
sql = "TRUNC(%s)" % field_name
|
||||||
elif lookup_type == 'hour':
|
elif lookup_type == 'hour':
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
SQLite3 backend for the sqlite3 module in the standard library.
|
SQLite3 backend for the sqlite3 module in the standard library.
|
||||||
"""
|
"""
|
||||||
import decimal
|
import decimal
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from sqlite3 import dbapi2 as Database
|
from sqlite3 import dbapi2 as Database
|
||||||
|
@ -309,6 +310,8 @@ def _sqlite_date_extract(lookup_type, dt):
|
||||||
return (dt.isoweekday() % 7) + 1
|
return (dt.isoweekday() % 7) + 1
|
||||||
elif lookup_type == 'week':
|
elif lookup_type == 'week':
|
||||||
return dt.isocalendar()[1]
|
return dt.isocalendar()[1]
|
||||||
|
elif lookup_type == 'quarter':
|
||||||
|
return math.ceil(dt.month / 3)
|
||||||
else:
|
else:
|
||||||
return getattr(dt, lookup_type)
|
return getattr(dt, lookup_type)
|
||||||
|
|
||||||
|
@ -320,6 +323,9 @@ def _sqlite_date_trunc(lookup_type, dt):
|
||||||
return None
|
return None
|
||||||
if lookup_type == 'year':
|
if lookup_type == 'year':
|
||||||
return "%i-01-01" % dt.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':
|
elif lookup_type == 'month':
|
||||||
return "%i-%02i-01" % (dt.year, dt.month)
|
return "%i-%02i-01" % (dt.year, dt.month)
|
||||||
elif lookup_type == 'day':
|
elif lookup_type == 'day':
|
||||||
|
@ -373,6 +379,8 @@ def _sqlite_datetime_extract(lookup_type, dt, tzname):
|
||||||
return (dt.isoweekday() % 7) + 1
|
return (dt.isoweekday() % 7) + 1
|
||||||
elif lookup_type == 'week':
|
elif lookup_type == 'week':
|
||||||
return dt.isocalendar()[1]
|
return dt.isocalendar()[1]
|
||||||
|
elif lookup_type == 'quarter':
|
||||||
|
return math.ceil(dt.month / 3)
|
||||||
else:
|
else:
|
||||||
return getattr(dt, lookup_type)
|
return getattr(dt, lookup_type)
|
||||||
|
|
||||||
|
@ -383,6 +391,9 @@ def _sqlite_datetime_trunc(lookup_type, dt, tzname):
|
||||||
return None
|
return None
|
||||||
if lookup_type == 'year':
|
if lookup_type == 'year':
|
||||||
return "%i-01-01 00:00:00" % dt.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':
|
elif lookup_type == 'month':
|
||||||
return "%i-%02i-01 00:00:00" % (dt.year, dt.month)
|
return "%i-%02i-01 00:00:00" % (dt.year, dt.month)
|
||||||
elif lookup_type == 'day':
|
elif lookup_type == 'day':
|
||||||
|
|
|
@ -4,9 +4,9 @@ from .base import (
|
||||||
)
|
)
|
||||||
from .datetime import (
|
from .datetime import (
|
||||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||||
ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
|
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
|
||||||
TruncDay, TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime,
|
Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
|
||||||
TruncYear,
|
TruncQuarter, TruncSecond, TruncTime, TruncYear,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -15,7 +15,7 @@ __all__ = [
|
||||||
'Lower', 'Now', 'StrIndex', 'Substr', 'Upper',
|
'Lower', 'Now', 'StrIndex', 'Substr', 'Upper',
|
||||||
# datetime
|
# datetime
|
||||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||||
'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractYear',
|
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay',
|
||||||
'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
|
'ExtractYear', 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute',
|
||||||
'TruncSecond', 'TruncTime', 'TruncYear',
|
'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncYear',
|
||||||
]
|
]
|
||||||
|
|
|
@ -101,6 +101,10 @@ class ExtractWeekDay(Extract):
|
||||||
lookup_name = 'week_day'
|
lookup_name = 'week_day'
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractQuarter(Extract):
|
||||||
|
lookup_name = 'quarter'
|
||||||
|
|
||||||
|
|
||||||
class ExtractHour(Extract):
|
class ExtractHour(Extract):
|
||||||
lookup_name = 'hour'
|
lookup_name = 'hour'
|
||||||
|
|
||||||
|
@ -118,6 +122,7 @@ DateField.register_lookup(ExtractMonth)
|
||||||
DateField.register_lookup(ExtractDay)
|
DateField.register_lookup(ExtractDay)
|
||||||
DateField.register_lookup(ExtractWeekDay)
|
DateField.register_lookup(ExtractWeekDay)
|
||||||
DateField.register_lookup(ExtractWeek)
|
DateField.register_lookup(ExtractWeek)
|
||||||
|
DateField.register_lookup(ExtractQuarter)
|
||||||
|
|
||||||
TimeField.register_lookup(ExtractHour)
|
TimeField.register_lookup(ExtractHour)
|
||||||
TimeField.register_lookup(ExtractMinute)
|
TimeField.register_lookup(ExtractMinute)
|
||||||
|
@ -179,7 +184,7 @@ class TruncBase(TimezoneMixin, Transform):
|
||||||
field.name, output_field.__class__.__name__ if explicit_output_field else 'DateTimeField'
|
field.name, output_field.__class__.__name__ if explicit_output_field else 'DateTimeField'
|
||||||
))
|
))
|
||||||
elif isinstance(field, TimeField) and (
|
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. " % (
|
raise ValueError("Cannot truncate TimeField '%s' to %s. " % (
|
||||||
field.name, output_field.__class__.__name__ if explicit_output_field else 'DateTimeField'
|
field.name, output_field.__class__.__name__ if explicit_output_field else 'DateTimeField'
|
||||||
))
|
))
|
||||||
|
@ -214,6 +219,10 @@ class TruncYear(TruncBase):
|
||||||
kind = 'year'
|
kind = 'year'
|
||||||
|
|
||||||
|
|
||||||
|
class TruncQuarter(TruncBase):
|
||||||
|
kind = 'quarter'
|
||||||
|
|
||||||
|
|
||||||
class TruncMonth(TruncBase):
|
class TruncMonth(TruncBase):
|
||||||
kind = 'month'
|
kind = 'month'
|
||||||
|
|
||||||
|
|
|
@ -342,6 +342,7 @@ Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
|
||||||
``lookup_name``\s return:
|
``lookup_name``\s return:
|
||||||
|
|
||||||
* "year": 2015
|
* "year": 2015
|
||||||
|
* "quarter": 2
|
||||||
* "month": 6
|
* "month": 6
|
||||||
* "day": 15
|
* "day": 15
|
||||||
* "week": 25
|
* "week": 25
|
||||||
|
@ -428,6 +429,12 @@ Usage example::
|
||||||
|
|
||||||
.. attribute:: lookup_name = 'week'
|
.. 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
|
These are logically equivalent to ``Extract('date_field', lookup_name)``. Each
|
||||||
class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
|
class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
|
||||||
as ``__(lookup_name)``, e.g. ``__year``.
|
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 datetime import datetime
|
||||||
>>> from django.utils import timezone
|
>>> from django.utils import timezone
|
||||||
>>> from django.db.models.functions import (
|
>>> 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)
|
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
|
||||||
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, 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())
|
... end_datetime=end_2015, end_date=end_2015.date())
|
||||||
>>> Experiment.objects.annotate(
|
>>> Experiment.objects.annotate(
|
||||||
... year=ExtractYear('start_date'),
|
... year=ExtractYear('start_date'),
|
||||||
|
... quarter=ExtractQuarter('start_date'),
|
||||||
... month=ExtractMonth('start_date'),
|
... month=ExtractMonth('start_date'),
|
||||||
... week=ExtractWeek('start_date'),
|
... week=ExtractWeek('start_date'),
|
||||||
... day=ExtractDay('start_date'),
|
... day=ExtractDay('start_date'),
|
||||||
... weekday=ExtractWeekDay('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'),
|
... 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
|
``DateTimeField`` extracts
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -483,8 +492,9 @@ Each class is also a ``Transform`` registered on ``DateTimeField`` as
|
||||||
>>> from datetime import datetime
|
>>> from datetime import datetime
|
||||||
>>> from django.utils import timezone
|
>>> from django.utils import timezone
|
||||||
>>> from django.db.models.functions import (
|
>>> from django.db.models.functions import (
|
||||||
... ExtractDay, ExtractHour, ExtractMinute, ExtractMonth, ExtractSecond,
|
... ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||||
... ExtractWeek, ExtractWeekDay, ExtractYear,
|
... ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay,
|
||||||
|
... ExtractYear,
|
||||||
... )
|
... )
|
||||||
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
|
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
|
||||||
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, 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())
|
... end_datetime=end_2015, end_date=end_2015.date())
|
||||||
>>> Experiment.objects.annotate(
|
>>> Experiment.objects.annotate(
|
||||||
... year=ExtractYear('start_datetime'),
|
... year=ExtractYear('start_datetime'),
|
||||||
|
... quarter=ExtractQuarter('start_datetime'),
|
||||||
... month=ExtractMonth('start_datetime'),
|
... month=ExtractMonth('start_datetime'),
|
||||||
... week=ExtractWeek('start_datetime'),
|
... week=ExtractWeek('start_datetime'),
|
||||||
... day=ExtractDay('start_datetime'),
|
... day=ExtractDay('start_datetime'),
|
||||||
|
@ -503,8 +514,8 @@ Each class is also a ``Transform`` registered on ``DateTimeField`` as
|
||||||
... ).values(
|
... ).values(
|
||||||
... 'year', 'month', 'week', 'day', 'weekday', 'hour', 'minute', 'second',
|
... 'year', 'month', 'week', 'day', 'weekday', 'hour', 'minute', 'second',
|
||||||
... ).get(end_datetime__year=ExtractYear('start_datetime'))
|
... ).get(end_datetime__year=ExtractYear('start_datetime'))
|
||||||
{'year': 2015, 'month': 6, 'week': 25, 'day': 15, 'weekday': 2, 'hour': 23,
|
{'year': 2015, 'quarter': 2, 'month': 6, 'week': 25, 'day': 15, 'weekday': 2,
|
||||||
'minute': 30, 'second': 1}
|
'hour': 23, 'minute': 30, 'second': 1}
|
||||||
|
|
||||||
When :setting:`USE_TZ` is ``True`` then datetimes are stored in the database
|
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
|
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:
|
return:
|
||||||
|
|
||||||
* "year": 2015-01-01 00:00:00+00:00
|
* "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
|
* "month": 2015-06-01 00:00:00+00:00
|
||||||
* "day": 2015-06-15 00:00:00+00:00
|
* "day": 2015-06-15 00:00:00+00:00
|
||||||
* "hour": 2015-06-15 14: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:
|
values returned when this timezone is active will be:
|
||||||
|
|
||||||
* "year": 2015-01-01 00:00:00+11:00
|
* "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
|
* "month": 2015-06-01 00:00:00+10:00
|
||||||
* "day": 2015-06-16 00:00:00+10:00
|
* "day": 2015-06-16 00:00:00+10:00
|
||||||
* "hour": 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'
|
.. 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
|
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
|
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
|
with less precision. ``expression`` can have an ``output_field`` of either
|
||||||
|
|
|
@ -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
|
current time zone before filtering. This requires :ref:`time zone definitions
|
||||||
in the database <database-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
|
.. fieldlookup:: time
|
||||||
|
|
||||||
``time``
|
``time``
|
||||||
|
|
|
@ -227,6 +227,15 @@ Models
|
||||||
from the database. For databases that don't support server-side cursors, it
|
from the database. For databases that don't support server-side cursors, it
|
||||||
controls the number of results Django fetches from the database adapter.
|
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
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ from django.db import connection
|
||||||
from django.db.models import DateField, DateTimeField, IntegerField, TimeField
|
from django.db.models import DateField, DateTimeField, IntegerField, TimeField
|
||||||
from django.db.models.functions import (
|
from django.db.models.functions import (
|
||||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||||
ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, Trunc, TruncDate,
|
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
|
||||||
TruncDay, TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncTime,
|
Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
|
||||||
TruncYear,
|
TruncQuarter, TruncSecond, TruncTime, TruncYear,
|
||||||
)
|
)
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -41,6 +41,11 @@ def truncate_to(value, kind, tzinfo=None):
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
return value.replace(day=1)
|
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
|
# otherwise, truncate to year
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
return value.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
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)],
|
[(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
|
||||||
lambda m: (m.start_datetime, m.extracted)
|
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(
|
self.assertQuerysetEqual(
|
||||||
DTModel.objects.annotate(extracted=Extract('start_datetime', 'month')).order_by('start_datetime'),
|
DTModel.objects.annotate(extracted=Extract('start_datetime', 'month')).order_by('start_datetime'),
|
||||||
[(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
|
[(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
|
||||||
|
@ -279,6 +289,47 @@ class DateFunctionTests(TestCase):
|
||||||
# both dates are from the same week.
|
# both dates are from the same week.
|
||||||
self.assertEqual(DTModel.objects.filter(start_datetime__week=ExtractWeek('start_datetime')).count(), 2)
|
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):
|
def test_extract_week_func_boundaries(self):
|
||||||
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
|
||||||
if settings.USE_TZ:
|
if settings.USE_TZ:
|
||||||
|
@ -456,12 +507,14 @@ class DateFunctionTests(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
test_date_kind('year')
|
test_date_kind('year')
|
||||||
|
test_date_kind('quarter')
|
||||||
test_date_kind('month')
|
test_date_kind('month')
|
||||||
test_date_kind('day')
|
test_date_kind('day')
|
||||||
test_time_kind('hour')
|
test_time_kind('hour')
|
||||||
test_time_kind('minute')
|
test_time_kind('minute')
|
||||||
test_time_kind('second')
|
test_time_kind('second')
|
||||||
test_datetime_kind('year')
|
test_datetime_kind('year')
|
||||||
|
test_datetime_kind('quarter')
|
||||||
test_datetime_kind('month')
|
test_datetime_kind('month')
|
||||||
test_datetime_kind('day')
|
test_datetime_kind('day')
|
||||||
test_datetime_kind('hour')
|
test_datetime_kind('hour')
|
||||||
|
@ -503,6 +556,47 @@ class DateFunctionTests(TestCase):
|
||||||
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
|
with self.assertRaisesMessage(ValueError, "Cannot truncate TimeField 'start_time' to DateTimeField"):
|
||||||
list(DTModel.objects.annotate(truncated=TruncYear('start_time', output_field=TimeField())))
|
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):
|
def test_trunc_month_func(self):
|
||||||
start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
|
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')
|
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),
|
week=Extract('start_datetime', 'week', tzinfo=melb),
|
||||||
weekday=ExtractWeekDay('start_datetime'),
|
weekday=ExtractWeekDay('start_datetime'),
|
||||||
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
|
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
|
||||||
|
quarter=ExtractQuarter('start_datetime', tzinfo=melb),
|
||||||
hour=ExtractHour('start_datetime'),
|
hour=ExtractHour('start_datetime'),
|
||||||
hour_melb=ExtractHour('start_datetime', tzinfo=melb),
|
hour_melb=ExtractHour('start_datetime', tzinfo=melb),
|
||||||
).order_by('start_datetime')
|
).order_by('start_datetime')
|
||||||
|
@ -733,6 +828,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
self.assertEqual(utc_model.week, 25)
|
self.assertEqual(utc_model.week, 25)
|
||||||
self.assertEqual(utc_model.weekday, 2)
|
self.assertEqual(utc_model.weekday, 2)
|
||||||
self.assertEqual(utc_model.weekday_melb, 3)
|
self.assertEqual(utc_model.weekday_melb, 3)
|
||||||
|
self.assertEqual(utc_model.quarter, 2)
|
||||||
self.assertEqual(utc_model.hour, 23)
|
self.assertEqual(utc_model.hour, 23)
|
||||||
self.assertEqual(utc_model.hour_melb, 9)
|
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.day_melb, 16)
|
||||||
self.assertEqual(melb_model.week, 25)
|
self.assertEqual(melb_model.week, 25)
|
||||||
self.assertEqual(melb_model.weekday, 3)
|
self.assertEqual(melb_model.weekday, 3)
|
||||||
|
self.assertEqual(melb_model.quarter, 2)
|
||||||
self.assertEqual(melb_model.weekday_melb, 3)
|
self.assertEqual(melb_model.weekday_melb, 3)
|
||||||
self.assertEqual(melb_model.hour, 9)
|
self.assertEqual(melb_model.hour, 9)
|
||||||
self.assertEqual(melb_model.hour_melb, 9)
|
self.assertEqual(melb_model.hour_melb, 9)
|
||||||
|
@ -836,12 +933,14 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
|
||||||
)
|
)
|
||||||
|
|
||||||
test_date_kind('year')
|
test_date_kind('year')
|
||||||
|
test_date_kind('quarter')
|
||||||
test_date_kind('month')
|
test_date_kind('month')
|
||||||
test_date_kind('day')
|
test_date_kind('day')
|
||||||
test_time_kind('hour')
|
test_time_kind('hour')
|
||||||
test_time_kind('minute')
|
test_time_kind('minute')
|
||||||
test_time_kind('second')
|
test_time_kind('second')
|
||||||
test_datetime_kind('year')
|
test_datetime_kind('year')
|
||||||
|
test_datetime_kind('quarter')
|
||||||
test_datetime_kind('month')
|
test_datetime_kind('month')
|
||||||
test_datetime_kind('day')
|
test_datetime_kind('day')
|
||||||
test_datetime_kind('hour')
|
test_datetime_kind('hour')
|
||||||
|
|
Loading…
Reference in New Issue