Confronto del numero di versione in Python


98

Voglio scrivere una cmpfunzione di -come che confronta due numeri di versione e ritorna -1, 0o 1in base alla loro valuses rispetto.

  • Restituisci -1se la versione A è precedente alla versione B
  • Restituisce 0se le versioni A e B sono equivalenti
  • Restituisci 1se la versione A è più recente della versione B

Ogni sottosezione dovrebbe essere interpretata come un numero, quindi 1.10> 1.1.

Le uscite delle funzioni desiderate sono

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

Ed ecco la mia implementazione, aperta al miglioramento:

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

Sto usando Python 2.4.5 btw. (installato nel mio posto di lavoro ...).

Ecco una piccola "suite di test" che puoi utilizzare

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

Non una risposta ma un suggerimento: potrebbe valere la pena implementare l'algoritmo di Debian per il confronto del numero di versione (fondamentalmente, alternando l'ordinamento di parti non numeriche e numeriche). L'algoritmo è descritto qui (a partire da "Le stringhe vengono confrontate da sinistra a destra").
hobbs

Blargh. Il sottoinsieme del markdown supportato nei commenti non manca mai di confondermi. Il collegamento funziona comunque, anche se sembra stupido.
hobbs

Nel caso in cui i lettori futuri ne abbiano bisogno per l'analisi della versione dell'agente utente, consiglio una libreria dedicata poiché la variazione storica è troppo ampia.
James Broadhead

2
Possibile duplicato delle stringhe di versione
John Y

1
Anche se la domanda qui è più antica, sembra che quest'altra domanda sia stata unta come quella canonica, poiché molte, molte domande sono chiuse come duplicati di quella.
John Y

Risposte:


36

Rimuovi la parte non interessante della stringa (zeri e punti finali), quindi confronta gli elenchi di numeri.

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

Questo è lo stesso approccio di Pär Wieslander, ma un po 'più compatto:

Ecco alcuni test, grazie a " Come confrontare due stringhe in formato versione separata da punti in 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
Temo che non funzionerà, rstrip(".0")cambierà ".10" in ".1" in "1.0.10".
RedGlyph

Scusa, ma con la tua funzione: mycmp ('1.1', '1.10') == 0
Johannes Charra

Con l'uso delle espressioni regolari, il problema sopra menzionato è stato risolto.
gnud

Ora hai unito tutte le buone idee degli altri nella tua soluzione ... :-P ancora, questo è più o meno quello che farei dopo tutto. Accetterò questa risposta. Grazie a tutti
Johannes Charra

2
Nota cmp () è stato rimosso in Python 3: docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
Dominic Cleal

279

Che ne dici di usare Python distutils.version.StrictVersion?

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

Quindi per la tua cmpfunzione:

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

Se vuoi confrontare i numeri di versione più complessi distutils.version.LooseVersionsarà più utile, tuttavia assicurati di confrontare solo gli stessi tipi.

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

LooseVersion non è lo strumento più intelligente e può essere facilmente ingannato:

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

Per avere successo con questa razza, sarà necessario fare un passo al di fuori della biblioteca e di utilizzo standard setuptools utility parsing 's parse_version.

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

Quindi, a seconda del tuo caso d'uso specifico, dovrai decidere se gli distutilsstrumenti incorporati sono sufficienti o se è giustificato aggiungerli come dipendenza setuptools.


2
sembra avere più senso usare solo ciò che è già presente :)
Patrick Wolf

2
Bello! Lo hai capito leggendo la fonte? Non riesco a trovare documenti per distutils.version da nessuna parte: - /
Adam Spires

3
Ogni volta che non riesci a trovare la documentazione, prova a importare il pacchetto e usa help ().
velocità

13
Tieni presente, però, che funziona StrictVersion SOLO con una versione fino a tre numeri. Fallisce per cose come 0.4.3.6!
abergmeier

6
Ogni istanza di distributein questa risposta dovrebbe essere sostituita da setuptools, che viene fornito in bundle con il pkg_resourcespacchetto e da allora ... come, mai . Allo stesso modo, questa è la documentazione ufficiale per la pkg_resources.parse_version()funzione fornita con setuptools.
Cecil Curry

30

Il riutilizzo è considerato eleganza in questo caso? :)

# 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
Hmm, non è così elegante quando fai riferimento a qualcosa al di fuori della libreria standard senza spiegare dove trovarlo. Ho inviato una modifica per includere l'URL. Personalmente preferisco usare distutils: non sembra valere la pena di inserire software di terze parti per un'attività così semplice.
Adam Spires

1
@ adam-spires wut? Hai anche letto il commento? pkg_resourcesè un setuptoolspacchetto in bundle. Poiché setuptoolsè effettivamente obbligatorio su tutte le installazioni Python, pkg_resourcesè effettivamente disponibile ovunque. Detto questo, anche il distutils.versionsottopacchetto è utile, sebbene notevolmente meno intelligente della pkg_resources.parse_version()funzione di livello superiore . Quello che dovresti sfruttare dipende dal grado di follia che ti aspetti nelle stringhe di versione.
Cecil Curry

