Rubyで文字列を特定の長さのチャンクに切り刻む最良の方法は何ですか?


88

私は、Rubyで文字列を特定の長さの部分文字列にチャンクするエレガントで効率的な方法を探していました。

これまでのところ、私が思いつくことができる最高のものはこれです:

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

の代わりchunk("", n)に戻ることをお勧めします。その場合は、これをメソッドの最初の行として追加します。[""][]

return [""] if string.empty?

より良い解決策をお勧めしますか?

編集

このエレガントで効率的なソリューションを提供してくれたJeremyRutenに感謝します:[編集:効率的ではありません!]

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

編集

string.scanソリューションは、わずか2.4秒しかかからない元のスライスベースのソリューションと比較して、512kを1kチャンクに10000回チョップするのに約60秒かかります。


元のソリューションは、可能な限り効率的でエレガントです。文字列の各文字を調べて、どこで切り刻むかを知る必要も、全体を配列に変換してから元に戻す必要もありません。
android.weasel

回答:


158

使用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"]

さて、これは素晴らしいです!もっと良い方法が必要だと私は知っていました。ジェレミー・ルーテン、どうもありがとう。
miniQuark 2009

3
defチャンク(文字列、サイズ); string.scan(/。{1、#{size}} /); 終了
MiniQuark 2009

1
うわー、今はバカだ。スキャンがどのように機能するかを確認することすらしませんでした。
チャック

18
この解決策には注意してください。これは正規表現であり、/.少しは改行を除くすべての文字が含まれることを意味します\n。改行を含める場合は、string.scan(/.{4}/m)
professormeowingtons

1
なんて賢い解決策でしょう。私は正規表現が大好きですが、この目的で数量詞を使用することはありません。ありがとうジェレミールーテン
Cec 2016年

18

これを行う別の方法は次のとおりです。

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

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


15
または:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr 2012年

3
改行を含む文字列で機能するので、これが好きです。
スティーブデイビス

1
これは受け入れられた解決策であるはずです。長さがパターンと一致しない場合、スキャンを使用すると最後のトークンがドロップされる可能性があります。
count0 2016年

6

文字列がチャンクサイズの倍数であることがわかっている場合、これが最も効率的なソリューションだと思います

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

および部品用

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

3
あなたの文字列を使用すると、交換した場合、チャンクサイズの倍数である必要はありませんstring.length / size(string.length + size - 1) / size、このパターンは、整数の切り捨てに対処しなければならないCのコードでは一般的です- 。
窒素

3

大きな文字列を処理し、一度にすべてのチャンクを格納する必要がない場合の、わずかに異なるケースのもう1つの解決策を次に示します。このようにして、一度に1つのチャンクを格納し、文字列をスライスするよりもはるかに高速に実行します。

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

非常に大規模な文字列の場合、これははるかにそれを行うための最善の方法。これは、メモリに文字列全体を読んで、避けるだろうErrno::EINVALようなエラーInvalid argument @ io_freadとしますInvalid argument @ io_write
Joshua Pinter

2

約593MBのデータを1899132KBの断片に切り刻む小さなテストを行いました。ctrl + Cを押す前に、スライス+マップバージョンが100%CPUを使用して少なくとも15分間実行されました。String#unpackを使用したこのバージョンは3.6秒で終了しました。

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

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

それ以外の場合はセット間の空白が含まれるため、拒否が必要です。私の正規表現-fuは、頭のてっぺんからそれを修正する方法を完全に理解していません。


スキャンアプローチは、一致しない文字を忘れます。つまり、3つの部分で10の長さの文字列スライスを試してみると、3つの部分があり、1つの要素が削除されますが、アプローチはそれを行わないので、最善です。
vinicius gati 2014年

1

チャンクサイズよりも小さい可能性がある文字列の最後の部分を考慮に入れるより良い解決策:

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

他に考えている制約はありますか?そうでなければ、私はひどく次のような単純なことをしたくなるでしょう

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

シンプルでエレガント、そして効率的なものを持っていることを除けば、私は実際には何の制約もありません。私はあなたのアイデアが好きですが、それをメソッドに変換していただけませんか?[0..10]はおそらくもう少し複雑になるでしょう。
miniQuark 2009

str [i w ...(i + 1)* w]の代わりにstr [i w 、w]を使用するように例を修正しました。Tx
MiniQuark 2009

これは、[0..10] .eachではなく(1..10).collectである必要があります。[1..10]は、1つの要素(範囲)で構成される配列です。(1..10)は範囲そのものです。また、+ each +は、ブロックによって返される値ではなく、呼び出された元のコレクション(この場合は[1..10])を返します。ここに+ map +が必要です。
チャック

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.