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.
-1
si la version A est antérieure à la version B0
si les versions A et B sont équivalentes1
si la version A est plus récente que la version BChaque 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
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
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
.
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))
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)
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))
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
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'))
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 ...
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.
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))
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.
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)
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')
j'utilise celui-ci sur mon projet:
cmp(v1.split("."), v2.split(".")) >= 0