2009-11-11 16 views
69

Quiero escribir una función similar a cmp que compara dos números de versión y devuelve -1, 0 o 1 en función de sus valos comparados.Comparación del número de versión en Python

  • Volver -1 si la versión A es anterior a la versión B
  • Volver 0 si la versión A y B son equivalentes
  • Volver 1 si la versión A es más reciente que la versión B

Cada subsección es se supone que debe interpretarse como un número, por lo tanto 1.10> 1.1.

salidas de función deseados son

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

Y aquí está mi aplicación, abierto a mejoras:

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 

estoy usando Python 2.4.5 cierto. (instalado en mi lugar de trabajo ...).

He aquí una pequeña 'banco de pruebas' se puede utilizar

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

No es una respuesta sino una sugerencia - podría valer la pena implementar el algoritmo de Debian para la versión de la comparación de número (básicamente, alternando la clasificación de no partes numéricas y numéricas). El algoritmo se describe [aquí] (http://www.debian.org/doc/debian-policy/ch-controlfields.html) (comenzando en "Las cuerdas se comparan de izquierda a derecha"). – hobbs

+0

Blargh. El subconjunto de rebajas admitido en los comentarios nunca deja de confundirme. El enlace funciona de todos modos, incluso si parece estúpido. – hobbs

+0

En caso de que los futuros lectores lo necesiten para el análisis sintáctico de la versión del agente de usuario, recomiendo una [biblioteca dedicada] (http://stackoverflow.com/questions/927552/parsing-http-user-agent-string/10109978#10109978) como el variación histórica es demasiado amplia. –

Respuesta

30

Quite la parte no interesante de la cuerda (puntos y ceros finales) y luego compare las listas de números.

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: mismo enfoque que Pär Wieslander, pero un poco más compacto.

algunas pruebas, gracias a 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

Me temo que no va a funcionar, la 'rstrip (". 0 ")' cambiará ".10" a ".1" en "1.0.10". – RedGlyph

+0

Lo siento, pero con su función: mycmp ('1.1', '1.10') == 0 –

+0

Heh. Eso es lo que obtengo para confiar en las pruebas :) Reparar inminente ... – gnud

12

No hay necesidad de iterar sobre las tuplas de versión. El operador de comparación integrado en listas y tuplas ya funciona exactamente como usted lo desea. Simplemente deberá extender cero las listas de versión a la longitud correspondiente. Con Python 2.6 puedes usar izip_longest para rellenar las secuencias.

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) 

Con versiones inferiores, se requiere una hacker de mapa.

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

Genial, pero difícil de entender para alguien que no puede leer el código como la prosa. :) Bueno, supongo que solo puedes acortar la solución a costa de la legibilidad ... –

9

Esto es un poco más compacto que su sugerencia. En lugar de llenar la versión más corta con ceros, eliminaré los ceros finales de las listas de versiones después de dividirlos.

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

Bueno, gracias. Pero todavía estoy esperando un uno o dos líneas ...;) –

+4

+1 @jellybean: dos líneas no siempre son las mejores para el mantenimiento y la legibilidad, este es un código muy claro y compacto al mismo tiempo, además, puede volver a utilizar 'mycmp' para otros fines en su código si lo necesita. – RedGlyph

+0

@RedGlyph: Tienes un punto ahí. Debería haber dicho "un dos líneas legible". :) –

6

Quitar detrás de 0,0 y 0,00 con expresiones regulares, dividir y utilizar la función cmp que compara las matrices correctamente.

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) 

y por supuesto se puede convertir a una sola línea, si no le importa que las largas colas

+0

Agradable y legible a pesar de la expresión regular ... Me gusta su solución. –

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('.')])))) 

Es un chiste (dividida para legability). No estoy seguro de que sea legible ...

+0

¡Sí! Y encogido aún más ('tuple' no es necesario por cierto): ' cmp (* zip (* map (lambda x, y: (x o 0, y o 0), map (int, v1.split ('.')), map (int, v2.split ('.'))))) ' – Paul

28

¿Es reutilizar considerado elegancia en este caso? :)

# 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, no es tan elegante cuando te refieres a algo [fuera de la biblioteca estándar] (http://www.python.org/dev/peps/pep-0365 /) sin explicar dónde conseguirlo. Envié una edición para incluir la URL. Personalmente, prefiero usar distutils; no parece que valga la pena el esfuerzo de instalar software de terceros para una tarea tan simple. –

