web-dev-qa-db-fra.com

Comparaison des numéros de version en Python

Je veux écrire une fonction semblable à cmp qui compare deux numéros de version et renvoie -1, 0, ou 1 en fonction de leurs valeurs comparées.

  • Revenir -1 si la version A est antérieure à la version B
  • Revenir 0 si les versions A et B sont équivalentes
  • Revenir 1 si la version A est plus récente que la version B

Chaque sous-section est censée être interprétée comme un nombre, donc 1.10> 1.1.

Les sorties de fonction souhaitées sont

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
...

Et voici ma mise en œuvre, ouverte à amélioration:

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

J'utilise Python 2.4.5 btw. (Installé sur mon lieu de travail ...).

Voici une petite "suite de tests" que vous pouvez utiliser

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
93
Johannes Charra

Supprimez la partie non intéressante de la chaîne (zéros et points de fin), puis comparez les listes de nombres.

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: même approche que Pär Wieslander, mais un peu plus compact.

Quelques tests, grâce à ce post :

assert mycmp("1", "1") == 0
assert mycmp("2.1", "2.2") < 0
assert mycmp("3.0.4.10", "3.0.4.2") > 0
assert mycmp("4.08", "4.08.01") < 0
assert mycmp("3.2.1.9.8144", "3.2") > 0
assert mycmp("3.2", "3.2.1.9.8144") < 0
assert mycmp("1.2", "2.1") < 0
assert mycmp("2.1", "1.2") > 0
assert mycmp("5.6.7", "5.6.7") == 0
assert mycmp("1.01.1", "1.1.1") == 0
assert mycmp("1.1.1", "1.01.1") == 0
assert mycmp("1", "1.0") == 0
assert mycmp("1.0", "1") == 0
assert mycmp("1.0", "1.0.1") < 0
assert mycmp("1.0.1", "1.0") > 0
assert mycmp("1.0.2.0", "1.0.2") == 0
35
gnud

Que diriez-vous d'utiliser Python distutils.version.StrictVersion?

>>> from distutils.version import StrictVersion
>>> StrictVersion('10.4.10') > StrictVersion('10.4.9')
True

Donc pour votre fonction cmp:

>>> cmp = lambda x, y: StrictVersion(x).__cmp__(y)
>>> cmp("10.4.10", "10.4.11")
-1

Si vous souhaitez comparer des numéros de version plus complexes distutils.version.LooseVersion sera plus utile, mais assurez-vous de ne comparer que les mêmes types.

>>> from distutils.version import LooseVersion, StrictVersion
>>> LooseVersion('1.4c3') > LooseVersion('1.3')
True
>>> LooseVersion('1.4c3') > StrictVersion('1.3')  # different types
False

LooseVersion n'est pas l'outil le plus intelligent et peut facilement être trompé:

>>> LooseVersion('1.4') > LooseVersion('1.4-rc1')
False

Pour réussir avec cette race, vous devrez sortir de la bibliothèque standard et utiliser l'utilitaire d'analyse de setuptoolsparse_version .

>>> from pkg_resources import parse_version
>>> parse_version('1.4') > parse_version('1.4-rc2')
True

Ainsi, selon votre cas d'utilisation spécifique, vous devrez décider si les outils intégrés distutils sont suffisants, ou s'il est justifié d'ajouter en tant que dépendance setuptools.

267
bradley.ayers

réutilisation est-il considéré comme de l'élégance dans ce cas? :)

# pkg_resources is in setuptools
# See http://peak.telecommunity.com/DevCenter/PkgResources#parsing-utilities
def mycmp(a, b):
    from pkg_resources import parse_version as V
    return cmp(V(a),V(b))
30
conny

Pas besoin d'itérer sur les tuples de version. L'opérateur de comparaison intégré sur les listes et les tuples fonctionne déjà exactement comme vous le souhaitez. Vous n'aurez qu'à étendre les listes de versions à la longueur correspondante. Avec python 2.6 vous pouvez utiliser izip_longest pour remplir les séquences.

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)

Avec les versions inférieures, un piratage de carte est requis.

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)
12
Ants Aasma

C'est un peu plus compact que votre suggestion. Plutôt que de remplir la version courte de zéros, je supprime les zéros de fin des listes de versions après la séparation.

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))
10
Pär Wieslander

Supprimez les .0 et .00 de fin avec regex, divisez et utilisez la fonction cmp qui compare correctement les tableaux.

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)

et bien sûr, vous pouvez le convertir en un seul revêtement si cela ne vous dérange pas les longues files d'attente

6
yu_sha

Les listes sont comparables en python, donc si l'on convertit les chaînes représentant les nombres en entiers, la comparaison de base python peut être utilisée avec succès.

J'avais cependant besoin d'étendre un peu cette approche, d'abord parce que j'utilise python3x où cmp la fonction n'existe plus, j'ai dû émuler cmp (a, b) avec - (a> b) - (a <b).

Deuxièmement, malheureusement, les numéros de version ne sont pas du tout propres, peuvent contenir toutes sortes d'autres caractères alphanumériques. Il y a des cas où la fonction ne peut pas dire l'ordre alors retournez False (voir le premier exemple).

