Qual è il modo migliore per tagliare una stringa in pezzi di una data lunghezza in Ruby?


88

Ho cercato un modo elegante ed efficiente per suddividere una stringa in sottostringhe di una determinata lunghezza in Ruby.

Finora, il meglio che sono riuscito a trovare è questo:

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)
=> []

Potresti voler chunk("", n)tornare [""]invece di []. In tal caso, aggiungilo come prima riga del metodo:

return [""] if string.empty?

Consiglieresti una soluzione migliore?

modificare

Grazie a Jeremy Ruten per questa soluzione elegante ed efficiente: [modifica: NON efficiente!]

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

modificare

La soluzione string.scan impiega circa 60 secondi per tagliare 512k in blocchi da 1k 10000 volte, rispetto alla soluzione originale basata su slice che richiede solo 2,4 secondi.


La tua soluzione originale è quanto più efficiente ed elegante possibile: non è necessario ispezionare ogni carattere della stringa per sapere dove tagliarlo, né è necessario trasformare l'intera cosa in un array e poi di nuovo.
android.weasel

Risposte:


158

Usa 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"]

Ok, ora è eccellente! Sapevo che doveva esserci un modo migliore. Grazie mille Jeremy Ruten.
MiniQuark

3
def chunk (stringa, dimensione); string.scan (/. {1, # {size}} /); fine
MiniQuark

1
Wow, mi sento stupido adesso. Non mi sono nemmeno preso la briga di controllare come funzionava la scansione.
Chuck

18
Stai attento con questa soluzione; questa è una regexp, e la /.parte significa che includerà tutti i caratteri TRANNE i newline \n. Se desideri includere le nuove righe, usastring.scan(/.{4}/m)
professormeowingtons

1
Che soluzione intelligente! Adoro le espressioni regolari ma non avrei pensato di usare il quantificatore per questo scopo. Grazie Jeremy Ruten
Cec

18

Ecco un altro modo per farlo:

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

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


15
In alternativa:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr

3
Mi piace perché funziona su stringhe che contengono newline.
Steve Davis

1
Questa dovrebbe essere la soluzione accettata. L'utilizzo della scansione potrebbe eliminare l'ultimo token se la lunghezza non corrisponde al modello .
count0

6

Penso che questa sia la soluzione più efficiente se sai che la tua stringa è un multiplo della dimensione del blocco

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

e per parti

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

3
La tua stringa non deve essere un multiplo della dimensione del blocco se sostituisci string.length / sizecon (string.length + size - 1) / size: questo modello è comune nel codice C che ha a che fare con il troncamento di interi.
azoto

3

Ecco un'altra soluzione per casi leggermente diversi, quando si elaborano stringhe di grandi dimensioni e non è necessario memorizzare tutti i blocchi contemporaneamente. In questo modo memorizza un singolo blocco alla volta e si comporta molto più velocemente dello slicing delle stringhe:

io = StringIO.new(string)
until io.eof?
  chunk = io.read(chunk_size)
  do_something(chunk)
end

Per corde molto grandi, questo è di gran lunga il modo migliore per farlo . Ciò eviterà di leggere l'intera stringa in memoria e di ottenere Errno::EINVALerrori come Invalid argument @ io_freade Invalid argument @ io_write.
Joshua Pinter,

2

Ho fatto un piccolo test che taglia circa 593 MB di dati in 18991 pezzi da 32 KB. La tua versione slice + map è stata eseguita per almeno 15 minuti utilizzando il 100% della CPU prima di premere ctrl + C. Questa versione che utilizza String # unpack è terminata in 3,6 secondi:

def chunk(string, size)
  string.unpack("a#{size}" * (string.size/size.to_f).ceil)
end

1
test.split(/(...)/).reject {|v| v.empty?}

Il rifiuto è necessario perché altrimenti include lo spazio vuoto tra le serie. La mia regex-fu non è del tutto in grado di vedere come risolverlo direttamente dalla cima della mia testa.


l'approccio di scansione dimenticherà i caratteri non corrispondenti, ad esempio: se provi con una sezione di stringa di 10 lunghezze su 3 parti, avrai 3 parti e 1 elemento verrà eliminato, il tuo approccio non lo fa, quindi è meglio.
vinicius gati

1

Una soluzione migliore che tiene conto dell'ultima parte della stringa che potrebbe essere inferiore alla dimensione del chunk:

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

0

Ci sono altri vincoli che hai in mente? Altrimenti sarei terribilmente tentato di fare qualcosa di semplice come

[0..10].each {
   str[(i*w),w]
}

Non ho davvero alcun vincolo, a parte avere qualcosa di semplice, elegante ed efficiente. Mi piace la tua idea, ma ti dispiacerebbe tradurla in un metodo per favore? [0..10] diventerebbe probabilmente leggermente più complesso.
MiniQuark

Ho corretto il mio esempio per usare str [i w, w] invece di str [i w ... (i + 1) * w]. Tx
MiniQuark

Dovrebbe essere (1..10) .collect anziché [0..10] .each. [1..10] è un array costituito da un elemento - un intervallo. (1..10) è l'intervallo stesso. E + ogni + restituisce la collezione originale su cui è chiamata ([1..10] in questo caso) invece dei valori restituiti dal blocco. Vogliamo + mappa + qui.
Chuck

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.