2

I would like to test multiple inequalities at once, i.e.

if (a < b < c < ...)

which is fine when all the values are present. However sometimes the numeric value of one or more of the variables to be compared may be missing/unknown; the correct behaviour in my context is to assume the associated inequality is satisfied. Let's say I assign the special value None when the value is unknown: the behaviour I want from the < operator (or an alternative) is:

>>> a = 1; b = 2; c = 3
>>> a < b < c # this works fine, obviously
True 
>>> b = None
>>> a < b < c # would like this to return True
False

So I want to get True if one variable is truly smaller than the other, or if one variable is missing (takes any particular pre-decided non-numerical value), or if both variables are missing, and I want to be able to string the comparisons together one go i.e. a < b < c < ...
I would also like to do this with <= as well as <.
Thanks

Anto
  • 6,806
  • 8
  • 43
  • 65
ChrisW
  • 1,265
  • 12
  • 12
  • I don't think that you can do it reliably with comparisons - you'd obtain a value that compares greater than anything and lesser than anything at the same time. I would instead define a function that takes an iterable of values and returns True if the non-None values are ordered. – Kos Sep 04 '14 at 12:29
  • 6
    if a is 3, b is None, and c is 1, should `a < b < c` return True, even though a is not less than c? – Kevin Sep 04 '14 at 12:30
  • 1
    And Python 3 rejects `a < b < c` altogether if a `None` value is used. – Martijn Pieters Sep 04 '14 at 12:31
  • @Kevin yes indeed, sorry should have made that explicit. As the accepted answer correctly interpreted I essentially want to exclude the undefined values and only compare what's left. – ChrisW Sep 04 '14 at 15:24

6 Answers6

5

You want to test if your sequence – bar the undefined values – is in ascending order:

import operator

def isAscending(strictly, *seq):
    cmp_op = operator.lt if strictly else operator.le 
    seq = [e for e in seq if e is not None]
    return all(cmp_op(a, b) for a, b in zip(seq, seq[1:]))

a, b, c = 1, None, 2
print isAscending(True, a, b, c) # strictly ascending ?

Edited for spelling, and to use comparison operators as suggested.

jtlz2
  • 7,700
  • 9
  • 64
  • 114
sebdelsol
  • 1,045
  • 8
  • 15
  • 1
    You could also pass the comparison to use as another parameter, e.g. `operator.lt` or as a lambda. – tobias_k Sep 04 '14 at 13:53
  • 1
    Spelling nitpick: that should be "strictly". – Kevin Sep 04 '14 at 15:32
  • It’s generally poor design to write a function in which one of the arguments is almost always going to be a hardcoded constant. `isAscending` should have been two functions: `is_ascending` and `is_strictly_ascending`. – user3840170 Mar 13 '21 at 20:32
2

This looks like you are actually trying to test if your values are unique and in sorted order which could be replaced by something like:

>>> def my_test(l):
>>>     filt_l = [v for v in l if not v is None]
>>>     return (sorted(filt_l) == filt_l) and (len(filt_l) == len(set(filt_l)))

>>> my_test([1,2,3])
True 
>>> my_test([1,None,3])
True 
>>> my_test([1,4,3])
False
>>> my_test([1,1,3])
False

Edit: including timings it seems that the function suggested by sebdelsol is even faster

>>> %timeit isAscending([int(1000*random.random()) for i in xrange(10000)])
100 loops, best of 3: 3.44 ms per loop

>>> %timeit my_test([int(1000*random.random()) for i in xrange(10000)])
100 loops, best of 3: 5.67 ms per loop
greole
  • 4,523
  • 5
  • 29
  • 49
  • What would happen if you wrote `my_test([1,0,3])`? ;) Change it to `v for v in l if not v is None` – flakes Sep 04 '14 at 12:56
  • 1
    also consider changing `my_test(l)` to `my_test(*l)`. This way you don't have to explicitly cast the parameters in a container. ie) you can be closer to OP's request `my_test(a, b, c)` – flakes Sep 04 '14 at 12:59
1

You could create your own type overloading comparison methods (as in this question: python overloading operators)

E.g.

class N(object):
    def __init__(self, value):
        self.value = value

    def __lt__(self, other):
        return (self.value is None or self.value < other.value)
    ...


a = N(1); b = N(None); c = N(3)
print a < b < c
Community
  • 1
  • 1
Don
  • 16,928
  • 12
  • 63
  • 101
  • does not work even with your example ... ogic only checs none in 1st value. change __th__ to: return (self.value is None or self.value < other.value or other.value is None)... not even that works as the True propagates incorrectly – Joop Sep 04 '14 at 13:06
  • I was thinking of suggesting this, too, but it's not very usable. Perhaps it would be slightly less unattractive if you didn't have to cast each number to this class but all the workarounds I can think of are more complex than some of the other answers. So on balance, +0. – tripleee Sep 04 '14 at 14:02
1

If you have your values in a list ([a, b, c]), then you can filter the None values from it, pair them up using zip(), apply the operator to all the pairs and see if all them hold.

In code:

import operator  # For operator.lt, which is < ("less than")

def mass_comparify(op, *args):
    return all(op(a, b) for a, b in zip(args, args[1:])
               if a is not None and b is not None)

print(mass_comparify(operator.lt, 1, None, 3))  # Prints True because 1 < 3
RemcoGerlich
  • 30,470
  • 6
  • 61
  • 79
0

I don't think you have a better option than to define a comparison function which does the comparison as you want, and then write the inequalities as

comp(a,b) and comp(b,c) and ...
Joel Cornett
  • 24,192
  • 9
  • 66
  • 88
HamHamJ
  • 435
  • 2
  • 10
0

I don't know if it fits perfectly, but you can use reduce:

>>> import operator
>>> reduce(operator.__lt__, [1, None, 3])
True
>>> reduce(operator.__lt__, [1, None, 0])
False

or, much more solid, as it explicitly ignores None values:

>>> import operator
>>> reduce(operator.__lt__, filter(None, [1, None, 3]))
True
>>> reduce(operator.__lt__, filter(None, [1, None, 0]))
False
Don
  • 16,928
  • 12
  • 63
  • 101