Ruby'de bir dizeyi belirli bir uzunluktaki parçalara ayırmanın en iyi yolu nedir?


89

Ruby'de belirli bir uzunluktaki alt dizelere bir dizge eklemenin zarif ve verimli bir yolunu arıyordum.

Şimdiye kadar bulabildiğim en iyi şey şudur:

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

Bunun yerine chunk("", n)geri dönmek isteyebilirsiniz . Öyleyse, bunu yöntemin ilk satırı olarak eklemeniz yeterlidir:[""][]

return [""] if string.empty?

Daha iyi bir çözüm önerir misiniz?

Düzenle

Bu zarif ve etkili çözüm için Jeremy Ruten'e teşekkürler: [düzenleme: Verimli DEĞİL!]

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

Düzenle

String.scan çözümü, yalnızca 2,4 saniye süren orijinal dilim tabanlı çözüme kıyasla 512 bin parçayı 10000 kez 1 bin parçaya ayırmak yaklaşık 60 saniye sürer.


Orijinal çözümünüz olabildiğince verimli ve zariftir: Nerede kesileceğini bilmek için dizenin her bir karakterini incelemeye veya her şeyi bir diziye ve ardından tekrar geri dönüştürmeye gerek yoktur.
android.weasel

Yanıtlar:


159

Kullanım 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"]

Tamam, şimdi bu mükemmel! Daha iyi bir yol olması gerektiğini biliyordum. Çok teşekkürler Jeremy Ruten.
MiniQuark

3
def yığın (dizge, boyut); string.scan (/. {1, # {size}} /); end
MiniQuark

1
Vay canına, şimdi kendimi aptal gibi hissediyorum. Taramanın nasıl çalıştığını kontrol etme zahmetine bile girmedim.
Chuck

18
Bu çözüme dikkat edin; bu bir normal ifadedir ve /.biraz da yeni satırlar HARİÇ tüm karakterleri içereceği anlamına gelir \n. Satırsonu eklemek istiyorsanız, şunu kullanınstring.scan(/.{4}/m)
professormeowingtons

1
Ne akıllıca bir çözüm! Normal ifadeleri seviyorum, ancak bu amaç için nicelik belirtecini kullanmak zorunda kalmazdım. Teşekkürler Jeremy Ruten
Cec

18

İşte bunu yapmanın başka bir yolu:

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

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


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

3
Bunu beğendim çünkü satırsonu içeren dizelerde çalışıyor.
Steve Davis

1
Kabul edilen çözüm bu olmalıdır. Uzunluk, kalıba uymuyorsa, taramanın kullanılması son belirteci düşebilir .
sayı 0

6

Dizinizin yığın boyutunun katları olduğunu biliyorsanız, bunun en etkili çözüm olduğunu düşünüyorum.

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

ve parçalar için

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

3
İle değiştirirseniz string.length / size, dizenizin yığın boyutunun bir katı olması gerekmez (string.length + size - 1) / size- bu şablon, tamsayı kesmeyle uğraşması gereken C kodunda yaygındır.
nitrogen

3

Burada, büyük dizeleri işlerken biraz farklı durum için başka bir çözüm var ve bir seferde tüm parçaları depolamaya gerek yok. Bu şekilde, bir seferde tek parçayı depolar ve dizeleri dilimlemekten çok daha hızlı performans gösterir:

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

Çok büyük dizeleri için bu kadar uzak bunu yapmanın en iyi yolu . Bu belleğe tüm dizeyi okuyup almamak olacak Errno::EINVALgibi hatalar Invalid argument @ io_freadve Invalid argument @ io_write.
Joshua Pinter

2

Yaklaşık 593MB veriyi 18991 32KB parçalara bölen küçük bir test yaptım. Dilim + harita sürümünüz ctrl + C'ye basmadan önce% 100 CPU kullanarak en az 15 dakika çalıştı. String # unpack kullanan bu sürüm 3.6 saniyede tamamlandı:

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

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

Red, aksi takdirde setler arasındaki boş alanı içerdiği için gereklidir. Benim regex-fu'm, bunu aklıma getirmeden nasıl düzelteceğimi görmekle yetinmiyor.


tarama yaklaşımı eşleşmeyen karakterleri unutacaktır, yani: 3 parça üzerinde 10 uzunlukta bir dizi dilimini denerseniz, 3 parçanız olacak ve 1 eleman düşülecektir, yaklaşımınız bunu yapmaz, bu yüzden en iyisi.
vinicius gati

1

Dizenin yığın boyutundan daha küçük olabilecek son bölümünü hesaba katan daha iyi bir çözüm:

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

Aklınızda bulunan başka kısıtlamalar var mı? Aksi takdirde, şunun gibi basit bir şey yapma konusunda son derece cazip olurdum

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

Basit, zarif ve verimli bir şeye sahip olmak dışında gerçekten herhangi bir kısıtlamam yok. Fikrini beğendim, ama lütfen onu bir yönteme çevirir misin? [0..10] muhtemelen biraz daha karmaşık hale gelecektir.
MiniQuark

Örneğimi str [i w ... (i + 1) * w] yerine str [i w, w] kullanacak şekilde düzelttim . Tx
MiniQuark

Bu, [0..10] .each yerine (1..10) .collect olmalıdır. [1..10], bir öğeden - bir aralıktan oluşan bir dizidir. (1..10) aralığın kendisidir. Ve + her +, blok tarafından döndürülen değerlerden ziyade çağrıldığı orijinal koleksiyonu (bu durumda [1..10]) döndürür. Burada + harita + istiyoruz.
Chuck

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.