web-dev-qa-db-ja.com

Pythonでのバージョン番号の比較

2つのバージョン番号を比較し、比較された値に基づいて-10、または1を返すcmpのような関数を作成したい。

  • バージョンAがバージョンBより古い場合、-1を返します
  • バージョンAとBが同等の場合は0を返します
  • バージョンAがバージョンBよりも新しい場合は1を返します

各サブセクションは数値として解釈されることになっているため、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
93
Johannes Charra

文字列の不要な部分(末尾のゼロとドット)を削除し、数字のリストを比較します。

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ärWieslanderと同じアプローチですが、もう少しコンパクトです。

この投稿 のおかげで、いくつかのテスト:

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
35
gnud

Pythonの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

この品種で成功するには、標準ライブラリの外に出て setuptools の解析ユーティリティ parse_version

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

したがって、特定のユースケースに応じて、組み込みのdistutilsツールで十分かどうか、または依存関係として追加する必要があるかどうかを判断する必要がありますsetuptools

267
bradley.ayers

reuseはこの例では優雅と見なされますか? :)

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

バージョンタプルを繰り返す必要はありません。リストとタプルに組み込まれた比較演算子は、すでに希望どおりに機能しています。バージョンリストを対応する長さにゼロ拡張するだけです。 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)
12
Ants Aasma

これは、提案よりも少しコンパクトです。短いバージョンをゼロで埋めるのではなく、分割後にバージョンリストから末尾のゼロを削除します。

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))
10
Pär Wieslander

末尾の.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)

そしてもちろん、長い行を気にしないなら、それをワンライナーに変換することができます

6
yu_sha

リストはPythonで比較できるため、数値を表す文字列を整数に変換すると、基本的なpython比較が成功して使用できます。

ただし、このアプローチを少し拡張する必要がありました。最初に、cmp関数が存在しないpython3xを使用し、エミュレートする必要がありましたcmp(a 、b)with(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
sanyi
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つです(読みやすいように分割されています)。読み取り可能かどうかわからない...

2
mavnn
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
heronotears

あなたが外部依存関係を引きたくない場合は、ここで私の試みです(python 3.x)のために書かれています。 「c」)は「リリース候補」と見なされ、バージョン番号を2つの部分に分けます。2番目の部分の値が欠落している場合(999)。 。


    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
Roland Puntaier

最も読みにくいソリューションですが、それでもワンライナーです!そして、イテレーターを使用して高速化します。

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
Paul

別の解決策:

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
pedrormjunior

これは、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 __== '__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
Pius Raeder

私は私のプロジェクトでこれを使用しています:

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