mirror of
https://github.com/django/django.git
synced 2025-08-04 10:59:45 +00:00
Fixed #25774 -- Refactor datetime expressions into public API
This commit is contained in:
parent
77b73e79a4
commit
2a4af0ea43
15 changed files with 1429 additions and 225 deletions
|
@ -293,3 +293,402 @@ Usage example::
|
|||
.. versionchanged:: 1.9
|
||||
|
||||
The ability to register the function as a transform was added.
|
||||
|
||||
Date Functions
|
||||
==============
|
||||
|
||||
.. module:: django.db.models.functions.datetime
|
||||
|
||||
.. versionadded:: 1.10
|
||||
|
||||
We'll be using the following model in examples of each function::
|
||||
|
||||
class Experiment(models.Model):
|
||||
start_time = models.DateTimeField()
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_time = models.DateTimeField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
|
||||
``Extract``
|
||||
-----------
|
||||
|
||||
.. class:: Extract(expression, lookup_name=None, tzinfo=None, **extra)
|
||||
|
||||
Extracts a component of a date as a number.
|
||||
|
||||
Takes an ``expression`` representing a ``DateField`` or ``DateTimeField`` and a
|
||||
``lookup_name``, and returns the part of the date referenced by ``lookup_name``
|
||||
as an ``IntegerField``. Django usually uses the databases' extract function, so
|
||||
you may use any ``lookup_name`` that your database supports. A ``tzinfo``
|
||||
subclass, usually provided by ``pytz``, can be passed to extract a value in a
|
||||
specific timezone.
|
||||
|
||||
Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
|
||||
``lookup_name``\s return:
|
||||
|
||||
* "year": 2015
|
||||
* "month": 6
|
||||
* "day": 15
|
||||
* "week_day": 2
|
||||
* "hour": 23
|
||||
* "minute": 30
|
||||
* "second": 1
|
||||
|
||||
If a different timezone like ``Australia/Melbourne`` is active in Django, then
|
||||
the datetime is converted to the timezone before the value is extracted. The
|
||||
timezone offset for Melbourne in the example date above is +10:00. The values
|
||||
returned when this timezone is active will be the same as above except for:
|
||||
|
||||
* "day": 16
|
||||
* "week_day": 3
|
||||
* "hour": 9
|
||||
|
||||
.. admonition:: ``week_day`` values
|
||||
|
||||
The ``week_day`` ``lookup_type`` is calculated differently from most
|
||||
databases and from Python's standard functions. This function will return
|
||||
``1`` for Sunday, ``2`` for Monday, through ``7`` for Saturday.
|
||||
|
||||
The equivalent calculation in Python is::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> dt = datetime(2015, 6, 15)
|
||||
>>> (dt.isoweekday() % 7) + 1
|
||||
2
|
||||
|
||||
Each ``lookup_name`` above has a corresponding ``Extract`` subclass (listed
|
||||
below) that should typically be used instead of the more verbose equivalent,
|
||||
e.g. use ``ExtractYear(...)`` rather than ``Extract(..., lookup_name='year')``.
|
||||
|
||||
Usage example::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.db.models.functions import Extract
|
||||
>>> start = datetime(2015, 6, 15)
|
||||
>>> end = datetime(2015, 7, 2)
|
||||
>>> Experiment.objects.create(
|
||||
... start_time=start, start_date=start.date(),
|
||||
... end_time=end, end_date=end.date())
|
||||
>>> # Add the experiment start year as a field in the QuerySet.
|
||||
>>> experiment = Experiment.objects.annotate(
|
||||
... start_year=Extract('start_time', 'year')).get()
|
||||
>>> experiment.start_year
|
||||
2015
|
||||
>>> # How many experiments completed in the same year in which they started?
|
||||
>>> Experiment.objects.filter(
|
||||
... start_time__year=Extract('end_time', 'year')).count()
|
||||
1
|
||||
|
||||
``DateField`` extracts
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: ExtractYear(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'year'
|
||||
|
||||
.. class:: ExtractMonth(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'month'
|
||||
|
||||
.. class:: ExtractDay(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'day'
|
||||
|
||||
.. class:: ExtractWeekDay(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'week_day'
|
||||
|
||||
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``.
|
||||
|
||||
Since ``DateField``\s don't have a time component, only ``Extract`` subclasses
|
||||
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 (
|
||||
... ExtractYear, ExtractMonth, ExtractDay, ExtractWeekDay
|
||||
... )
|
||||
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
|
||||
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(
|
||||
... start_time=start_2015, start_date=start_2015.date(),
|
||||
... end_time=end_2015, end_date=end_2015.date())
|
||||
>>> Experiment.objects.annotate(
|
||||
... year=ExtractYear('start_date'),
|
||||
... month=ExtractMonth('start_date'),
|
||||
... day=ExtractDay('start_date'),
|
||||
... weekday=ExtractWeekDay('start_date'),
|
||||
... ).values('year', 'month', 'day', 'weekday').get(
|
||||
... end_date__year=ExtractYear('start_date'),
|
||||
... )
|
||||
{'year': 2015, 'month': 6, 'day': 15, 'weekday': 2}
|
||||
|
||||
``DateTimeField`` extracts
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In addition to the following, all extracts for ``DateField`` listed above may
|
||||
also be used on ``DateTimeField``\s .
|
||||
|
||||
.. class:: ExtractHour(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'hour'
|
||||
|
||||
.. class:: ExtractMinute(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'minute'
|
||||
|
||||
.. class:: ExtractSecond(expression, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'second'
|
||||
|
||||
These are logically equivalent to ``Extract('datetime_field', lookup_name)``.
|
||||
Each class is also a ``Transform`` registered on ``DateTimeField`` as
|
||||
``__(lookup_name)``, e.g. ``__minute``.
|
||||
|
||||
``DateTimeField`` examples::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.utils import timezone
|
||||
>>> from django.db.models.functions import (
|
||||
... ExtractYear, ExtractMonth, ExtractDay, ExtractWeekDay,
|
||||
... ExtractHour, ExtractMinute, ExtractSecond,
|
||||
... )
|
||||
>>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
|
||||
>>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(
|
||||
... start_time=start_2015, start_date=start_2015.date(),
|
||||
... end_time=end_2015, end_date=end_2015.date())
|
||||
>>> Experiment.objects.annotate(
|
||||
... year=ExtractYear('start_time'),
|
||||
... month=ExtractMonth('start_time'),
|
||||
... day=ExtractDay('start_time'),
|
||||
... weekday=ExtractWeekDay('start_time'),
|
||||
... hour=ExtractHour('start_time'),
|
||||
... minute=ExtractMinute('start_time'),
|
||||
... second=ExtractSecond('start_time'),
|
||||
... ).values(
|
||||
... 'year', 'month', 'day', 'weekday', 'hour', 'minute', 'second',
|
||||
... ).get(end_time__year=ExtractYear('start_time'))
|
||||
{'year': 2015, 'month': 6, '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
|
||||
to that timezone before the value is extracted. The example below converts to
|
||||
the Melbourne timezone (UTC +10:00), which changes the day, weekday, and hour
|
||||
values that are returned::
|
||||
|
||||
>>> import pytz
|
||||
>>> tzinfo = pytz.timezone('Australia/Melbourne') # UTC+10:00
|
||||
>>> with timezone.override(tzinfo):
|
||||
... Experiment.objects.annotate(
|
||||
... day=ExtractDay('start_time'),
|
||||
... weekday=ExtractWeekDay('start_time'),
|
||||
... hour=ExtractHour('start_time'),
|
||||
... ).values('day', 'weekday', 'hour').get(
|
||||
... end_time__year=ExtractYear('start_time'),
|
||||
... )
|
||||
{'day': 16, 'weekday': 3, 'hour': 9}
|
||||
|
||||
Explicitly passing the timezone to the ``Extract`` function behaves in the same
|
||||
way, and takes priority over an active timezone::
|
||||
|
||||
>>> import pytz
|
||||
>>> tzinfo = pytz.timezone('Australia/Melbourne')
|
||||
>>> Experiment.objects.annotate(
|
||||
... day=ExtractDay('start_time', tzinfo=melb),
|
||||
... weekday=ExtractWeekDay('start_time', tzinfo=melb),
|
||||
... hour=ExtractHour('start_time', tzinfo=melb),
|
||||
... ).values('day', 'weekday', 'hour').get(
|
||||
... end_time__year=ExtractYear('start_time'),
|
||||
... )
|
||||
{'day': 16, 'weekday': 3, 'hour': 9}
|
||||
|
||||
|
||||
``Trunc``
|
||||
---------
|
||||
|
||||
.. class:: Trunc(expression, kind, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
Truncates a date up to a significant component.
|
||||
|
||||
When you only care if something happened in a particular year, hour, or day,
|
||||
but not the exact second, then ``Trunc`` (and its subclasses) can be useful to
|
||||
filter or aggregate your data. For example, you can use ``Trunc`` to calculate
|
||||
the number of sales per day.
|
||||
|
||||
``Trunc`` takes a single ``expression``, representing a ``DateField`` or
|
||||
``DateTimeField``, a ``kind`` representing a date part, and an ``output_field``
|
||||
that's either ``DateTimeField()`` or ``DateField()``. It returns a datetime or
|
||||
date, depending on ``output_field``, with fields up to ``kind`` set to their
|
||||
minimum value. If ``output_field`` is omitted, it will default to the
|
||||
``output_field`` of ``expression``. A ``tzinfo`` subclass, usually provided by
|
||||
``pytz``, can be passed to truncate a value in a specific timezone.
|
||||
|
||||
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
|
||||
* "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
|
||||
* "minute": 2015-06-15 14:30:00+00:00
|
||||
* "second": 2015-06-15 14:30:50+00:00
|
||||
|
||||
If a different timezone like ``Australia/Melbourne`` is active in Django, then
|
||||
the datetime is converted to the new timezone before the value is truncated.
|
||||
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
|
||||
* "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
|
||||
* "minute": 2015-06-16 00:30:00+10:00
|
||||
* "second": 2015-06-16 00:30:50+10:00
|
||||
|
||||
The year has an offset of +11:00 because the result transitioned into daylight
|
||||
saving time.
|
||||
|
||||
Each ``kind`` above has a corresponding ``Trunc`` subclass (listed below) that
|
||||
should typically be used instead of the more verbose equivalent,
|
||||
e.g. use ``TruncYear(...)`` rather than ``Trunc(..., kind='year')``.
|
||||
|
||||
The subclasses are all defined as transforms, but they aren't registered with
|
||||
any fields, because the obvious lookup names are already reserved by the
|
||||
``Extract`` subclasses.
|
||||
|
||||
Usage example::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.db.models import Count, DateTimeField
|
||||
>>> from django.db.models.functions import Trunc
|
||||
>>> Experiment.objects.create(start_time=datetime(2015, 6, 15, 14, 30, 50, 321))
|
||||
>>> Experiment.objects.create(start_time=datetime(2015, 6, 15, 14, 40, 2, 123))
|
||||
>>> Experiment.objects.create(start_time=datetime(2015, 12, 25, 10, 5, 27, 999))
|
||||
>>> experiments_per_day = Experiment.objects.annotate(
|
||||
... start_day=Trunc('start_time', 'day', output_field=DateTimeField())
|
||||
... ).values('start_day').annotate(experiments=Count('id'))
|
||||
>>> for exp in experiments_per_day:
|
||||
... print(exp['start_day'], exp['experiments'])
|
||||
...
|
||||
2015-06-15 00:00:00 2
|
||||
2015-12-25 00:00:00 1
|
||||
>>> experiments = Experiment.objects.annotate(
|
||||
... start_day=Trunc('start_time', 'day', output_field=DateTimeField())
|
||||
... ).filter(start_day=datetime(2015, 6, 15))
|
||||
>>> for exp in experiments:
|
||||
... print(exp.start_time)
|
||||
...
|
||||
2015-06-15 14:30:50.000321
|
||||
2015-06-15 14:40:02.000123
|
||||
|
||||
``DateField`` truncation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: TruncYear(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'year'
|
||||
|
||||
.. class:: TruncMonth(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'month'
|
||||
|
||||
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
|
||||
``DateField`` or ``DateTimeField``.
|
||||
|
||||
Since ``DateField``\s don't have a time component, only ``Trunc`` subclasses
|
||||
that deal with date-parts can be used with ``DateField``::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from django.db.models import Count
|
||||
>>> from django.db.models.functions import TruncMonth, TruncYear
|
||||
>>> from django.utils import timezone
|
||||
>>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc)
|
||||
>>> start2 = datetime(2015, 6, 15, 14, 40, 2, 123, tzinfo=timezone.utc)
|
||||
>>> start3 = datetime(2015, 12, 31, 17, 5, 27, 999, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(start_time=start1, start_date=start1.date())
|
||||
>>> Experiment.objects.create(start_time=start2, start_date=start2.date())
|
||||
>>> Experiment.objects.create(start_time=start3, start_date=start3.date())
|
||||
>>> experiments_per_year = Experiment.objects.annotate(
|
||||
... year=TruncYear('start_date')).values('year').annotate(
|
||||
... experiments=Count('id'))
|
||||
>>> for exp in experiments_per_year:
|
||||
... print(exp['year'], exp['experiments'])
|
||||
...
|
||||
2014-01-01 1
|
||||
2015-01-01 2
|
||||
|
||||
>>> import pytz
|
||||
>>> melb = pytz.timezone('Australia/Melbourne')
|
||||
>>> experiments_per_month = Experiment.objects.annotate(
|
||||
... month=TruncMonth('start_time', tzinfo=melb)).values('month').annotate(
|
||||
... experiments=Count('id'))
|
||||
>>> for exp in experiments_per_month:
|
||||
... print(exp['month'], exp['experiments'])
|
||||
...
|
||||
2015-06-01 00:00:00+10:00 1
|
||||
2016-01-01 00:00:00+11:00 1
|
||||
2014-06-01 00:00:00+10:00 1
|
||||
|
||||
``DateTimeField`` truncation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: TruncDate(expression, **extra)
|
||||
|
||||
.. attribute:: lookup_name = 'date'
|
||||
.. attribute:: output_field = DateField()
|
||||
|
||||
``TruncDate`` casts ``expression`` to a date rather than using the built-in SQL
|
||||
truncate function. It's also registered as a transform on ``DateTimeField`` as
|
||||
``__date``.
|
||||
|
||||
.. class:: TruncDay(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'day'
|
||||
|
||||
.. class:: TruncHour(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'hour'
|
||||
|
||||
.. class:: TruncMinute(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'minute'
|
||||
|
||||
.. class:: TruncSecond(expression, output_field=None, tzinfo=None, **extra)
|
||||
|
||||
.. attribute:: kind = 'second'
|
||||
|
||||
These are logically equivalent to ``Trunc('datetime_field', kind)``. They
|
||||
truncate all parts of the date up to ``kind`` and allow grouping or filtering
|
||||
datetimes with less precision. ``expression`` must have an ``output_field`` of
|
||||
``DateTimeField``.
|
||||
|
||||
Usage example::
|
||||
|
||||
>>> from datetime import date, datetime
|
||||
>>> from django.db.models import Count
|
||||
>>> from django.db.models.functions import (
|
||||
... TruncDate, TruncDay, TruncHour, TruncMinute, TruncSecond,
|
||||
... )
|
||||
>>> from django.utils import timezone
|
||||
>>> import pytz
|
||||
>>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc)
|
||||
>>> Experiment.objects.create(start_time=start1, start_date=start1.date())
|
||||
>>> melb = pytz.timezone('Australia/Melbourne')
|
||||
>>> Experiment.objects.annotate(
|
||||
... date=TruncDate('start_time'),
|
||||
... day=TruncDay('start_time', tzinfo=melb),
|
||||
... hour=TruncHour('start_time', tzinfo=melb),
|
||||
... minute=TruncMinute('start_time'),
|
||||
... second=TruncSecond('start_time'),
|
||||
... ).values('date', 'day', 'hour', 'minute', 'second').get()
|
||||
{'date': datetime.date(2014, 6, 15),
|
||||
'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>),
|
||||
'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>),
|
||||
'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=<UTC>),
|
||||
'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=<UTC>)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue