2009-11-11 10 views
69

Je souhaite écrire une fonction similaire à cmp qui compare deux numéros de version et renvoie -1, 0 ou 1 en fonction de leurs valeurs de comparaison.Comparaison des numéros de version en Python

  • Retour -1 si la version A est plus ancienne que la version B
  • Retour 0 si la version A et B sont équivalentes
  • Retour 1 si la version A est plus récente que la version B

Chaque paragraphe est supposé être interprété comme un nombre, donc 1.10> 1.1.

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, ouvert à l'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é à mon lieu de travail ...).

Voici une petite 'suite de tests', 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 
+0

Pas une réponse, mais une suggestion - il pourrait être utile mise en oeuvre de l'algorithme de Debian pour la comparaison de numéro de version (essentiellement, en alternant le tri des non parties numériques et numériques). L'algorithme est décrit [ici] (http://www.debian.org/doc/debian-policy/ch-controlfields.html) (en commençant par "Les chaînes sont comparées de gauche à droite"). – hobbs

+0

Blargh. Le sous-ensemble de démarque pris en charge dans les commentaires ne manque jamais de me dérouter. Le lien fonctionne quand même, même si ça a l'air stupide. – hobbs

+0

Si les futurs lecteurs en ont besoin pour l'analyse de la version user-agent, je recommande une [bibliothèque dédiée] (http://stackoverflow.com/questions/927552/parsing-http-user-agent-string/10109978#10109978) en tant que variation historique trop large. –

Répondre

30

Retirez la partie sans intérêt de la chaîne (zéros et points de fuite), puis comparer les listes de numéros.

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.

Certains tests, grâce à this 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 
+2

Je crains que cela ne fonctionne pas, le 'rstrip (". 0 ")' va changer ".10" à ".1" dans "1.0.10". – RedGlyph

+0

Désolé, mais avec votre fonction: mycmp ('1.1', '1.10') == 0 –

+0

Heh. C'est ce que j'obtiens pour les tests de confiance :) Fix imminent ... – gnud

12

Pas besoin de parcourir 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 aurez juste besoin de zéro é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) 
+0

Cool, mais difficile à comprendre pour quelqu'un qui ne peut pas lire le code comme la prose. :) Eh bien, je suppose que vous ne pouvez que raccourcir la solution au prix de la lisibilité ... –

9

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

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)) 
+0

Nice one, thx. Mais j'espère encore un ou deux-liner ...;) –

+4

+1 @jellybean: les deux-liners ne sont pas toujours les meilleurs pour la maintenance et la lisibilité, celui-ci est un code très clair et compact en même temps, En outre, vous pouvez réutiliser 'mycmp' à d'autres fins dans votre code si vous en avez besoin. – RedGlyph

+0

@RedGlyph: Vous avez un point là-bas. Aurait dû dire "un doubleur lisible". :) –

6

Supprimer arrière et .0 .00 avec regex, split et utiliser la fonction cmp qui compare les tableaux correctement.

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 une seule ligne si vous ne me dérange pas les longues lignes

+0

Nice, et lisible malgré regex ... J'aime votre solution. –

1
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 (split pour legability). Pas sûr de lire ...

+0

Oui! Et rétréci encore plus loin («tuple» n'est pas nécessaire btw): 'cmp (* zip (* carte (lambda x, y: (x ou 0, y ou 0), carte (int, v1.split ('.')), map (int, v2.split ('.')))) ' – Paul

28

Est-ce que réutilise considérée comme é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)) 
+6

Hmm, ce n'est pas si élégant quand vous faites référence à quelque chose [en dehors de la bibliothèque standard] (http://www.python.org/dev/peps/pep-0365 /) sans expliquer où l'obtenir. J'ai soumis une modification pour inclure l'URL. Personnellement, je préfère utiliser distutils - il ne semble pas valoir la peine de tirer parti de logiciels tiers pour une tâche aussi simple. –

+1

@ adam-spiers _wut? _ Avez-vous lu le commentaire? 'pkg_resources' est un paquetage' setuptools'-bundled. Puisque 'setuptools' est effectivement obligatoire sur toutes les installations Python,' pkg_resources' est effectivement disponible partout. Cela dit, le sous-paquetage 'distutils.version' est également utile - bien que considérablement moins intelligent que la fonction' pkg_resources.parse_version() 'de niveau supérieur. Ce que vous devez exploiter dépend du degré de folie auquel vous vous attendez dans les chaînes de version. –

