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


98

私が書きたいcmp2つのバージョン番号とリターンを比較する様機能を-10あるいは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を使用しています。(私の職場にインストールされています...)。

ここにあなたが使える小さな「テストスイート」があります

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のアルゴリズムを実装する価値があるかもしれません。アルゴリズムはここで説明されています(「文字列は左から右に比較されます」から始まります)。
ホブ

ブラー。コメントでサポートされているマークダウンのサブセットは、私を混乱させることに決して失敗しません。それは愚かに見えても、リンクはとにかく機能します。
ホブ

将来の読者がユーザーエージェントのバージョンの解析にこれを必要とする場合に備えて、履歴のバリエーションが広すぎるため、専用ライブラリをお勧めします。
James Broadhead、2012


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

Bashでドット区切りバージョン形式の2つの文字列を比較する方法」のおかげで、いくつかのテストがあります:

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")。「1.0.10」で「.10」が「.1」に変更されます。
RedGlyph 2009

申し訳ありませんが、関数:mycmp( '1.1'、 '1.10')== 0
Johannes Charra

正規表現を使用すると、上記の問題が修正されます。
gnud 2009

これで、他のすべての優れたアイデアがソリューションにマージされました... :-Pまだ、これは結局私がやろうとしていることのほとんどです。この答えを受け入れます。皆さん、ありがとう
ヨハネスチャラ2009

2
注意:cmp()はPython 3で削除されました:docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
Dominic Cleal

279

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


2
既にそこにあるものを使用するのが最も理にかなっているようです:)
Patrick Wolf

2
いいね!ソースを読んでこれを理解しましたか?distutils.versionのドキュメントがどこにも見つかりません:-/
Adam Spiers

3
ドキュメントが見つからない場合は、パッケージをインポートしてhelp()を使用してください。
rspeed 2012年

13
ただし、これは3桁までのバージョンでStrictVersion のみ機能します。のようなものは失敗します0.4.3.6
abergmeier 2013

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を使用することを好みます-非常に単純なタスクのためにサードパーティのソフトウェアを導入する努力に見合う価値はないようです。
Adam Spiers、2012

1
@ adam- spiers wut?解説も読みましたか?pkg_resourcessetuptoolsバンドルされたパッケージです。setuptoolsはすべてのPythonインストールで事実上必須であるため、pkg_resourcesどこでも効果的に使用できます。とdistutils.versionはいえ、サブパッケージも役立ちます。ただし、上位レベルのpkg_resources.parse_version()関数よりもかなりインテリジェントではありません。どちらを利用するかは、バージョン文字列で予想される狂気の程度によって異なります。
Cecil Curry

@CecilCurryはいもちろんコメント(ary)を読んだので、それを編集して改善したので、その旨を述べました。おそらく、あなたはsetuptools標準ライブラリの外にある私の声明に同意せず、代わりdistutils にこの場合の私の好みに同意します。では、「事実上義務的」とはどういう意味ですか。4.5年前に私がこのコメントを書いたときに、「事実上義務的」であったという証拠を教えていただけますか。
Adam Spiers 2016年

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)

散文のようにコードを読むことができない人にとってはクールですが、理解するのは難しいです。:)まあ、私はあなたが読みやすさを犠牲にしてソリューションを短くすることができるだけだと思います...
ヨハネス・チャラ2009

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。しかし、私はまだ1つまたは2つのライナーを期待しています...;)
ヨハネスチャラ2009

4
+1 @jellybean:2行は保守と読みやすさの点で常に最適であるとは限りません。これは同時に非常に明確でコンパクトなコードであることに加え、mycmp必要に応じてコード内で他の目的に再利用できます。
RedGlyph 2009

@RedGlyph:あなたはそこにポイントを持っています。「読みやすい2ライナー」と言うべきだった。:)
ヨハネス・チャラ2009

こんにちは@PärWieslander、このソリューションを使用してリートコード問題で同じ問題を解決すると、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ます(必要ありません):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

version_compare「=」以外のphpを実装します。あいまいなので。


2

リストはPythonで比較できるので、誰かが数値を表す文字列を整数に変換した場合、基本的なPythonの比較をうまく使用できます。

cmp関数がもう存在しない場所でPython3xを使用しているため、このアプローチを少し拡張する必要がありました。でエミュレートする必要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用に書かれた私の試みを示します。

rcrel(場合によっては追加可能c)は「リリース候補」と見なされ、バージョン番号を2つの部分に分割します。欠落している場合、2番目の部分の値は高くなります(999)。その他の文字は分割を生成し、base-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. +の場合、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

私の好ましい解決策:

文字列に追加のゼロを埋め、最初に4つを使用することは理解しやすく、正規表現を必要とせず、ラムダは多かれ少なかれ読みやすいです。私は読みやすさのために2行を使用しています。エレガントさは短くてシンプルです。

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

これは私の解決策です(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;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.