Qual é a melhor maneira de cortar uma corda em pedaços de um determinado comprimento em Ruby?


89

Tenho procurado uma maneira elegante e eficiente de dividir uma string em substrings de um determinado comprimento em Ruby.

Até agora, o melhor que consegui sugerir é o seguinte:

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

Você pode querer chunk("", n)retornar em [""]vez de []. Nesse caso, basta adicionar isso como a primeira linha do método:

return [""] if string.empty?

Você recomendaria alguma solução melhor?

Editar

Obrigado a Jeremy Ruten por esta solução elegante e eficiente: [editar: NÃO eficiente!]

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

Editar

A solução string.scan leva cerca de 60 segundos para dividir 512k em blocos de 1k 10.000 vezes, em comparação com a solução baseada em fatias original, que leva apenas 2,4 segundos.


Sua solução original é a mais eficiente e elegante possível: não há necessidade de inspecionar cada caractere da string para saber onde cortá-la, nem de transformar tudo em um array e depois voltar.
android.weasel

Respostas:


159

Use 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, agora isso é excelente! Eu sabia que deveria haver uma maneira melhor. Muito obrigado Jeremy Ruten.
MiniQuark de

3
chunk def (string, tamanho); string.scan (/. {1, # {size}} /); fim
MiniQuark

1
Uau, eu me sinto estúpido agora. Nunca me preocupei em verificar como a digitalização funcionava.
Chuck,

18
Tenha cuidado com esta solução; esta é uma expressão regular, e um /.pouco dela significa que incluirá todos os caracteres EXCETO as novas linhas \n. Se você quiser incluir novas linhas, usestring.scan(/.{4}/m)
professormeowingtons

1
Que solução inteligente! Eu amo regexps, mas não teria pensado em usar o quantificador para esse propósito. Obrigado Jeremy Ruten
Cec

18

Aqui está outra maneira de fazer isso:

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

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


16
Alternativamente:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr

3
Gosto deste porque funciona em strings que contêm novas linhas.
Steve Davis de

1
Esta deve ser a solução aceita. O uso de varredura pode eliminar o último token se o comprimento não corresponder ao padrão .
contagem0

6

Eu acho que esta é a solução mais eficiente se você souber que sua string é um múltiplo do tamanho do pedaço

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

e por peças

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

3
Sua string não precisa ser um múltiplo do tamanho do pedaço se você substituir string.length / sizepor (string.length + size - 1) / size- esse padrão é comum no código C que precisa lidar com truncamento de inteiro.
nitrogênio de

3

Aqui está outra solução para casos ligeiramente diferentes, ao processar cadeias de caracteres grandes e não há necessidade de armazenar todos os pedaços de uma vez. Desta forma, ele armazena um único pedaço por vez e tem um desempenho muito mais rápido do que fatiar strings:

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

Para strings muito grandes, essa é de longe a melhor maneira de fazer isso . Isso evitará a leitura de toda a string na memória e a obtenção de Errno::EINVALerros como Invalid argument @ io_freade Invalid argument @ io_write.
Joshua Pinter,

2

Fiz um pequeno teste que divide cerca de 593 MB de dados em 18991 pedaços de 32 KB. Sua versão do slice + map rodou por pelo menos 15 minutos usando 100% da CPU antes de eu pressionar ctrl + C. Esta versão usando String # unpack terminou em 3,6 segundos:

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

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

A rejeição é necessária porque, de outra forma, inclui o espaço em branco entre os conjuntos. Meu regex-fu não está bem para ver como consertar isso logo de cara.


a abordagem de varredura esquecerá caracteres não combinados, ou seja: se você tentar com uma fatia de corda de 10 comprimentos em 3 partes, você terá 3 partes e 1 elemento será descartado, sua abordagem não faz isso, então é melhor.
vinicius gati

1

Uma solução melhor que leva em conta a última parte da string, que pode ser menor que o tamanho do pedaço:

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

Existem outras restrições que você tem em mente? Caso contrário, ficaria terrivelmente tentado a fazer algo simples como

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

Eu realmente não tenho nenhuma restrição, além de ter algo simples, elegante e eficiente. Gostei da sua ideia, mas você se importaria de traduzi-la em um método, por favor? O [0..10] provavelmente se tornaria um pouco mais complexo.
MiniQuark de

Eu fixei meu exemplo para usar str [i w, w] em vez de str [i w ... (i + 1) * w]. Tx
MiniQuark

Deve ser (1..10) .collect em vez de [0..10] .each. [1..10] é uma matriz que consiste em um elemento - um intervalo. (1..10) é o próprio intervalo. E + each + retorna a coleção original que é chamada ([1..10] neste caso) em vez dos valores retornados pelo bloco. Queremos + mapa + aqui.
Chuck,

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.