Порівняння номерів версії в Python


98

Я хочу написати cmp-like функцію , яка порівнює два номери версії і повертається -1, 0або на 1основі їх порівнянні valuses.

  • Повернутись, -1якщо версія A старша за версію B
  • Повернення, 0якщо версії A і B еквівалентні
  • Поверніться, 1якщо версія A новіша, ніж версія 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

Не відповідь, а пропозиція - можливо, варто застосувати алгоритм Debian для порівняння номерів версій (в основному, чергування сортування нечислових та числових частин). Алгоритм описаний тут (починаючи з «Рядки порівнюються зліва направо»).
варення

Blargh. Підмножина розмітки, підтримувана в коментарях, ніколи не збиває з пантелику мене. Посилання працює в будь-якому випадку, навіть якщо воно виглядає нерозумно.
варення

У разі, якщо майбутнім читачам це знадобиться для розбору версій користувацького агента, я рекомендую виділену бібліотеку, оскільки історична варіація її занадто широка.
Джеймс Броудхед


1
Незважаючи на те, що питання тут старіше, схоже, це інше питання було помазане як канонічне, тому що багато, багато запитань є закритими як дублікати цього.
Джон Y

Відповіді:


36

Видаліть нецікаву частину рядка (кінцеві нулі та крапки), а потім порівняйте списки чисел.

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

Це той самий підхід, що і Pär Wieslander, але трохи компактніший:

Ось кілька тестів, завдяки " Як порівняти два рядки у форматі точок розділеної версії в Bash? ":

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

Вибачте, але з вашою функцією: mycmp ('1.1', '1.10') == 0
Йоханнес Чарра

З використанням регулярного вираження проблема, зазначена вище, виправлена.
gnud

Тепер ви об'єднали всі хороші ідеї з іншими у ваше рішення ... :-П все-таки, це майже все, що я б зробив зрештою. Я прийму цю відповідь. Спасибі всім
Йоганнес Чарра

2
Примітка. Cmp () видалено в Python 3: docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
Dominic Cleal

279

Як щодо використання 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

Щоб досягти успіху з цією породою, вам потрібно буде вийти за межі стандартної бібліотеки і використовувати утиліту розбору setuptoolsparse_version .

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

Отже, залежно від конкретного випадку використання, вам потрібно буде вирішити, чи distutilsдостатньо вбудованих інструментів, чи доцільно їх додати як залежність setuptools.


2
Здається, має найбільш сенс просто використовувати те, що вже є :)
Патрік Вовк

2
Приємно! Ви це зрозуміли, читаючи джерело? Я не можу знайти документів для distutils.version де завгодно: - /
Адам Шпіерс

3
Щоразу, коли ви не можете знайти документацію, спробуйте імпортувати пакунок та скористайтеся довідкою ().
rspeed

13
Будьте в курсі, що StrictVersion ТІЛЬКИ працює з версією до трьох номерів. Це не вдається для таких речей 0.4.3.6!
abergmeier

6
Кожен екземпляр distributeу цій відповіді має бути замінений на той setuptools, який поставляється в комплекті з pkg_resourcesпакетом і з тих пір, як і колись . Так само це офіційна документація для pkg_resources.parse_version()функції, яка входить до комплекту setuptools.
Сесіль Карі

30

Чи повторне використання вважається елегантністю в цьому випадку? :)

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

7
Гм, це не так елегантно, коли ви посилаєтесь на щось поза стандартною бібліотекою, не пояснюючи, де його взяти. Я подав редагування, щоб включити URL-адресу. Особисто я вважаю за краще використовувати distutils - здається, не варто докладати зусиль, щоб залучити стороннє програмне забезпечення для такої простої задачі.
Адам Шпієр

1
@ adam-spiers wut? Ви навіть читали коментар? pkg_resourcesявляє собою setuptoolsпакет-пакет. Оскільки він setuptoolsє обов'язковим для всіх установок Python, pkg_resourcesвін фактично доступний скрізь. Однак, distutils.versionсубпакет також корисний - хоча і значно менш розумний, ніж pkg_resources.parse_version()функція вищого рівня . На який ви можете скористатися, залежить те, який рівень божевілля ви очікуєте в рядках версій.
Сесіль Карі

@CecilCurry Так, звичайно, я прочитав коментар (арі), саме тому я відредагував його, щоб покращити його, а потім заявив, що мав. Імовірно, ви не погоджуєтесь з моїм твердженням, яке setuptoolsзнаходиться поза стандартною бібліотекою, а натомість з моїм заявленим уподобанням distutils у цьому випадку . Отже, що саме ви маєте на увазі під "фактично обов'язковим", і, будь ласка, можете надати докази того, що це було "фактично обов'язково" 4,5 роки тому, коли я писав цей коментар?
Адам Шпієр