+1

@ adam-spiers _wut? _ ¿Leíste el comentario? 'pkg_resources' es un paquete' setuptools'-bundled. Como 'setuptools' es efectivamente obligatorio en todas las instalaciones de Python,' pkg_resources' está efectivamente disponible en todas partes. Dicho esto, el subpaquete 'distutils.version' también es útil, aunque considerablemente menos inteligente que la función' pkg_resources.parse_version() 'de nivel superior. Lo que debe aprovechar depende del grado de locura que espera en las cadenas de versión. –

+0

@CecilCurry Sí, por supuesto, leí el comentario (ary), que es por lo que lo edité para hacerlo mejor, y luego afirmé que sí. Es de suponer que no está en desacuerdo con mi afirmación de que 'setuptools' está fuera de la biblioteca estándar, y en su lugar con mi preferencia declarada por' distutils' * en este caso *. Entonces, ¿qué quiere decir exactamente con "efectivamente obligatorio", y por favor ¿puede proporcionar evidencia de que fue "efectivamente obligatorio" hace 4.5 años cuando escribí este comentario? –

1

¡La solución más difícil de leer, pero de una sola línea! y usar iteradores para ser rápido.

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) 

que es para Python2.6 y 3. + por cierto, Python 2.5 y mayores necesitan para coger el StopIteration.

-1

Mi solución preferida:

Relleno la cadena con ceros adicionales y simplemente usando los cuatro primeros es fácil de entender, no requiere ninguna expresión regular y el lambda es más o menos legible. Utilizo dos líneas para la legibilidad, para mí la elegancia es corta y 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

¿Qué le parece usar Python's distutils.version.StrictVersion?

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

Así que para su función cmp:

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

Si desea comparar los números de versión que son más complejos distutils.version.LooseVersion será más útil, sin embargo estar seguro sólo para comparar los mismos tipos.

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

LooseVersion no es la herramienta más inteligente, y puede ser fácilmente engañado:

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

Para tener éxito con esta raza, tendrá que dar un paso fuera de la biblioteca estándar y utilizar distribute ' s utilidad de análisis parse_version.

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

Así que dependiendo de su caso de uso específico, tendrá que decidir si la orden interna distutils herramientas son suficientes, o si se justifica para añadir una dependencia distribute.

+2

parece tener más sentido simplemente usar lo que ya está allí :) –

+1

¡Agradable! ¿Lo resolviste leyendo la fuente? No puedo encontrar documentos para distutils.version en ningún lado: -/ –

+2

No creo que haya ningún documento. Sí, estuve leyendo la fuente hace un tiempo cuando estaba pensando en escribir mi propia solución de empaquetado, pero luego encontré distutils2. –

-1

Esta es mi solución (escrita en C, lo siento). Espero que les sea útil

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 por ser pitónico – Tom

2

listas son comparables en Python, por lo que si uno convierte las cadenas que representan los números en números enteros, la comparación básica pitón puede ser utilizado con éxito.

sin embargo que necesitaba para extender un poco este enfoque, primero porque yo utilizo python3x donde cmp función no existe ninguna -más que tuvo que emular cmp (a, b) con (a> b) - (a < b).

En segundo lugar, por desgracia, los números de versión no son para nada limpios, pueden contener todo tipo de otros caracteres alfanuméricos. Hay casos en que la función no puede indicar el orden, por lo que el resultado es False (vea el primer ejemplo).

Publicando esto incluso si la pregunta es antigua y ya se respondió, pero puede ahorrar unos minutos de la vida de uno.

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

En caso de que no desee para tirar en una dependencia externa que aquí es un intento mío (escrito para 3.x pitón). "rc", "rel" (y posiblemente uno podría agregar "c") se consideran como "candidato de lanzamiento" y dividen el número de versión en dos partes y si falta el valor de la segunda parte es alto (999). Otras letras producen una división y se tratan como subnúmeros a través del código 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 

implementar para php version_compare, excepto "=". Porque es ambiguo

0

Otra solución:

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

Uno puede usar como esto también:

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

hizo esto con el fin de ser capaz de analizar y comparar la cadena de versión de paquetes de Debian. Tenga en cuenta que no es estricto con la validación del personaje.

Esto podría ser útil también.

#!/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

estoy usando éste en mi proyecto:

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