+0

@CecilCurry Oui, bien sûr, j'ai lu le commentaire (ary), ce qui explique pourquoi je l'ai édité pour le rendre meilleur, puis a déclaré que j'avais. Je présume que vous n'êtes pas en désaccord avec ma déclaration selon laquelle 'setuptools' est en dehors de la bibliothèque standard, et à la place avec ma préférence déclarée pour' distutils' * dans ce cas *. Alors, que voulez-vous dire par «effectivement obligatoire», et pouvez-vous fournir la preuve qu'il était «effectivement obligatoire» il y a 4,5 ans lorsque j'ai écrit ce commentaire? –

1

La solution la plus difficile à lire, mais néanmoins à une seule ligne! et en utilisant les 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) 

qui est pour python2.6 et 3. + BTW, Python 2.5 et plus besoin de prendre le StopIteration.

-1

Ma solution préférée:

formate la chaîne avec des zéros supplémentaires et en utilisant simplement les quatre premiers est facile à comprendre, ne nécessite pas regex et le lambda est plus ou moins lisible. J'utilise deux lignes pour la lisibilité, pour moi l'élégance est courte et 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)) 
217

Comment utiliser Python distutils.version.StrictVersion?

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

Donc, pour votre cmp fonction:

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

Si vous voulez comparer les numéros de version qui sont plus complexes distutils.version.LooseVersion sera plus utile, cependant être sûr 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 dupé:

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

Pour avoir du succès avec cette race, vous aurez besoin de sortir de la bibliothèque standard et utiliser distribute ' s utilitaire d'analyse parse_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 distutils BUILTIN suffisent, ou si elle est justifiée d'ajouter comme une dépendance distribute.

+2

semble faire le plus de sens pour simplement utiliser ce qui est déjà là :) –

+1

Nice! Avez-vous compris cela en lisant la source? Je ne trouve pas de document pour distutils.version n'importe où: -/ –

+2

Je ne pense pas qu'il y ait de docs. Oui, je lisais la source il y a quelque temps lorsque je pensais écrire ma propre solution d'emballage, mais j'ai ensuite trouvé distutils2. –

-1

Ceci est ma solution (écrit en C, désolé). J'espère que vous trouverez utile

int compare_versions(const char *s1, const char *s2) { 
    while(*s1 && *s2) { 
     if(isdigit(*s1) && isdigit(*s2)) { 
      /* compare as two decimal integers */ 
      int s1_i = strtol(s1, &s1, 10); 
      int s2_i = strtol(s2, &s2, 10); 

      if(s1_i != s2_i) return s1_i - s2_i; 
     } else { 
      /* compare as two strings */ 
      while(*s1 && !isdigit(*s1) && *s2 == *s1) { 
       s1++; 
       s2++; 
      } 

      int s1_i = isdigit(*s1) ? 0 : *s1; 
      int s2_i = isdigit(*s2) ? 0 : *s2; 

      if(s1_i != s2_i) return s1_i - s2_i; 
     } 
    } 

    return 0; 
} 
+3

+1 pour être pythonic – Tom

2

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

Je cependant besoin d'étendre un peu cette approche, d'abord parce que je l'utilise python3x où cmp fonction n'existe-plus je devais imiter cmp (a, b) avec (a> b) - (< b). Deuxièmement, malheureusement, les numéros de version ne sont pas si 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 renvoyez False (voir le premier exemple).Donc, en publiant 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

Si vous ne voulez pas tirer dans une dépendance externe ici est une tentative de la mine (écrit pour 3.x python). "rc", "rel" (et éventuellement on pourrait ajouter "c") sont considérés comme "release candidate" et divisent le numéro de version en deux parties et si elles manquent, la valeur de la seconde partie est haute (999). Les autres lettres produisent une division et sont traitées comme des sous-nombres via le code de 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
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 

Mettre en oeuvre pour php version_compare, sauf "=". Parce que c'est ambigu.

0

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 utiliser comme ça aussi:

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

a fait cela afin de pouvoir analyser et comparer la chaîne de version du package debian. S'il vous plaît noter que ce n'est pas stricte avec la validation du personnage.

Cela pourrait aussi ê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 __name__ == '__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

J'utilise celui-ci sur mon projet:

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