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):
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:
......
......@@ -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':
......
......@@ -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':
......
......@@ -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',
]
......@@ -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'
......
......@@ -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
......
......@@ -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``
......
......@@ -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
~~~~~~~~~~~~~~~~~~~~~~
......
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