Correct way to filter objects with `created` date exactly 1/2/3 years ago?

Hello,

for a new feature I would need to query objects (which have created attribute) where the date is 1 year ago, or 2 years ago etc… The use case is displaying “anniversaries”.

What is the correct approach to take into account leap years and other calendar weirdness? I have used timedelta for shorter timespans, but I don’t think that is correct usage, since I can init it with days value max.

Thanks!

Btw my database is Postgres.

See the month and day comparison operators.

1 Like

Also relativedelta(years=1) may be what you want: relativedelta — dateutil 2.8.2 documentation

2 Likes

Thanks both! The relativedelta looks especially nice, since I can specify entire years.

Will try this out and report how it went :smiley:

So finally got around to it, but somehow I cant seem to get it quite right, although it seems somewhat trivial.

I initially tried the relativedelta approach to generate anniversary dates for given years and then query apps.

But then I realized I can use the __day and __month filter options on the datetime field to get all records which are precisely X years ago…

objects.filter(app_released__month=month, app_released__day=day)

I am using the timezone.now() to get these numbers.

This kind of seems to work, but it keeps showing same record the next day as anniversaries, which is wrong.

I have a few tests where I am injecting mock datetime and all pass sucessfully. Even the one which tests the day right after the anniversary day.

I am using the app_released field also on detail page, to know if there is anniversary and this logic (also based on checking day and month) works correctly. Meaning first day it shows the anniversary info but not the next day.

Any ideas what to test?

PS: All these times should be in UTC.

If you’re using timezone.now() to get the current day and month, check to verify if that’s local time. See: Time zones | Django documentation | Django

I have the project template, which uses UTC for the TIME_ZONE settings and USE_TZ is set to True.

Since month and day are just integers, I find it curious why are same records filtered on two separate days.

If it were a timezone issue (not saying it is) your local time days don’t necessarily correspond to UTC days. (Nov 17 EST spans in part both UTC Nov 17 and Nov 18.)

Without seeing the actual code in the view, it’s tough to say what might be wrong. I’d be looking to verify that the right values are being used at all points in the process - but anything specific would all be conjecture.

Here is the entire “logic” that I have. Nothing fancy I would say.

def get_apps_with_anniversary(now=timezone.now()):
    day, month = now.day, now.month

    possible_anniversaries = AppRecord.objects.filter(app_released__month=month, app_released__day=day)

    anniversaries = []

    year = now.year

    for app in possible_anniversaries:
        if app.app_released.year == year:
            continue

        anniversaries.append(AppWithAnniversary(app=app, year=year-app.app_released.year))

    return anniversaries

In my understanding timezones shouldn’t even play a part here?

For example some app was released Nov 17 2016.

With the timezone.now(), that gets me day=17 and month=11 as long as it is this day in UTC time… But the current behavior is for some reason that I would get the same result for my query tomorrow Now 18, although day should be 18 and therefore this part of the filter: app_released__day=day should rule it out?

This problem must be something rather dumb but I cannot find it

Please try moving the assignment of now to a line within the function. I’ve got a feeling that the value is being bound once, and not new for every call.

2 Likes

:scream: No way. That would explain it, as it would set this to always be the day I deployed the changes.

I think I briefly considered it but thought it must surely be wrong since this is function that shouldn’t be in any global scope and is always called from the view…

That line is interpreted at the time the function is compiled - the timezone.now() statement returns an object, bound to the variable at that time. It’s effectively the same issue encountered as using a mutable object as a default parameter.