Donc, affichez cela même si la question est ancienne et a déjà répondu, mais peut sauver quelques minutes de votre vie.

import re

def _preprocess(v, separator, ignorecase):
    if ignorecase: v = v.lower()
    return [int(x) if x.isdigit() else [int(y) if y.isdigit() else y for y in re.findall("\d+|[a-zA-Z]+", x)] for x in v.split(separator)]

def compare(a, b, separator = '.', ignorecase = True):
    a = _preprocess(a, separator, ignorecase)
    b = _preprocess(b, separator, ignorecase)
    try:
        return (a > b) - (a < b)
    except:
        return False

print(compare('1.0', 'beta13'))    
print(compare('1.1.2', '1.1.2'))
print(compare('1.2.2', '1.1.2'))
print(compare('1.1.beta1', '1.1.beta2'))
2
sanyi
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('.')]))))

C'est une doublure (divisée pour plus de lisibilité). Pas sûr de lisible ...

2
mavnn
from distutils.version import StrictVersion
def version_compare(v1, v2, op=None):
    _map = {
        '<': [-1],
        'lt': [-1],
        '<=': [-1, 0],
        'le': [-1, 0],
        '>': [1],
        'gt': [1],
        '>=': [1, 0],
        'ge': [1, 0],
        '==': [0],
        'eq': [0],
        '!=': [-1, 1],
        'ne': [-1, 1],
        '<>': [-1, 1]
    }
    v1 = StrictVersion(v1)
    v2 = StrictVersion(v2)
    result = cmp(v1, v2)
    if op:
        assert op in _map.keys()
        return result in _map[op]
    return result

Implémenter pour php version_compare, sauf "=". Parce que c'est ambigu.

2
heronotears

Dans le cas où vous ne voulez pas insérer une dépendance externe, voici une de mes tentatives (écrite pour python 3.x). "Rc", "rel" (et peut-être on pourrait ajouter "c") sont considérés comme "release candidate" et divisent le numéro de version en deux parties et s'il manque la valeur de la deuxième partie est élevée (999). Les autres lettres produisent une division et sont traitées comme des sous-nombres via le code base 36 .


    import re
    from itertools import chain
    def compare_version(version1,version2):
        '''compares two version numbers
        >>> compare_version('1', '2') >> compare_version('2', '1') > 0
        True
        >>> compare_version('1', '1') == 0
        True
        >>> compare_version('1.0', '1') == 0
        True
        >>> compare_version('1', '1.000') == 0
        True
        >>> compare_version('12.01', '12.1') == 0
        True
        >>> compare_version('13.0.1', '13.00.02') >> compare_version('1.1.1.1', '1.1.1.1') == 0
        True
        >>> compare_version('1.1.1.2', '1.1.1.1') >0
        True
        >>> compare_version('1.1.3', '1.1.3.000') == 0
        True
        >>> compare_version('3.1.1.0', '3.1.2.10') >> compare_version('1.1', '1.10') >> compare_version('1.1.2','1.1.2') == 0
        True
        >>> compare_version('1.1.2','1.1.1') > 0
        True
        >>> compare_version('1.2','1.1.1') > 0
        True
        >>> compare_version('1.1.1-rc2','1.1.1-rc1') > 0
        True
        >>> compare_version('1.1.1a-rc2','1.1.1a-rc1') > 0
        True
        >>> compare_version('1.1.10-rc1','1.1.1a-rc2') > 0
        True
        >>> compare_version('1.1.1a-rc2','1.1.2-rc1') >> compare_version('1.11','1.10.9') > 0
        True
        >>> compare_version('1.4','1.4-rc1') > 0
        True
        >>> compare_version('1.4c3','1.3') > 0
        True
        >>> compare_version('2.8.7rel.2','2.8.7rel.1') > 0
        True
        >>> compare_version('2.8.7.1rel.2','2.8.7rel.1') > 0
        True

        '''
        chn = lambda x:chain.from_iterable(x)
        def split_chrs(strings,chars):
            for ch in chars:
                strings = chn( [e.split(ch) for e in strings] )
            return strings
        split_digit_char=lambda x:[s for s in re.split(r'([a-zA-Z]+)',x) if len(s)>0]
        splt = lambda x:[split_digit_char(y) for y in split_chrs([x],'.-_')]
        def pad(c1,c2,f='0'):
            while len(c1) > len(c2): c2+=[f]
            while len(c2) > len(c1): c1+=[f]
        def base_code(ints,base):
            res=0
            for i in ints:
                res=base*res+i
            return res
        ABS = lambda lst: [abs(x) for x in lst]
        def cmp(v1,v2):
            c1 = splt(v1)
            c2 = splt(v2)
            pad(c1,c2,['0'])
            for i in range(len(c1)): pad(c1[i],c2[i])
            cc1 = [int(c,36) for c in chn(c1)]
            cc2 = [int(c,36) for c in chn(c2)]
            maxint = max(ABS(cc1+cc2))+1
            return base_code(cc1,maxint) - base_code(cc2,maxint)
        v_main_1, v_sub_1 = version1,'999'
        v_main_2, v_sub_2 = version2,'999'
        try:
            v_main_1, v_sub_1 = Tuple(re.split('rel|rc',version1))
        except:
            pass
        try:
            v_main_2, v_sub_2 = Tuple(re.split('rel|rc',version2))
        except:
            pass
        cmp_res=[cmp(v_main_1,v_main_2),cmp(v_sub_1,v_sub_2)]
        res = base_code(cmp_res,max(ABS(cmp_res))+1)
        return res


    import random
    from functools import cmp_to_key
    random.shuffle(versions)
    versions.sort(key=cmp_to_key(compare_version))
