¿Cuál es la mejor manera de cortar una cuerda en trozos de una longitud determinada en Ruby?

88

He estado buscando una forma elegante y eficiente de fragmentar una cadena en subcadenas de una longitud determinada en Ruby.

Hasta ahora, lo mejor que se me ocurrió es esto:

def chunk(string, size)
  (0..(string.length-1)/size).map{|i|string[i*size,size]}
end

>> chunk("abcdef",3)
=> ["abc", "def"]
>> chunk("abcde",3)
=> ["abc", "de"]
>> chunk("abc",3)
=> ["abc"]
>> chunk("ab",3)
=> ["ab"]
>> chunk("",3)
=> []

Es posible que desee chunk("", n)regresar en [""]lugar de []. Si es así, simplemente agregue esto como la primera línea del método:

return [""] if string.empty?

¿Recomendarías alguna solución mejor?

Editar

Gracias a Jeremy Ruten por esta elegante y eficiente solución: [editar: ¡NO eficiente!]

def chunk(string, size)
    string.scan(/.{1,#{size}}/)
end

Editar

La solución string.scan tarda unos 60 segundos en cortar 512k en 1k trozos 10000 veces, en comparación con la solución original basada en cortes, que solo tarda 2,4 segundos.

MiniQuark
fuente
Su solución original es lo más eficiente y elegante posible: no es necesario inspeccionar cada carácter de la cadena para saber dónde cortarlo, ni tampoco es necesario convertir todo en una matriz y luego volver a hacerlo.
android.weasel

Respuestas:

158

Utilizar String#scan:

>> 'abcdefghijklmnopqrstuvwxyz'.scan(/.{4}/)
=> ["abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx"]
>> 'abcdefghijklmnopqrstuvwxyz'.scan(/.{1,4}/)
=> ["abcd", "efgh", "ijkl", "mnop", "qrst", "uvwx", "yz"]
>> 'abcdefghijklmnopqrstuvwxyz'.scan(/.{1,3}/)
=> ["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]
Jeremy Ruten
fuente
Ok, ¡esto es excelente! Sabía que tenía que haber una forma mejor. Muchas gracias Jeremy Ruten.
MiniQuark
3
def chunk (cadena, tamaño); string.scan (/. {1, # {size}} /); fin
MiniQuark
1
Vaya, me siento estúpido ahora. Ni siquiera me he molestado en comprobar cómo funcionaba el escaneo.
Chuck
18
Tenga cuidado con esta solución; esta es una /.expresión regular , y la parte de ella significa que incluirá todos los caracteres EXCEPTO las líneas nuevas \n. Si desea incluir nuevas líneas, usestring.scan(/.{4}/m)
professormeowingtons
1
¡Qué solución tan inteligente! Me encantan las expresiones regulares, pero no habría pensado en usar el cuantificador para este propósito. Gracias Jeremy Ruten
Cec
18

Aquí hay otra forma de hacerlo:

"abcdefghijklmnopqrstuvwxyz".chars.to_a.each_slice(3).to_a.map {|s| s.to_s }

=> ["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]

Jason
fuente
15
Alternativamente:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr
3
Me gusta este porque funciona en cadenas que contienen nuevas líneas.
Steve Davis
1
Esta debería ser la solución aceptada. El uso de escaneo puede eliminar el último token si la longitud no coincide con el patrón .
recuento0
6

Creo que esta es la solución más eficiente si sabe que su cadena es un múltiplo del tamaño del fragmento

def chunk(string, size)
    (string.length / size).times.collect { |i| string[i * size, size] }
end

y para partes

def parts(string, count)
    size = string.length / count
    count.times.collect { |i| string[i * size, size] }
end
Davispuh
fuente
3
Su cadena no tiene que ser un múltiplo del tamaño del fragmento si reemplaza string.length / sizecon (string.length + size - 1) / size; este patrón es común en el código C que tiene que lidiar con el truncamiento de enteros.
nitrógeno
3

Aquí hay otra solución para un caso ligeramente diferente, cuando se procesan cadenas grandes y no es necesario almacenar todos los fragmentos a la vez. De esta manera, almacena un solo fragmento a la vez y funciona mucho más rápido que cortar cadenas:

io = StringIO.new(string)
until io.eof?
  chunk = io.read(chunk_size)
  do_something(chunk)
end
prcu
fuente
Para cadenas muy grandes, esta es, con mucho, la mejor manera de hacerlo . Esto evitará leer toda la cadena en la memoria y obtener Errno::EINVALerrores como Invalid argument @ io_fready Invalid argument @ io_write.
Joshua Pinter
2

Hice una pequeña prueba que corta aproximadamente 593 MB de datos en 18991 piezas de 32 KB. Su versión de slice + map se ejecutó durante al menos 15 minutos usando el 100% de CPU antes de presionar ctrl + C. Esta versión que usa String # unpack terminó en 3.6 segundos:

def chunk(string, size)
  string.unpack("a#{size}" * (string.size/size.to_f).ceil)
end
Por Wigren
fuente
1
test.split(/(...)/).reject {|v| v.empty?}

El rechazo es necesario porque, de lo contrario, incluye el espacio en blanco entre conjuntos. Mi regex-fu no está a la altura de ver cómo arreglar eso de la parte superior de mi cabeza.

Arrojar
fuente
el enfoque de escaneo se olvidará de los caracteres no coincidentes, es decir: si intentas con un segmento de cadena de 10 longitudes en 3 partes, tendrás 3 partes y se eliminará 1 elemento, tu enfoque no hace eso, así que es mejor.
vinicius gati
1

Una mejor solución que tiene en cuenta la última parte de la cadena, que podría ser menor que el tamaño del fragmento:

def chunk(inStr, sz)  
  return [inStr] if inStr.length < sz  
  m = inStr.length % sz # this is the last part of the string
  partial = (inStr.length / sz).times.collect { |i| inStr[i * sz, sz] }
  partial << inStr[-m..-1] if (m % sz != 0) # add the last part 
  partial
end
kirkytullins
fuente
0

¿Tiene otras limitaciones en mente? De lo contrario, estaría terriblemente tentado a hacer algo simple como

[0..10].each {
   str[(i*w),w]
}
Charlie martin
fuente
Realmente no tengo ninguna restricción, aparte de tener algo simple, elegante y eficiente. Me gusta tu idea, pero ¿te importaría traducirla en un método, por favor? El [0..10] probablemente se volvería un poco más complejo.
MiniQuark
Arreglé mi ejemplo para usar str [i w, w] en lugar de str [i w ... (i + 1) * w]. Tx
MiniQuark
Debe ser (1..10) .collect en lugar de [0..10] .each. [1..10] es una matriz que consta de un elemento: un rango. (1..10) es el rango en sí. Y + cada + devuelve la colección original a la que se llama ([1..10] en este caso) en lugar de los valores devueltos por el bloque. Queremos + mapa + aquí.
Chuck
0

Solo text.scan(/.{1,4}/m)resuelve el problema

Vyacheslav
fuente