Harry Day
Harry Day

Reputation: 408

Move zeros to the end of a list while leaving False alone

I have some code that should find the position of any 0 items in a list and move it to the back whilst preserving the order of the other elements.

def test2(array):
    try:
        for n in range(len(array)):
            array.append(array.pop(array.index(0)))
        return array
    except ValueError:
        return array

This code works fine for any list apart from one with False in. I think this is because the .index(0): will also return the position of any False in the list. Any way to get around this?

For example if array = [0,1,None,2,False,1,0] then the output should be [1,None,2,False,1,0,0]

With that same input my code produces: [1, None, 2, 1, False, 0, 0]

Upvotes: 3

Views: 799

Answers (6)

cs95
cs95

Reputation: 402423

This is a consequence of the fact that bool is a subclass of int in python, so searching for the first index of 0 will return the index of False, if it is in the list before a 0 because False == 0.

What you can do is check whether a list element is an instance of int, and at the same time, not an instance of bool. This way, you avoid matching other false values (like empty containers and None).

def is_zero(v):
    # return is instance(v, int) and v is not False and not v
    # return isinstance(v, int) and not isinstance(v, bool) and not v
    return type(v) in (int, float) and not v

You can then iterate over lst in reverse and update in-place.

lst = [1, 0, None, False, 0, 3, True] # For example.

for i in reversed(range(len(lst))):
    if is_zero(lst[i]):
        lst.append(lst.pop(i))

print(lst)
# [1, None, False, 3, True, 0, 0]

This is amortised linear time complexity, if I'm not mistaken.

Upvotes: 3

gilch
gilch

Reputation: 11651

Python guarantees True and False are the only instances of bool, so you can use is to distinguish False from 0.

z = []
return [e for e in array if e is False or e != 0 or z.append(e)] + z

This will preserve the order of the various non-False zeros (0, 0.0, 0j, Decimal(0), Fraction(0, 1)), that may be in the list.

Upvotes: 0

Harry Day
Harry Day

Reputation: 408

I think this is as concise as this can be. Thanks for the help everyone

l = [i for i in array if isinstance(i, bool) or i!=0]
return l+[0]*(len(array)-len(l))

Upvotes: 0

o11c
o11c

Reputation: 16056

You can use a wrapper with a custom __eq__:

class Typed:
    def __init__(self, val):
        self.val = val
    def __hash__(self):
        return hash(self.val)
    def __eq__(self, other):
        if isinstance(other, Typed):
            other = other.val
        return type(self.val) is type(other) and self.val == other

Then replace array.index(0) with array.index(Typed(0)) - you don't need to use this within the array itself.

Extending this to containers (so that (0, 1) and (False, True) won't be equal) is left as an exercise for the reader.

Upvotes: 1

DYZ
DYZ

Reputation: 57033

A variation of @coldspeed's solution:

array = [0, 1, None, 2, False, 1, 0]
nonzero = [x for x in array if x or x is None or isinstance(x,bool)]
nonzero + [0] * (len(array) - len(nonzero))
# [1, None, 2, False, 1, 0, 0]

Upvotes: 1

Patrick Artner
Patrick Artner

Reputation: 51643

If creating another list is of no concern, you can use list-comprehensions:

def test2(array):
    la = len(array)
    return ([x for x in array if not isinstance(x,int) or x]+[0]*la)[:la]

The first part filters out any 0 integerbut lets any non-int pass. The second part adds (too many) zeros and trims the result back down to the original lenght.

Caveat: This will create up to twice the original list-length in data - so not good for "big" lists or lists with few 0.

Usage:

k = [1,2,3,0,4,5,False,92,-3,0,1,0,0,True,[],None,True]
print(k)
print(test2(k))

Output:

[1, 2, 3, 0, 4, 5, False, 92, -3, 0, 1, 0, 0, True, [], None, True]
[1, 2, 3, 4, 5, 92, -3, 1, True, True, 0, 0, 0, 0, 0, 0, 0]

Doku:

  • (related) Truth-testing - there are other values that are "False" as well.

Upvotes: 2

Related Questions