2009-11-11 5 views
69

Я хочу написать cmp-подобную функцию, которая сравнивает два номера версий и возвращает -1, 0 или 1 на основе их сравниваемых значений.Сравнение номеров версий в Python

  • Возврат -1 если версия старше версии B
  • Возврат 0 если версия А и В эквивалентны
  • Return 1 если версия новее, чем версия B

Каждый подраздел является предполагается интерпретироваться как число, поэтому 1.10> 1.1.

искомая функция выходов

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

А вот моя реализация, открыта для улучшения:

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 

Я использую Python 2.4.5 BTW. (установлен на моем рабочем месте ...).

Вот небольшой «набор тестов» вы можете использовать

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

не ответ, но предложение - это могло бы стоить реализации алгоритма в Debian для номера версии сравнения (в основном, чередуя сортировка не- числовые и числовые части). Алгоритм описан [здесь] (http://www.debian.org/doc/debian-policy/ch-controlfields.html) (начиная с «Строки сравниваются слева направо»). – hobbs

+0

Blargh. Подмножество уценки, поддержанное в комментариях, никогда не перестает меня путать. Ссылка работает в любом случае, даже если это выглядит глупо. – hobbs

+0

В случае, если будущие читатели нуждаются в этом для разбора версии агента пользователя, я рекомендую [выделенную библиотеку] (http://stackoverflow.com/questions/927552/parsing-http-user-agent-string/10109978#10109978) в качестве историческая вариация слишком широка. –

ответ

30

Удалите неинтересную часть строки (замыкающие нули и точки), а затем сравните списки номеров.

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: такой же подход, как Pär Wieslander, но немного более компактный.

Некоторые тесты, благодаря 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

Боюсь, что это не сработает, 'rstrip (". 0 ")' изменится ".10" до ".1" в "1.0.10". – RedGlyph

+0

Извините, но с вашей функцией: mycmp ('1.1', '1.10') == 0 –

+0

Хех. То, что я получаю для доверчивых тестов :) Fix imminent ... – gnud

12

Нет необходимости перебирать версии кортежей. Встроенный оператор сравнения в списках и кортежах уже работает точно так, как вы этого хотите. Вам просто нужно обнулить расширение списков версий до соответствующей длины. С python 2.6 вы можете использовать izip_longest для заполнения последовательностей.

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) 

С нижними версиями требуется хакерская карта.

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

Прохладный, но трудно понять для тех, кто не умеет читать код, как проза. :) Ну, я полагаю, вы можете сократить только решение за счет удобочитаемости ... –

9

Это немного более компактно, чем ваше предложение. Вместо того, чтобы заполнять более короткую версию нулями, я удаляю конечные нули из списков версий после разделения.

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. Но я все еще надеюсь на один или два лайнера ...;) –

+4

+1 @jellybean: двухстрочные линии не всегда являются лучшими для обслуживания и удобочитаемости, это очень четкий и компактный код в то же время, кроме того, вы можете повторно использовать 'mycmp' для других целей в своем коде, если вам это нужно. – RedGlyph

+0

@RedGlyph: У вас там есть точка. Должен был сказать «читаемый двухслойный». :) –

6

Снимите задний .0 и .00 с регулярным выражением, колотая и использовать функции CMP, который сравнивает массивы правильно.

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) 

и, конечно, вы можете преобразовать его в однострочник, если вы не возражаете, длинные линии

+0

Приятный и читаемый, несмотря на регулярное выражение ... Мне нравится ваше решение. –

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

Это один вкладыш (сплит для legability). Не уверен в читабельности ...

+0

Да! И уменьшилось еще больше ('tuple' не требуется btw): ' cmp (* zip (* map (lambda x, y: (x или 0, y или 0), map (int, v1.split ('.')), map (int, v2.split ('.'))))) ' – Paul

28

повторное использование считается элегантность в данном случае? :)

# 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