2
Roland Puntaier

La solution la plus difficile à lire, mais une doublure quand même! et utiliser des itérateurs pour être rapide.

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)

c'est-à-dire pour Python2.6 et 3. + btw, Python 2.5 et plus doivent capturer le StopIteration.

1
Paul

Une autre solution:

def mycmp(v1, v2):
    import itertools as it
    f = lambda v: list(it.dropwhile(lambda x: x == 0, map(int, v.split('.'))[::-1]))[::-1]
    return cmp(f(v1), f(v2))

On peut aussi utiliser comme ça:

import itertools as it
f = lambda v: list(it.dropwhile(lambda x: x == 0, map(int, v.split('.'))[::-1]))[::-1]
f(v1) <  f(v2)
f(v1) == f(v2)
f(v1) >  f(v2)
0
pedrormjunior

A fait cela afin de pouvoir analyser et comparer la chaîne de version du paquet debian. Veuillez noter qu'il n'est pas strict avec la validation des caractères.

Cela pourrait également être utile.

#!/usr/bin/env python

# Read <https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version> for further informations.

class CommonVersion(object):
    def __init__(self, version_string):
        self.version_string = version_string
        self.tags = []
        self.parse()

    def parse(self):
        parts = self.version_string.split('~')
        self.version_string = parts[0]
        if len(parts) > 1:
            self.tags = parts[1:]


    def __lt__(self, other):
        if self.version_string < other.version_string:
            return True
        for index, tag in enumerate(self.tags):
            if index not in other.tags:
                return True
            if self.tags[index] < other.tags[index]:
                return True

    @staticmethod
    def create(version_string):
        return UpstreamVersion(version_string)

class UpstreamVersion(CommonVersion):
    pass

class DebianMaintainerVersion(CommonVersion):
    pass

class CompoundDebianVersion(object):
    def __init__(self, Epoch, upstream_version, debian_version):
        self.Epoch = Epoch
        self.upstream_version = UpstreamVersion.create(upstream_version)
        self.debian_version = DebianMaintainerVersion.create(debian_version)

    @staticmethod
    def create(version_string):
        version_string = version_string.strip()
        Epoch = 0
        upstream_version = None
        debian_version = '0'

        Epoch_check = version_string.split(':')
        if Epoch_check[0].isdigit():
            Epoch = int(Epoch_check[0])
            version_string = ':'.join(Epoch_check[1:])
        debian_version_check = version_string.split('-')
        if len(debian_version_check) > 1:
            debian_version = debian_version_check[-1]
            version_string = '-'.join(debian_version_check[0:-1])

        upstream_version = version_string

        return CompoundDebianVersion(Epoch, upstream_version, debian_version)

    def __repr__(self):
        return '{} {}'.format(self.__class__.__name__, vars(self))

    def __lt__(self, other):
        if self.Epoch < other.Epoch:
            return True
        if self.upstream_version < other.upstream_version:
            return True
        if self.debian_version < other.debian_version:
            return True
        return False


if __== '__main__':
    def lt(a, b):
        assert(CompoundDebianVersion.create(a) < CompoundDebianVersion.create(b))

    # test Epoch
    lt('1:44.5.6', '2:44.5.6')
    lt('1:44.5.6', '1:44.5.7')
    lt('1:44.5.6', '1:44.5.7')
    lt('1:44.5.6', '2:44.5.6')
    lt('  44.5.6', '1:44.5.6')

    # test upstream version (plus tags)
    lt('1.2.3~rc7',          '1.2.3')
    lt('1.2.3~rc1',          '1.2.3~rc2')
    lt('1.2.3~rc1~nightly1', '1.2.3~rc1')
    lt('1.2.3~rc1~nightly2', '1.2.3~rc1')
    lt('1.2.3~rc1~nightly1', '1.2.3~rc1~nightly2')
    lt('1.2.3~rc1~nightly1', '1.2.3~rc2~nightly1')

    # test debian maintainer version
    lt('44.5.6-lts1', '44.5.6-lts12')
    lt('44.5.6-lts1', '44.5.7-lts1')
    lt('44.5.6-lts1', '44.5.7-lts2')
    lt('44.5.6-lts1', '44.5.6-lts2')
    lt('44.5.6-lts1', '44.5.6-lts2')
    lt('44.5.6',      '44.5.6-lts1')
0
Pius Raeder

j'utilise celui-ci sur mon projet:

cmp(v1.split("."), v2.split(".")) >= 0
0
Keyrr Perino