12

Не потрібно перебирати кортежі версії. Вбудований оператор порівняння для списків і кортежів вже працює саме так, як вам цього хочеться. Вам просто знадобиться нульове розширення списків версій до відповідної довжини. З python 2.6 ви можете використовувати izip_lo Long для прокладки послідовностей.

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)

Класно, але важко зрозуміти для того, хто не вміє читати код, як проза. :) Ну, я припускаю, що ви можете лише скоротити рішення ціною читабельності ...
Йоханнес Чарра

10

Це трохи компактніше, ніж ваша пропозиція. Замість того, щоб заповнювати коротшу версію нулями, я видаляю нулі із списків версій після розбиття.

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

Хороший, THX. Але я все ще сподіваюся на одно- або
дволінійний

4
+1 @jellybean: два вкладиші не завжди найкращі для обслуговування та читабельності, цей одночасно є дуже чітким і компактним кодом, до того ж, ви можете повторно використовувати його mycmpдля іншого призначення у своєму коді, якщо вам це потрібно.
RedGlyph

@RedGlyph: У вас там є крапка. Повинен був сказати "читабельний двоколірний". :)
Йоханнес Чарра

привіт @ Pär Wieslander, коли я використовую це рішення для вирішення тієї самої проблеми в проблемі Leetcode, я отримую помилку в циклі while, кажучи "список індексу поза діапазоном". Чи можете ви допомогти, чому це відбувається? Ось проблема: leetcode.com/explore/interview/card/amazon/76/array-and-strings/…
YouHaveaBigEgo

7

Видаліть трейлінг .0і за .00допомогою регулярного вираження splitта використовуйте 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)

І, звичайно, ви можете перетворити його на однолінійний, якщо ви не заперечуєте довгі рядки.


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

Це один вкладиш (розкол для легальності). Не впевнений у читанні ...


1
Так! І скоротився ще далі ( tupleне потрібно btw):cmp(*zip(*map(lambda x,y:(x or 0,y or 0), map(int,v1.split('.')), map(int,v2.split('.')) )))
Павло

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, за винятком "=". Тому що це неоднозначно.


2

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

Мені потрібно було трохи розширити цей підхід, оскільки я використовую Python3x там, де cmpфункція вже не існує. Я повинен був наслідувати cmp(a,b)з (a > b) - (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

Якщо ви не хочете втягувати зовнішню залежність, ось моя спроба написана для Python 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') < 0
    True
    >>> 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') <0
    True
    >>> 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') <0
    True
    >>> compare_version('1.1', '1.10') <0
    True
    >>> 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') < 0
    True
    >>> 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))

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. + btw, Python 2.5 та старіші потрібні, щоб зловити StopIteration.


1

Я зробив це для того, щоб можна було проаналізувати та порівняти рядок версії пакету 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

Ще одне рішення:

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

Роками пізніше, але в цьому питанні це питання на самому верху.

Ось моя функція сортування версій. Він розділяє версію на розділи числа та нечислових номерів. Числа порівнюються як intінші str(як частини елементів списку).

def sort_version_2(data):
    def key(n):
        a = re.split(r'(\d+)', n)
        a[1::2] = map(int, a[1::2])
        return a
    return sorted(data, key=lambda n: key(n))

Ви можете використовувати функцію keyяк тип користувальницького Versionтипу з операторами порівняння. Якщо ви дійсно хочете використовувати, cmpви можете зробити це, як у цьому прикладі: https://stackoverflow.com/a/22490617/9935708

def Version(s):
    s = re.sub(r'(\.0*)*$', '', s)  # to avoid ".0" at end
    a = re.split(r'(\d+)', s)
    a[1::2] = map(int, a[1::2])
    return a

def mycmp(a, b):
    a, b = Version(a), Version(b)
    return (a > b) - (a < b)  # DSM's answer

Тестовий набір проходить.


-1

Моє бажане рішення:

Прокладка струни із додатковими нулями та просто використання чотирьох перших легко зрозуміти, не потребує повторного вираження, а лямбда - більш-менш читабельний. Я використовую два рядки для читабельності, для мене витонченість коротка і проста.

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

-1

Це моє рішення (написано на С, вибачте). Сподіваюся, вам це стане в нагоді

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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.