2010-08-07 22 views
15

Tengo una lista de enteros que necesito analizar en una cadena de rangos.Manera pitónica de convertir una lista de enteros en una cadena de rangos separados por comas

Por ejemplo:

[0, 1, 2, 3] -> "0-3" 
[0, 1, 2, 4, 8] -> "0-2,4,8" 

Y así sucesivamente.

Todavía estoy aprendiendo más formas pitónicas de manejar listas, y esta es un poco difícil para mí. Mi último pensamiento fue para crear una lista de listas que realiza un seguimiento de pares de números:

[ [0, 3], [4, 4], [5, 9], [20, 20] ] 

entonces podría iterar a través de esta estructura, imprimiendo cada sub-lista, ya sea como un rango o un único valor.

No me gusta hacer esto en dos iteraciones, pero parece que no puedo seguir cada número dentro de cada iteración. Mi pensamiento sería hacer algo como esto:

Aquí está mi intento más reciente. Funciona, pero no estoy completamente satisfecho; Sigo pensando que hay una solución más elegante que se me escapa por completo. La iteración cadena de manejo no es el mejor, lo sé - que es bastante temprano en la mañana para mí :)

def createRangeString(zones): 
     rangeIdx = 0 
     ranges = [[zones[0], zones[0]]] 
     for zone in list(zones): 
      if ranges[rangeIdx][1] in (zone, zone-1): 
       ranges[rangeIdx][1] = zone 
      else: 
       ranges.append([zone, zone]) 
       rangeIdx += 1 

     rangeStr = "" 
     for range in ranges: 
      if range[0] != range[1]: 
       rangeStr = "%s,%d-%d" % (rangeStr, range[0], range[1]) 
      else: 
       rangeStr = "%s,%d" % (rangeStr, range[0]) 

     return rangeStr[1:] 

¿Hay una manera sencilla que puedo combinar esto en una sola iteración? ¿Qué más podría hacer para que sea más Pythonic?

+0

Vea una demostración de la herramienta 'more_itertools.consecutive_groups' [aquí] (https://stackoverflow.com/a/47642650/4531270). – pylang

Respuesta

20
>>> from itertools import count, groupby 
>>> L=[1, 2, 3, 4, 6, 7, 8, 9, 12, 13, 19, 20, 22, 23, 40, 44] 
>>> G=(list(x) for _,x in groupby(L, lambda x,c=count(): next(c)-x)) 
>>> print ",".join("-".join(map(str,(g[0],g[-1])[:len(g)])) for g in G) 
1-4,6-9,12-13,19-20,22-23,40,44 

La idea aquí es emparejar cada elemento con count(). Entonces la diferencia entre el valor y el recuento() es constante para valores consecutivos. groupby() hace el resto del trabajo

Como sugiere Jeff, una alternativa a count() es usar enumerate(). Esto añade un poco de costra extra que necesita ser despojado en el pliego de impresión

G=(list(x) for _,x in groupby(enumerate(L), lambda (i,x):i-x)) 
print ",".join("-".join(map(str,(g[0][1],g[-1][1])[:len(g)])) for g in G) 

Actualización: de la lista muestra dada aquí, la versión con enumerar corre alrededor de un 5% más lento que la versión usando count() en mi equipo

+0

Pruebe con L = [1,2,3,4,7,9,11,13,14,15]. Probablemente se rompa si te entendí correctamente. –

+4

¿Puedo recuperarlo? Solución genial, te pido mi sombrero. –

+0

@Muhammad, por supuesto :) revisa la versión actualizada - ya no requiere 'izip' –

2

Si esto es pitónico está por debatir. Pero es muy compacto. La verdadera carne está en la función Rangify(). Todavía hay margen de mejora si quieres eficiencia o Pythonism.

def CreateRangeString(zones): 
    #assuming sorted and distinct 
    deltas = [a-b for a, b in zip(zones[1:], zones[:-1])] 
    deltas.append(-1) 
    def Rangify((b, p), (z, d)): 
     if p is not None: 
      if d == 1: return (b, p) 
      b.append('%d-%d'%(p,z)) 
      return (b, None) 
     else: 
      if d == 1: return (b, z) 
      b.append(str(z)) 
      return (b, None) 
    return ','.join(reduce(Rangify, zip(zones, deltas), ([], None))[0]) 

Para describen los parámetros:

  • deltas es la distancia al siguiente valor (inspirado desde un answer aquí en SO)
  • Rangify() hace la reducción en estos parámetros
    • b - base o acumulador
    • p - anterior gama inicio
    • z - número de zona
    • d - delta
+0

puede suponer distinto, pero no necesariamente ordenado. Sin embargo, dado que la lista es numérica, el método sort() hace que la resolución de la suposición sea bastante trivial. – bedwyr

+0

En realidad, retiro eso: mi código que inserta la lista en la función realiza una ordenación previa. No importa :) – bedwyr

1

para concatenar cadenas que puedes usar ','.join. Esto elimina el 2do ciclo.

def createRangeString(zones): 
     rangeIdx = 0 
     ranges = [[zones[0], zones[0]]] 
     for zone in list(zones): 
      if ranges[rangeIdx][1] in (zone, zone-1): 
       ranges[rangeIdx][1] = zone 
      else: 
       ranges.append([zone, zone]) 
       rangeIdx += 1 

     return ','.join(
       map(
        lambda p: '%s-%s'%tuple(p) if p[0] != p[1] else str(p[0]), 
        ranges 
       ) 
      ) 

Aunque prefiero un enfoque más genérico:

from itertools import groupby 

