Sorting a list by conditional criteria

18,150

Solution 1

One liner solution:

mylist.sort(key=lambda x: (len(x.split())>1, x if len(x.split())==1 else int(x.split()[-1]) ) )

Explanation: First condition len(x.split())>1 makes sure that multi word strings go behind single word strings as they will probably have numbers. So now ties will be there only between single word strings with single word strings or multi word strings with multi word strings due to first condition. Note there won't be any ties with multi word and single word strings. So if multi word string I return an integer else return string itself.

Example:

['xyz', 'keyword 1000', 'def', 'abc', 'keyword 2', 'keyword 1']

Results :

>>> mylist=['xyz', 'keyword 1000', 'def', 'abc', 'keyword 2', 'keyword 1']
>>> mylist.sort(key=lambda x: (len(x.split())>1, x if len(x.split())==1 else int(x.split()[-1]) ) )
>>> mylist
['abc', 'def', 'xyz', 'keyword 1', 'keyword 2', 'keyword 1000']

Solution 2

You can use the "last" element that doesn't contain your keyword as a barrier to sort first the words without the keyword and then the words with the keyword:

barrier = max(filter(lambda x: 'keyword' not in x, mylist))
# 'xyz'    

mylist_barriered = [barrier + x if 'keyword' in x else x for x in mylist]
# ['abc', 'xyz', 'xyzkeyword 2', 'def', 'xyzkeyword 1']

res = sorted(mylist_barriered)
# ['abc', 'def', 'xyz', 'xyzkeyword 1', 'xyzkeyword 2']

# Be sure not to replace the barrier itself, `x != barrier`
res = [x.replace(barrier, '') if barrier in x and x != barrier else x for x in res]

res is now:

['abc', 'def', 'xyz', 'keyword 1', 'keyword 2']

The benefit of this non-hard-coded approach (outside out 'keyword', obviously), is that your keyword can occur anywhere in the string and the method will still work. Try the above code with ['abc', 'def', '1 keyword 2', 'xyz', '1 keyword 4'] to see what I mean.

Another easy way to do this, with a divide-and-conquer approach:

precedes = [x for x in mylist if 'keyword' not in x]

sort_precedes = sorted(precedes)

follows = [x for x in mylist if 'keyword' in x]

sort_follows = sorted(follows)

together = sort_precedes + sort_follows

together
['abc', 'def', 'xyz', 'keyword 1', 'keyword 2']

Solution 3

Sort with a tuple by first checking if the item starts with the keyword. If it is, set the first item in the tuple to 1 and then set the other item to the number following the keyword. For non-keyword items, set the first tuple item to 0 (so they always come before keywords) and then the other tuple item can be used for a lexicographical sort:

def func(x):
   if x.startswith('keyword'):
       return 1, int(x.split()[-1])
   return 0, x

mylist.sort(key=func)
print(mylist)
# ['abc', 'def', 'xyz', 'keyword 1', 'keyword 2']
Share:
18,150
Demosthene
Author by

Demosthene

Updated on July 16, 2022

Comments

  • Demosthene
    Demosthene almost 2 years

    I know how to simply sort a list in Python using the sort() method and an appropriate lambda rule. However I don't know how to deal with the following situation :

    I have a list of strings, that either contain only letters or contain a specific keyword and a number. I want to sort the list first so as to put the elements with the keyword at the end, then sort those by the number they contain.

    e.g. my list could be: mylist = ['abc','xyz','keyword 2','def','keyword 1'] and I want it sorted to ['abc','def','xyz','keyword 1','keyword 2'].

    I already have something like

    mylist.sort(key=lambda x: x.split("keyword")[0],reverse=True)
    

    which produces only

    ['xyz', 'def', 'abc', 'keyword 2', 'keyword 1']
    
  • Demosthene
    Demosthene about 7 years
    Is there a simple way of sorting the numbers as numbers and not strings? i.e. with your solution keyword 1000 comes between keyword 1 and keyword 2
  • Moses Koledoye
    Moses Koledoye about 7 years
    This does a lexicographical sort on the numbers instead of a numerical sort, so 1000 will come before 2
  • Demosthene
    Demosthene about 7 years
    @MosesKoledoye yeah, I get that, which is why I'm asking if there's a simple way of throwing a numerical sort on top of it.
  • Moses Koledoye
    Moses Koledoye about 7 years
    @Demosthene Well, I addressed that in my answer by converting to int
  • Lucien950
    Lucien950 over 2 years
    len(x.split())>1 will always return False given len(x.split())==1, so can you just write False, or 0 instead?