Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django model query for the closest integer match

I got some Django model objects with different rating field values:

puzzles_rating = [0, 123, 245, 398, 412, 445, 556, 654, 875, 1000]
    for rating in puzzles_rating:
        puzzle = Puzzle(rating=rating)
        puzzle.save()

Now, for a user_rating = 500, I want to select the puzzle with the closest rating match. In the case above it should be puzzle #6 with rating 445.

The issue is that I can't just do:

puzzle = Puzzle.objects.filter(rating__lte=user_rating).order_by('-rating')[0]

as, in general, my closest match rating might be greater than target rating.

Is there a handy way to query the closest match from both directions?

like image 813
Vadim Tikanov Avatar asked Mar 27 '15 16:03

Vadim Tikanov


2 Answers

You can use the extra method:

puzzle = Puzzle.objects.extra(select={
    'abs_diff': 'ABS(`rating` - %s)',
}, select_params=(rating,)).order_by('abs_diff').first()

From Django 1.8 onwards you don't need to write raw SQL, you can use Func:

from django.db.models import Func, F

    puzzle = Puzzle.objects.annotate(abs_diff=Func(F('rating') - rating, function='ABS')).order_by('abs_diff').first()
like image 115
knbk Avatar answered Nov 09 '22 04:11

knbk


You can get both Puzzle objects and compare in Python which is closer:

# Note, be sure to check that puzzle_lower and puzzle_higher are not None
puzzle_lower = Puzzle.objects.filter(rating__lte=user_rating).order_by('-rating').first()
puzzle_higher = Puzzle.objects.filter(rating__gte=user_rating).order_by('rating').first()

# Note that in a tie, this chooses the lower rated puzzle
if (puzzle_higher.rating - user_rating) < abs(puzzle_lower.rating - user_rating):
    puzzle = puzzle_higher
else:
    puzzle = puzzle_lower
like image 43
Joseph Avatar answered Nov 09 '22 03:11

Joseph