@CecilCurry Sì, certo, ho letto il commento (ary), motivo per cui l'ho modificato per renderlo migliore, e poi ho dichiarato di averlo fatto. Presumibilmente non sei in disaccordo con la mia affermazione che setuptoolsè al di fuori della libreria standard, e invece con la mia preferenza dichiarata distutils in questo caso . Quindi cosa intendi esattamente per "effettivamente obbligatorio" e per favore puoi fornire la prova che era "effettivamente obbligatorio" 4,5 anni fa quando ho scritto questo commento?
Adam Spires

12

Non c'è bisogno di iterare sulle tuple di versione. L'operatore di confronto integrato su elenchi e tuple funziona già esattamente come lo desideri. Dovrai solo estendere a zero gli elenchi delle versioni alla lunghezza corrispondente. Con python 2.6 puoi usare izip_longest per riempire le sequenze.

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)

Con le versioni precedenti, è necessario un hacker di mappe.

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)

Bello, ma difficile da capire per qualcuno che non sa leggere il codice come la prosa. :) Beh, presumo che tu possa solo accorciare la soluzione a costo della leggibilità ...
Johannes Charra

10

Questo è un po 'più compatto del tuo suggerimento. Invece di riempire la versione più breve con zeri, rimuovo gli zeri finali dagli elenchi delle versioni dopo la divisione.

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

Bello, grazie. Ma spero ancora in una o due linee ...;)
Johannes Charra

4
+1 @jellybean: le due righe non sono sempre le migliori per manutenzione e leggibilità, questo è un codice molto chiaro e compatto allo stesso tempo, inoltre, puoi riutilizzarlo mycmpper altri scopi nel tuo codice se ne hai bisogno.
RedGlyph

@ RedGlyph: hai ragione. Avrebbe dovuto dire "una doppia riga leggibile". :)
Johannes Charra

ciao @ Pär Wieslander, quando uso questa soluzione per risolvere lo stesso problema con il problema Leetcode, ricevo un errore nel ciclo while che dice "list index out of range". Puoi aiutare per favore perché ciò accade? Ecco il problema: leetcode.com/explore/interview/card/amazon/76/array-and-strings/…
YouHaveaBigEgo

7

Rimuovi trailing .0e .00con regex splite usa la cmpfunzione che confronta correttamente gli array:

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)

E, naturalmente, puoi convertirlo in una riga se non ti dispiace le lunghe code.


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

È un rivestimento (diviso per leggibilità). Non sono sicuro di leggibile ...


1
Sì! E si è ridotto ulteriormente ( tuplenon è necessario tra l'altro):cmp(*zip(*map(lambda x,y:(x or 0,y or 0), map(int,v1.split('.')), map(int,v2.split('.')) )))
Paolo

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

Implementa per php version_compare, tranne "=". Perché è ambiguo.


2

Le liste sono confrontabili in Python, quindi se qualcuno converte le stringhe che rappresentano i numeri in interi, il confronto di base di Python può essere utilizzato con successo.

Avevo bisogno di estendere un po 'questo approccio perché uso Python3x dove la cmpfunzione non esiste più. Ho dovuto emulare cmp(a,b)con (a > b) - (a < b). Inoltre, i numeri di versione non sono affatto puliti e possono contenere tutti i tipi di altri caratteri alfanumerici. Ci sono casi in cui la funzione non può dire l'ordine, quindi ritornaFalse (vedi il primo esempio).

Quindi sto postando questo anche se la domanda è vecchia e ha già ricevuto risposta, perché potrebbe far risparmiare qualche minuto nella vita di qualcuno.

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

Nel caso in cui non si desideri inserire una dipendenza esterna, ecco il mio tentativo scritto per Python 3.x.

rc, rel(ed eventualmente si potrebbe aggiungere c) sono considerati come "release candidate" e dividono il numero di versione in due parti e se manca il valore della seconda parte è alto (999). Altrimenti le lettere producono una divisione e vengono trattate come sotto-numeri tramite il codice 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

La soluzione più difficile da leggere, ma comunque una riga! e usare gli iteratori per essere veloci.

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)

cioè per Python2.6 e 3. + btw, Python 2.5 e versioni precedenti devono catturare StopIteration.


1

L'ho fatto per poter analizzare e confrontare la stringa della versione del pacchetto Debian. Si prega di notare che non è rigoroso con la convalida dei caratteri.

Anche questo potrebbe essere 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 __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

Un'altra soluzione:

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

Si può usare anche in questo modo:

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

Sto usando questo nel mio progetto:

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

0

Anni dopo, ma questa domanda è in cima.

Ecco la mia funzione di ordinamento della versione. Divide la versione in numeri e sezioni non numeriche. I numeri vengono confrontati come intresto come str(come parti di voci di elenco).

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

È possibile utilizzare la funzione keycome una sorta di Versiontipo personalizzato con operatori di confronto. Se vuoi davvero usarlo cmppuoi farlo come in questo esempio: 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

La suite di test è stata superata.


-1

La mia soluzione preferita:

Riempire la stringa con zeri extra e usare solo i primi quattro è facile da capire, non richiede alcuna regex e il lambda è più o meno leggibile. Uso due righe per la leggibilità, per me l'eleganza è breve e semplice.

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

Questa è la mia soluzione (scritta in C, scusa). Spero che lo troverai utile

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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.