Apa cara terbaik untuk memotong string menjadi potongan dengan panjang tertentu di Ruby?

89

Saya telah mencari cara yang elegan dan efisien untuk memotong string menjadi substring dengan panjang tertentu di Ruby.

Sejauh ini, yang terbaik yang bisa saya dapatkan adalah ini:

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

Anda mungkin ingin chunk("", n)kembali, [""]bukan []. Jika demikian, tambahkan saja ini sebagai baris pertama dari metode ini:

return [""] if string.empty?

Apakah Anda akan merekomendasikan solusi yang lebih baik?

Sunting

Terima kasih kepada Jeremy Ruten untuk solusi yang elegan dan efisien ini: [edit: TIDAK efisien!]

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

Sunting

Solusi string.scan membutuhkan waktu sekitar 60 detik untuk memotong 512k menjadi 1k potongan 10000 kali, dibandingkan dengan solusi berbasis slice asli yang hanya membutuhkan 2,4 detik.

MiniQuark
sumber
Solusi asli Anda seefisien dan seelegan mungkin: tidak perlu memeriksa setiap karakter string untuk mengetahui di mana harus memotongnya, atau perlu mengubah semuanya menjadi array dan kemudian kembali lagi.
android.weasel

Jawaban:

159

Penggunaan 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
sumber
Oke, sekarang ini luar biasa! Saya tahu pasti ada cara yang lebih baik. Terima kasih banyak, Jeremy Ruten.
MiniQuark
3
def chunk (string, size); string.scan (/. {1, # {size}} /); akhir
MiniQuark
1
Wow, saya merasa bodoh sekarang. Saya tidak pernah repot-repot memeriksa cara kerja pemindaian.
Chuck
18
Hati-hati dengan solusi ini; ini adalah regexp, dan /.sedikit artinya itu akan menyertakan semua karakter KECUALI baris baru \n. Jika Anda ingin memasukkan baris baru, gunakanstring.scan(/.{4}/m)
professormeowingtons
1
Solusi yang sangat cerdas! Saya suka regexps tetapi saya tidak akan pernah menggunakan pembilang untuk tujuan ini. Terima kasih Jeremy Ruten
Cec
18

Berikut cara lain untuk melakukannya:

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

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

Jason
sumber
16
Atau:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr
3
Saya suka yang ini karena berfungsi pada string yang berisi baris baru.
Steve Davis
1
Ini harus menjadi solusi yang diterima. Menggunakan scan mungkin menjatuhkan token terakhir jika panjangnya tidak cocok dengan pola .
hitung0
6

Saya pikir ini adalah solusi paling efisien jika Anda tahu string Anda adalah kelipatan ukuran chunk

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

dan untuk suku cadang

def parts(string, count)
    size = string.length / count
    count.times.collect { |i| string[i * size, size] }
end
davispuh
sumber
3
String Anda tidak harus berupa kelipatan ukuran potongan jika Anda mengganti string.length / sizedengan (string.length + size - 1) / size- pola ini umum terjadi pada kode C yang harus berurusan dengan pemotongan integer.
nitrogen
3

Berikut adalah satu solusi lain untuk kasus yang sedikit berbeda, saat memproses string besar dan tidak perlu menyimpan semua potongan sekaligus. Dengan cara ini, ia menyimpan potongan tunggal pada satu waktu dan bekerja lebih cepat daripada mengiris string:

io = StringIO.new(string)
until io.eof?
  chunk = io.read(chunk_size)
  do_something(chunk)
end
prcu
sumber
Untuk string yang sangat besar, ini adalah jauh yang cara terbaik untuk melakukannya . Ini akan menghindari pembacaan seluruh string ke dalam memori dan mendapatkan Errno::EINVALkesalahan seperti Invalid argument @ io_freaddan Invalid argument @ io_write.
Joshua Pinter
2

Saya membuat tes kecil yang memotong sekitar 593MB data menjadi 18991 32KB potongan. Versi slice + map Anda berjalan setidaknya selama 15 menit menggunakan CPU 100% sebelum saya menekan ctrl + C. Versi ini menggunakan String # membongkar selesai dalam 3,6 detik:

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

Penolakan diperlukan karena jika tidak termasuk ruang kosong di antara set. Regex-fu saya tidak cukup untuk melihat bagaimana cara memperbaikinya langsung dari pikiran saya.

Membuang
sumber
pendekatan pemindaian akan melupakan karakter yang tidak cocok, yaitu: jika Anda mencoba dengan potongan string 10 panjang pada 3 bagian, Anda akan memiliki 3 bagian dan 1 elemen akan dijatuhkan, pendekatan Anda tidak melakukannya, jadi yang terbaik.
vinicius gati
1

Solusi yang lebih baik yang memperhitungkan bagian terakhir dari string yang bisa lebih kecil dari ukuran potongan:

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
sumber
0

Apakah ada kendala lain yang Anda pikirkan? Jika tidak, saya akan tergoda untuk melakukan sesuatu yang sederhana seperti

[0..10].each {
   str[(i*w),w]
}
Charlie Martin
sumber
Saya tidak memiliki kendala apa pun, selain memiliki sesuatu yang sederhana, elegan, dan efisien. Saya suka ide Anda, tetapi bisakah Anda menerjemahkannya ke dalam metode? [0..10] mungkin akan menjadi sedikit lebih kompleks.
MiniQuark
Saya memperbaiki contoh saya untuk menggunakan str [i w, w] daripada str [i w ... (i + 1) * w]. Tx
MiniQuark
Ini harus (1..10) .collect daripada [0..10] .each. [1..10] adalah larik yang terdiri dari satu elemen - rentang. (1..10) adalah kisaran itu sendiri. Dan + setiap + mengembalikan koleksi asli yang dipanggil ([1..10] dalam hal ini) daripada nilai yang dikembalikan oleh blok. Kami ingin + peta + di sini.
Chuck
0

Hanya text.scan(/.{1,4}/m)menyelesaikan masalah

Vyacheslav
sumber