Kaydet (Commit) c7f6ffbd authored tarafından Mads Jensen's avatar Mads Jensen Kaydeden (comit) Tim Graham

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

Thanks Mariusz Felisiak, Tim Graham, and Adam Johnson for review.
üst f6bd0013
...@@ -39,6 +39,10 @@ class DatabaseOperations(BaseDatabaseOperations): ...@@ -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): ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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 @@ ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ( ...@@ -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__ = [ ...@@ -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): ...@@ -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) ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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:: ...@@ -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``:: ...@@ -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``:: ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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:: ...@@ -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 ...@@ -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 ...@@ -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
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment