views:

306

answers:

8

Python must have a more elegant solution to this ... maybe one of you can help:

I want to write a cmp-like function returning -1, 0 or 1 for version numbers, allowing for an arbitrary number of subsections.

Each subsection is supposed to be interpreted as a number, therefore 1.10 > 1.1.

Desired function outputs are

mycmp('1.0', '1') == 0
mycmp('1.0.0', '1') == 0
mycmp('1', '1.0.0.1') == -1
mycmp('12.10', '11.0.0.0.0') == 1
...

And here is my implementation, open for improvement:

def mycmp(version1, version2):
    parts1 = [int(x) for x in version1.split('.')]
    parts2 = [int(x) for x in version2.split('.')]

    # fill up the shorter version with zeros ...
    lendiff = len(parts1) - len(parts2)
    if lendiff > 0:
        parts2.extend([0] * lendiff)
    elif lendiff < 0:
        parts1.extend([0] * (-lendiff))

    for i, p in enumerate(parts1):
        ret = cmp(p, parts2[i])
        if ret: return ret
    return 0

I'm using Python 2.4.5 btw. (installed at my working place ...).

Here's a small 'test suite' you can use

assert mycmp('1', '2') == -1
assert mycmp('2', '1') == 1
assert mycmp('1', '1') == 0
assert mycmp('1.0', '1') == 0
assert mycmp('1', '1.000') == 0
assert mycmp('12.01', '12.1') == 0
assert mycmp('13.0.1', '13.00.02') == -1
assert mycmp('1.1.1.1', '1.1.1.1') == 0
assert mycmp('1.1.1.2', '1.1.1.1') == 1
assert mycmp('1.1.3', '1.1.3.000') == 0
assert mycmp('3.1.1.0', '3.1.2.10') == -1
assert mycmp('1.1', '1.10') == -1
+5  A: 

No need to iterate over the version tuples. The built in comparison operator on lists and tuples already works exactly like you want it. You'll just need to zero extend the version lists to the corresponding length. With python 2.6 you can use izip_longest to pad the sequences.

from itertools import izip_longest
def version_cmp(v1, v2):
    parts1, parts2 = [map(int, v.split('.')) for v in [v1, v2]]
    parts1, parts2 = zip(*izip_longest(parts1, parts2, fillvalue=0))
    return cmp(parts1, parts2)

With lower versions, some map hackery is required.

def version_cmp(v1, v2):
    parts1, parts2 = [map(int, v.split('.')) for v in [v1, v2]]
    parts1, parts2 = zip(*map(lambda p1,p2: (p1 or 0, p2 or 0), parts1, parts2))
    return cmp(parts1, parts2)
Ants Aasma
Cool, but hard to understand for someone who can't read code like prose. :) Well, I assume you can only shorten the solution at the cost of readability ...
jellybean
+4  A: 

This is a little more compact than your suggestion. Rather than filling the shorter version with zeros, I'm removing trailing zeros from the version lists after splitting.

def normalize_version(v):
    parts = [int(x) for x in v.split(".")]
    while parts[-1] == 0:
        parts.pop()
    return parts

def mycmp(v1, v2):
    return cmp(normalize_version(v1), normalize_version(v2))
Pär Wieslander
Nice one, thx. But I'm still hoping for a one or two-liner ... ;)
jellybean
+1 @jellybean: two-liners are not always the best for maintenance and readability, this one is very clear and compact code at the same time, besides, you can re-use `mycmp` for other purposes in your code should you need it.
RedGlyph
@RedGlyph: You've got a point there. Should have said "a readable two-liner". :)
jellybean
+3  A: 

Remove the uninteresting part of the string (trailing zeroes and dots), and then compare the lists of numbers.

import re

def mycmp(version1, version2):
    def normalize(v):
        return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
    return cmp(normalize(version1), normalize(version2))

EDIT: same approach as Pär Wieslander, but a bit more compact.

gnud
I'm afraid it won't work, the `rstrip(".0")` will change ".10" to ".1" in "1.0.10".
RedGlyph
Sorry, but with your function:mycmp('1.1', '1.10') == 0
jellybean
Heh. Thats what I get for trusting tests :) Fix imminent...
gnud
Lol ... yeah, thx and +1 for finding the missing test.
jellybean
With the regex use, the problem mentioned above is fixed.
gnud
Now you've merged all the good ideas from the others into your solution ... :-P still, this is pretty much what I'd do after all. I'll accept this answer. Thanks, everyone
jellybean
Heh. First, Pär answered while I was testing my solution - and when I switched to regexes, I didn't read the new answers - I'll go upvote the ones I "copied" now :)
gnud
+3  A: 

Remove trailing .0 and .00 with regex, split and use cmp function which compares arrays correctly.

def mycmp(v1,v2):
 c1=map(int,re.sub('(\.0+)+\Z','',v1).split('.'))
 c2=map(int,re.sub('(\.0+)+\Z','',v2).split('.'))
 return cmp(c1,c2)

and of course you can convert it to a one-liner if you don't mind the long lines

yu_sha
Nice, and readable despite regex ... I like your solution.
jellybean
+1  A: 
def compare_version(v1, v2):
    return cmp(*tuple(zip(*map(lambda x, y: (x or 0, y or 0), 
           [int(x) for x in v1.split('.')], [int(y) for y in v2.split('.')]))))

It's a one liner (split for legability). Not sure about readable...

mavnn
Yes! And shrunk even further (`tuple` is not needed btw):`cmp(*zip(*map(lambda x,y:(x or 0,y or 0), map(int,v1.split('.')), map(int,v2.split('.')) )))`
Paul
+5  A: 

Is reuse considered elegance in this instance? :)

# pkg_resources is in setup_tools
def mycmp(a, b):
    from pkg_resources import parse_version as V
    return cmp(V(a),V(b))
conny
This had to happen ... :D
jellybean
A: 

The most difficult to read solution, but a one-liner nevertheless! and using iterators to be fast.

next((c for c in imap(lambda x,y:cmp(int(x or 0),int(y or 0)),
            v1.split('.'),v2.split('.')) if c), 0)

that is for Python2.6 and 3.+ btw, Python 2.5 and older need to catch the StopIteration.

Paul
A: 

My preferred solution:

Padding the string with extra zeroes and just using the four first is easy to understand, doesn't require any regex and the lambda is more or less readable. I use two lines for readability, for me elegance is short and simple.

def mycmp(version1,version2):
  tup = lambda x: [int(y) for y in (x+'.0.0.0.0').split('.')][:4]
  return cmp(tup(version1),tup(version2))
daramarak