Хм, это не так изящно, когда вы обращаетесь к чему-то [вне стандартной библиотеки] (http://www.python.org/dev/peps/pep-0365 /), не объясняя, где его получить. Я отправил изменение для включения URL-адреса. Лично я предпочитаю использовать distutils - это не похоже на усилия по привлечению стороннего программного обеспечения для столь простой задачи. –

+1

@ adam-spires _wut? _ Вы даже прочитали комментарий? 'pkg_resources' является пакетом' setuptools'. Поскольку 'setuptools' является фактически обязательным во всех установках Python,' pkg_resources' эффективно доступен повсюду. Тем не менее, подпакет 'distutils.version' также полезен - хотя и значительно менее интеллектуальный, чем функция' pkg_resources.parse_version() 'более высокого уровня. Который вы должны использовать, зависит от того, какую степень безумия вы ожидаете в версиях. –

+0

@CecilCurry Да, конечно, я прочитал комментарий (ary), поэтому я его отредактировал, чтобы сделать его лучше, а затем заявил, что у меня есть. Предположительно, вы не согласны с моим утверждением, что 'setuptools' находится за пределами стандартной библиотеки, и вместо этого с моим заявленным предпочтением для' distutils' * в этом случае *. Итак, что именно вы подразумеваете под «эффективно обязательным», и, пожалуйста, можете ли вы предоставить доказательства того, что он был «фактически обязательным» 4,5 года назад, когда я написал этот комментарий? –

1

Самое трудное для чтения решение, но однострочный вкладыш тем не менее! и использование итераторов будет быстрым.

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) 

что для python2.6 и 3. + Кстати, Python 2.5 и старше должны поймать StopIteration.

-1

Моего предпочтительным решение:

Padding строки с дополнительными нулями и просто используя четыре первыми легко понять, не требует регулярных выражений и лямбда является более или менее читаемым. Я использую две строки для удобства чтения, для меня элегантность коротка и проста.

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

Как насчет использования Python's distutils.version.StrictVersion?

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

Так для cmp функции:

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

Если вы хотите сравнить номера версий, которые являются более сложными distutils.version.LooseVersion будет более полезным, однако обязательно сравнивать только те же самые типы.

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

LooseVersion не самый умный инструмент, и может быть легко обмануть:

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

Чтобы иметь успех с этой породой, вам нужно выйти за пределы стандартной библиотеки и использовать distribute ' s разборки parse_version.

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

Поэтому в зависимости от конкретного сценария использования, вы должны решить, является ли встроенные distutils инструментов достаточно, или, если это оправданно, чтобы добавить как зависимость distribute.

+2

, похоже, имеет смысл просто использовать то, что уже есть :) –

+1

Nice! Вы поняли это, прочитав источник? Я не могу найти документы для distutils.version в любом месте: -/ –

+2

Я не думаю, что есть какие-либо документы. Да, я читал источник через некоторое время назад, когда я подумывал написать собственное решение для упаковки, но потом нашел distutils2. –

-1

Это мое решение (написано на C, извините). Я надеюсь, что вы найдете его полезным

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 для того, чтобы быть pythonic – Tom

2

Списки сравнимы по питона, так что если один преобразует строки, представляющие числа в для целых чисел, основное сравнение питон может быть с успехом использован.

я однако необходимо расширить немного этот подход, первая причина я использую python3x где CMP функция не существует, больше я должен был подражать CMP (а, б) с (а> Ь) - (a < b).

Во-вторых, к сожалению, номера версий не такие чистые вообще, могут содержать все виды буквенно-цифровых символов. Бывают случаи, когда функция не может определить порядок, так что возвращает False (см. Первый пример).

Так отправляйте это, даже если вопрос старый и уже ответил, но может сэкономить несколько минут от жизни.

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

В случае, если вы не хотите, чтобы тянуть в качестве внешней зависимости здесь является попытка моего (написанный для питон 3.x). «rc», «rel» (и, возможно, можно добавить «c») рассматриваются как «кандидат на выпуск» и делят номер версии на две части, и если отсутствует значение второй части, она является высокой (999). Другие буквы производят раздвоение и обрабатываются в качестве подкатегорий через код базы 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 

Реализовать для PHP version_compare, за исключением "=". Потому что это неоднозначно.

0

Другое решение:

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

можно использовать, как это тоже:

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

ли это для того, чтобы иметь возможность анализировать и сравнивать строку версии пакетов Debian. Обратите внимание, что это не является строгим с валидацией символов.

Это может быть полезно.

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

Я использую этот на моем проекте:

cmp(v1.split("."), v2.split(".")) >= 0 
Смежные вопросы