# auxiliary functor to allow groupby to compare by adjacent elements. 
class cmp_to_groupby_key(object): 
    def __init__(self, f): 
    self.f = f 
    self.uninitialized = True 
    def __call__(self, newv): 
    if self.uninitialized or not self.f(self.oldv, newv): 
     self.curkey = newv 
     self.uninitialized = False 
    self.oldv = newv 
    return self.curkey 

# returns the first and last element of an iterable with O(1) memory. 
def first_and_last(iterable): 
    first = next(iterable) 
    last = first 
    for i in iterable: 
    last = i 
    return (first, last) 

# convert groups into list of range strings 
def create_range_string_from_groups(groups): 
    for _, g in groups: 
    first, last = first_and_last(g) 
    if first != last: 
     yield "{0}-{1}".format(first, last) 
    else: 
     yield str(first) 

def create_range_string(zones): 
    groups = groupby(zones, cmp_to_groupby_key(lambda a,b: b-a<=1)) 
    return ','.join(create_range_string_from_groups(groups)) 

assert create_range_string([0,1,2,3]) == '0-3' 
assert create_range_string([0, 1, 2, 4, 8]) == '0-2,4,8' 
assert create_range_string([1,2,3,4,6,7,8,9,12,13,19,20,22,22,22,23,40,44]) == '1-4,6-9,12-13,19-20,22-23,40,44' 
0

Aquí está mi solución. Necesita hacer un seguimiento de varias piezas de información mientras itera por la lista y crea el resultado; esto me grita generador.Así que aquí va:

def rangeStr(start, end): 
    '''convert two integers into a range start-end, or a single value if they are the same''' 
    return str(start) if start == end else "%s-%s" %(start, end) 

def makeRange(seq): 
    '''take a sequence of ints and return a sequence 
    of strings with the ranges 
    ''' 
    # make sure that seq is an iterator 
    seq = iter(seq) 
    start = seq.next() 
    current = start 
    for val in seq: 
     current += 1 
     if val != current: 
      yield rangeStr(start, current-1) 
      start = current = val 
    # make sure the last range is included in the output 
    yield rangeStr(start, current) 

def stringifyRanges(seq): 
    return ','.join(makeRange(seq)) 

>>> l = [1,2,3, 7,8,9, 11, 20,21,22,23] 
>>> l2 = [1,2,3, 7,8,9, 11, 20,21,22,23, 30] 
>>> stringifyRanges(l) 
'1-3,7-9,11,20-23' 
>>> stringifyRanges(l2) 
'1-3,7-9,11,20-23,30' 

Mi versión funcionará correctamente si se da una lista vacía, que creo que algunas de las otras no.

>>> stringifyRanges([]) 
'' 

makeRanges funcionará en cualquier iterador que devuelve enteros y perezosamente devuelve una secuencia de cadenas de modo se puede utilizar en secuencias infinitas.

editar: He actualizado el código para manejar números individuales que no son parte de un rango.

edición2: campo redibujadoStr para eliminar la duplicación.

+0

OP * did * especifica lo que sucede cuando aparece un solo número en la lista. '[0, 1, 2, 4, 8] ->" 0-2,4,8 "'. – kennytm

+0

Gracias Kenny, me lo perdí. He actualizado el código para manejar correctamente los números individuales. –

0
def createRangeString(zones): 
    """Create a string with integer ranges in the format of '%d-%d' 
    >>> createRangeString([0, 1, 2, 4, 8]) 
    "0-2,4,8" 
    >>> createRangeString([1,2,3,4,6,7,8,9,12,13,19,20,22,22,22,23,40,44]) 
    "1-4,6-9,12-13,19-20,22-23,40,44" 
    """ 
    buffer = [] 

    try: 
     st = ed = zones[0] 
     for i in zones[1:]: 
      delta = i - ed 
      if delta == 1: ed = i 
      elif not (delta == 0): 
       buffer.append((st, ed)) 
       st = ed = i 
     else: buffer.append((st, ed)) 
    except IndexError: 
     pass 

    return ','.join(
      "%d" % st if st==ed else "%d-%d" % (st, ed) 
      for st, ed in buffer) 
1

esto es más detallado, sobre todo porque he utilizado funciones genéricas que tengo y que son variaciones menores de funciones itertools y recetas:

from itertools import tee, izip_longest 
def pairwise_longest(iterable): 
    "variation of pairwise in http://docs.python.org/library/itertools.html#recipes" 
    a, b = tee(iterable) 
    next(b, None) 
    return izip_longest(a, b) 

def takeuntil(predicate, iterable): 
    """returns all elements before and including the one for which the predicate is true 
    variation of http://docs.python.org/library/itertools.html#itertools.takewhile""" 
    for x in iterable: 
     yield x 
     if predicate(x): 
      break 

def get_range(it): 
    "gets a range from a pairwise iterator" 
    rng = list(takeuntil(lambda (a,b): (b is None) or (b-a>1), it)) 
    if rng: 
     b, e = rng[0][0], rng[-1][0] 
     return "%d-%d" % (b,e) if b != e else "%d" % b 

def create_ranges(zones): 
    it = pairwise_longest(zones) 
    return ",".join(iter(lambda:get_range(it),None)) 

k=[0,1,2,4,5,7,9,12,13,14,15] 
print create_ranges(k) #0-2,4-5,7,9,12-15 
0

¿qué hay de este lío ...

def rangefy(mylist): 
    mylist, mystr, start = mylist + [None], "", 0 
    for i, v in enumerate(mylist[:-1]): 
      if mylist[i+1] != v + 1: 
        mystr += ["%d,"%v,"%d-%d,"%(start,v)][start!=v] 
        start = mylist[i+1] 
    return mystr[:-1] 
+0

¿Alguien tiene alguna idea de cómo reescribir usando join o cómo evitar sumar None o cómo hacer que el código anterior sea más corto? –

Cuestiones relacionadas