ThePloki
ThePloki

Reputation: 185

Python sorting str price with two decimal points

My goal: Sort a list of Products (dict) first by Price, then by Name. My problem: Str values with numbers in them aren't sorted properly (AKA "Human sorting" or "Natural Sorting").

I found this function from a similar question: Python sorting list of dictionaries by multiple keys

def multikeysort(items, columns):
    from operator import itemgetter
    comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else
                  (itemgetter(col.strip()), 1)) for col in columns]
    def comparer(left, right):
        for fn, mult in comparers:
            result = cmp(fn(left), fn(right))
            if result:
                return mult * result
        else:
            return 0
    return sorted(items, cmp=comparer)

The problem is that my Prices are str type, like this:

products = [
    {'name': 'Product 200', 'price': '3000.00'},
    {'name': 'Product 4', 'price': '100.10'},
    {'name': 'Product 15', 'price': '20.00'},
    {'name': 'Product 1', 'price': '5.05'},
    {'name': 'Product 2', 'price': '4.99'},
]

So they're getting sorted alphabetically, like this:

'100.10'
'20.10'
'3000.00'
'4.99'
'5.05'

Similarly, when I sort by name, I get this:

'Product 1'
'Product 15'
'Product 2'
'Product 200'
'Product 4'

The names should be listed in "human" order (1,2,15 instead of 1,15,2). Is it possible to fix this? I'm pretty new to python, so maybe I'm missing something vital. Thanks.

EDIT

More Info: I'm sending the list of products to a Django template, which requires the numbers to be properly formatted. If I float the prices and then un-float them, I have to iterate through the list of products twice, which seems like overkill.

Upvotes: 0

Views: 1364

Answers (4)

Padraic Cunningham
Padraic Cunningham

Reputation: 180441

To break ties should there be any sorting the strings using the integer value from the product, you can return a tuple:

products = [
    {'name': 'Product 200', 'price': '2.99'},
    {'name': 'Product 4', 'price': '4.99'},
    {'name': 'Product 15', 'price': '4.99'},
    {'name': 'Product 1', 'price': '9.99'},
    {'name': 'Product 2', 'price': '4.99'},
]

def key(x):
    p, i = x["name"].rsplit(None, 1)
    return float(x["price"]), p, int(i)

sorted_products = sorted(products, key=key)

Which would give you:

[{'name': 'Product 200', 'price': '2.99'},
 {'name': 'Product 2', 'price': '4.99'},
 {'name': 'Product 4', 'price': '4.99'},
 {'name': 'Product 15', 'price': '4.99'},
 {'name': 'Product 1', 'price': '9.99'}]

As opposed to:

[{'name': 'Product 200', 'price': '2.99'},
 {'name': 'Product 15', 'price': '4.99'},
 {'name': 'Product 2', 'price': '4.99'},
 {'name': 'Product 4', 'price': '4.99'},
 {'name': 'Product 1', 'price': '9.99'}]

using just float(x['price']), x['name']

Upvotes: 0

Robᵩ
Robᵩ

Reputation: 168646

Your sort function is overkill. Try this simple approach:

from pprint import pprint

products = [
    {'name': 'Product 200', 'price': '3000.00'},
    {'name': 'Product 4', 'price': '100.10'},
    {'name': 'Product 15', 'price': '20.00'},
    {'name': 'Product 1', 'price': '5.05'},
    {'name': 'Product 2', 'price': '4.99'},
]

sorted_products = sorted(products, key=lambda x: (float(x['price']), x['name']))
pprint(sorted_products)

Result:

[{'name': 'Product 2', 'price': '4.99'},
 {'name': 'Product 1', 'price': '5.05'},
 {'name': 'Product 15', 'price': '20.00'},
 {'name': 'Product 4', 'price': '100.10'},
 {'name': 'Product 200', 'price': '3000.00'}]

The essence of my solution is to have the key function return a tuple of the sort conditions. Tuples always compare lexicographically, so the first item is the primary sort, the second is the secondary sort, and so on.

Upvotes: 3

Vaibhav Bajaj
Vaibhav Bajaj

Reputation: 1934

Try typecasting them to floats in the question and when you need to print 2 decimal places, you can easily format the output like so:

float_num = float("110.10")
print "{0:.2f}".format(float_num) # prints 110.10

Upvotes: 1

Tyler
Tyler

Reputation: 18177

I think your best bet is to parse the prices as floats (so you can sort them):

float("1.00")
# output: 1.0

Then output them with two decimal places:

"{:.2f}".format(1.0)
# output: "1.00"

Upvotes: 2

Related Questions