日期时间对象上的Django F表达式

Nis*_*ede 5 python django django-queryset python-2.7

我的模型是:

class Test():
   date1 = models.DateTimeField()
   date2 = models.DateTimeField()
Run Code Online (Sandbox Code Playgroud)

我可以使用以下查询找出date2大于的对象date1

Test.obejcts.filter(date2__gt=F('date1'))
Run Code Online (Sandbox Code Playgroud)

我想找到所有date2大于date1一年的物体。
如何根据date1和之间的差异找出对象date2

Joh*_*fis 6

通用解决方案:

您可以更改annotate日期,然后对照timedelta(days=365)(与@Anonymous在其评论中建议的内容非常接近)进行检查:

Test.objects.annotate(
    duration=F('date2') - F('date1')
).filter(duration__gt=timedelta(days=365))
Run Code Online (Sandbox Code Playgroud)


PostgreSQL特定解决方案:

如果您正在使用PostgreSQL,则可以从此答案中得到另一个选项:

from django.db.models import F, Func

Test.objects.annotate(
    duration = Func(F('date2'), F('date1'), function='age')
).filter(duration__gt=timedelta(days=365))
Run Code Online (Sandbox Code Playgroud)


imp*_*ren 5

您可以一起使用__date查找TruncDate函数


from django.db.models import DateField, ExpressionWrapper, F
from django.db.models.functions import TruncDate
Test.obejcts.filter(
    date2__date__gt=ExpressionWrapper(
        TruncDate(F('date1')) + datetime.timedelta(days=365),
        output_field=DateField(),
    ),
)

Run Code Online (Sandbox Code Playgroud)

如果您真正需要的是类似date1 = 2019-05-14, date2 > 2020-05-14. 那么这种方法并不总是正确的,因为闰年​​有 366 天。这个问题可以结合使用TruncExtract函数来解决。不同的方法是可能的......例如:

from django.db.models import DateField, ExpressionWrapper, F
from django.db.models.functions import TruncDate, ExtractDay

date_field = DateField()

YEAR = timedelta(days=365)
LEAP_YEAR = timedelta(days=366)

shifted_date1 = ExpressionWrapper(
    TruncDate(F('date1')) + YEAR,
    output_field=date_field,
)

leap_shifted_date1 = ExpressionWrapper(
    TruncDate(F('date1')) + LEAP_YEAR,
    output_field=date_field,
)


qs = Test.objects.filter(
    (
        # It's ok to add 365 days if...
        Q(date2__date__gt=shifted_date1)
        &
        (
            # If day of month after 365 days is the same...
            Q(date1__day=ExtractDay(shifted_date1))
            |
            # Or it's 29-th of February
            Q(
                date1__month=2,
                date1__day=29,
            )
        )
    )
    |
    Q(
        # Use 366 days for other cases
        date2__date__gt=leap_shifted_date1,
    )
)

Run Code Online (Sandbox Code Playgroud)

PS如果您USE_TZ = True在特定时区执行查询(例如timezone.activate(...)在执行查询集之前使用),那么TruncDate 添加之前timedelta做很重要,因为在TruncDate(F('date1')+timedelta(...))每年不同日期执行切换到“夏令时”的国家/地区这样做可能会产生不正确的结果。例如:

  • 一些国家2019-03-31在 2019 年切换到夏令时时间,并将2020-03-29在 2020 年切换。
  • 当地时间2019-03-30 23:30尚未使用夏令时。
  • 将 366 天(因为明年是闰年)添加到它会给出2020-03-30 23:30 "non-DST",所以在“标准化”之后这个日期时间将变成 2020-03-31 00:30 "DST"
  • TruncDate在添加 timedelta 之前使用解决了这个问题,因为TruncDate将 value 转换为 date

额外信息:一些国家/地区在固定日期(例如每年 2 月 1 日)切换到 DST,其他国家/地区可能会在“3 月的最后一个星期日”切换,这可能是每年不同的日期。

import pytz
import datetime

kyiv.localize(datetime.datetime(2011, 3, 28, 0, 1)) - kyiv.localize(datetime.datetime(2010, 3, 28, 0, 1))
# `datetime.timedelta(364, 82800)` is less than 365 days
Run Code Online (Sandbox Code Playgroud)

闰秒年”( 2016-12-31 23:59:60.999) 的PPS 最后几秒也可能受到 TruncDate/timedelta-shift 顺序的影响,但“幸运的是”大多数数据库不支持闰秒,pythondatetime.datetime 也缺少此功能