2

I have a list of strings which can represent integers as well as names. The default string compare does the following:

sorted(['1','2','3','4','10','102','14','Alice','John','Sally'])
['1', '10', '102', '14', '2', '3', '4', 'Alice', 'John', 'Sally']

I would like to sort the list as follows:

['1', '2', '3', '4', '10', '14', '102', 'Alice', 'John', 'Sally']

which means:

  1. sort all strings which represent an integer numerically
  2. sort the 'real' strings alphabetically and append this list to (1.)

I have tried with a compare method, but I don't know how to determine cleanly if the string represents an integer without try/except?

Thanks in advance

any1
  • 292
  • 3
  • 9

4 Answers4

10

If there are no negative numbers:

lyst = ['1','2','3','4','10','102','14','Alice','John','Sally']
print sorted(lyst, key=lambda k: int(k) if k.isdigit() else k)

Here's a version that doesn't rely on CPython details and works with Python 3:

sorted(lyst, key=lambda k: (0, int(k)) if k.isdigit() else (1, k))

Here the key is a tuple. The first item in the tuple is 0 or 1 for number or text, which causes the numbers to sort before the text. Then the second item in the tuple is the value, which causes the values to be sorted appropriately within their group. I originally used float("+inf") to make the text items sort after the numbers, but this approach (inspired by Tom Zych's answer) is simpler and faster.

If you want the string sorting to not be case sensitive, just add .lower():

sorted(lyst, key=lambda k: (0, int(k)) if k.isdigit() else (1, k.lower()))
kindall
  • 178,883
  • 35
  • 278
  • 309
  • Thanks, exactly what I needed! (There are no ocurrences of negative numbers in my case) – any1 Oct 17 '11 at 15:14
  • 4
    Note that this: (1) relies on CPtyhon's implementation detail (ints compare before strings rather than after); (2) doesn't work with Python 3 since comparing ints to strings is not allowed. – NPE Oct 17 '11 at 15:20
5

The following works in both Python 2 and Python 3:

l = ['1','2','3','4','10','102','14','Alice','John','Sally','33']
num, alpha = [], []
[num.append(elem) if elem.isdigit() else alpha.append(elem) for elem in l]
result = sorted(num, key=int) + sorted(alpha)
print(result)

It avoids comparing strings to ints by partitioning the list. The reason to avoid such a comparison is that it's either not fully specified (Python 2) or prohibited (Python 3).

Community
  • 1
  • 1
NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • +1. I like that this works on 2 and 3. I don't like the subscripting with a boolean since that can be easily avoided. I would change the list comp to `[num.append(elem) if elem.isdigit() else alpha.append(elem) for elem in l]`. It also allows you to snip one line. – Steven Rumbalski Oct 17 '11 at 16:23
  • Clever, upvote. You could avoid creating a list of `None`s by using a list comprehension inside `any(...)`. – kindall Oct 17 '11 at 17:09
  • Or, that is, a generator expression inside `any(...)`, not a list comprehension! – kindall Oct 17 '11 at 17:16
5

This should work with the versions of sort that take a key function.

def sortkey(s):
    try:
        n = int(s)
        return (0, n)
    except ValueError:
        return (1, s)
Tom Zych
  • 13,329
  • 9
  • 36
  • 53
  • This works because tuples are compared lexicographically; the first items are compared; if they are the same then the second items are compared, and so on. Very nice. – Jarek Przygódzki Oct 17 '11 at 15:28
  • To make this work for Python 3, return `(0, n, "")` for integers and `(1, 0, s)` for strings. I like how this also works with negative numbers. It can, however, produce incorrect results with floats. – Steven Rumbalski Oct 17 '11 at 16:12
  • @StevenRumbalski: OP did say "integers". If there were floats, of course, we would need to add a trial conversion for them. My code appears to work correctly as written in 3.1; why do you think your change is necessary? – Tom Zych Oct 17 '11 at 16:19
  • @Tom. You are correct, I was thinking that the second value of the tuple would fail with an integer to string comparison on Python 3, but I see that it would compare the first value and not the second item unless the first are the same. So nevermind. – Steven Rumbalski Oct 17 '11 at 17:07
0

I would go with the compare function:

import types

def cmp_func(val1, val2):
  # is val1 an integer?
  try: 
    val1 = int(val1)
  except ValueError: 
    pass # val1 is no integer
  try: 
    val2 = int(val2)
  except ValueError: 
    pass #val2 is no integer

  if type(val1) == types.IntType and type(val2) == types.IntType:
    return cmp(val1, val2)
  elif type(val1) == types.StringType and type(val2) == types.IntType:
    # firstly strings, afterwards integer values
    return -1
  elif type(val1) == types.IntType and type(val2) == types.StringType:
    # firstly strings, afterwards integer values
    return 1
  else:
    return cmp(val1, val2)


if __name__ == "__main__":
  my_list = ['1', '10', '102', '14', '2', '3', '4', 'Alice', 'John', 'Sally']
  my_list.sort(cmp_func)
  print(my_list)
Nicola Coretti
  • 2,655
  • 2
  • 20
  • 22