Jaki jest najlepszy sposób na pocięcie sznurka na kawałki określonej długości w Rubim?

88

Szukałem eleganckiego i wydajnego sposobu na podzielenie sznurka na podciągi o określonej długości w Rubim.

Jak dotąd najlepsze, co mogłem wymyślić, to:

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

Może chcesz chunk("", n), aby powrócić [""]zamiast []. Jeśli tak, po prostu dodaj to jako pierwszy wiersz metody:

return [""] if string.empty?

Czy poleciłbyś jakieś lepsze rozwiązanie?

Edytować

Podziękowania dla Jeremy'ego Rutena za to eleganckie i wydajne rozwiązanie: [edytuj: NIE wydajne!]

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

Edytować

Rozwiązanie string.scan zajmuje około 60 sekund, aby posiekać 512k na 1k kawałków 10000 razy, w porównaniu z oryginalnym rozwiązaniem opartym na plasterkach, które zajmuje tylko 2,4 sekundy.

MiniQuark
źródło
Twoje oryginalne rozwiązanie jest tak wydajne i eleganckie, jak to tylko możliwe: nie ma potrzeby sprawdzania każdego znaku ciągu, aby wiedzieć, gdzie go pociąć, ani żadnej potrzeby przekształcania całości w tablicę, a następnie z powrotem.
android.weasel

Odpowiedzi:

158

Zastosowanie 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
źródło
Ok, teraz jest świetnie! Wiedziałem, że musi być lepszy sposób. Wielkie dzięki, Jeremy Ruten.
MiniQuark
3
def chunk (string, size); string.scan (/. {1, # {rozmiar}} /); koniec
MiniQuark
1
Wow, teraz czuję się głupio. Nigdy nawet nie zadałem sobie trudu, żeby sprawdzić, jak działa skan.
Chuck
18
Uważaj na to rozwiązanie; to jest wyrażenie regularne i jego /.bit oznacza, że ​​będzie zawierał wszystkie znaki Z WYJĄTKIEM nowych linii \n. Jeśli chcesz dołączyć nowe linie, użyjstring.scan(/.{4}/m)
professormeowingtons
1
Cóż za sprytne rozwiązanie! Uwielbiam wyrażenia regularne, ale nie pomyślałbym o używaniu kwantyfikatora do tego celu. Dziękuję Jeremy Ruten
Cec
18

Oto inny sposób, aby to zrobić:

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

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

Jason
źródło
15
Alternatywnie:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr
3
Podoba mi się ten, ponieważ działa na łańcuchach zawierających znaki nowej linii.
Steve Davis
1
To powinno być przyjęte rozwiązanie. Użycie skanowania może spowodować utratę ostatniego tokena, jeśli długość nie będzie pasować do wzorca .
count0
6

Myślę, że jest to najbardziej wydajne rozwiązanie, jeśli wiesz, że twój ciąg jest wielokrotnością rozmiaru fragmentu

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

i na części

def parts(string, count)
    size = string.length / count
    count.times.collect { |i| string[i * size, size] }
end
davispuh
źródło
3
Twój ciąg nie musi być wielokrotnością rozmiaru fragmentu, jeśli zastąpisz string.length / sizego (string.length + size - 1) / size- ten wzorzec jest powszechny w kodzie C, który ma do czynienia z obcinaniem liczb całkowitych.
azot
3

Oto kolejne rozwiązanie dla nieco innego przypadku, podczas przetwarzania dużych ciągów i nie ma potrzeby przechowywania wszystkich fragmentów naraz. W ten sposób przechowuje pojedyncze fragmenty na raz i działa znacznie szybciej niż krojenie ciągów:

io = StringIO.new(string)
until io.eof?
  chunk = io.read(chunk_size)
  do_something(chunk)
end
prcu
źródło
W przypadku bardzo dużych ciągów, jest to zdecydowanie Najlepszym sposobem, aby to zrobić . Pozwoli to uniknąć wczytywania całego ciągu do pamięci i Errno::EINVALbłędów, takich jak Invalid argument @ io_freadi Invalid argument @ io_write.
Joshua Pinter
2

Zrobiłem mały test, który dzieli około 593 MB danych na 18991 32 KB części. Twoja wersja plasterka + mapy działała przez co najmniej 15 minut przy 100% CPU, zanim nacisnąłem ctrl + C. Ta wersja przy użyciu String # unpack zakończyła się w 3,6 sekundy:

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

Odrzucenie jest konieczne, ponieważ w przeciwnym razie obejmuje spację między zestawami. Moje regex-fu nie jest do końca gotowe, aby zobaczyć, jak to naprawić od razu na czubku mojej głowy.

Gdakanie
źródło
metoda skanowania zapomni o niedopasowanych karakterach, tj .: jeśli spróbujesz z kawałkiem struny o długości 10 na 3 części, będziesz miał 3 części i 1 element zostanie odrzucony, twoje podejście nie robi tego, więc najlepiej.
vinicius gati
1

Lepsze rozwiązanie uwzględniające ostatnią część ciągu, która może być mniejsza niż rozmiar fragmentu:

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
źródło
0

Czy masz na myśli jakieś inne ograniczenia? W przeciwnym razie bardzo bym się kusił, żeby zrobić coś prostego

[0..10].each {
   str[(i*w),w]
}
Charlie Martin
źródło
Nie mam żadnych ograniczeń, poza tym, że mam coś prostego, eleganckiego i wydajnego. Podoba mi się twój pomysł, ale czy mógłbyś przełożyć go na metodę? [0..10] prawdopodobnie stałoby się nieco bardziej złożone.
MiniQuark
Naprawiłem mój przykład, aby użyć str [i w, w] zamiast str [i w ... (i + 1) * w]. Tx
MiniQuark
Powinno to być (1..10) .collect, a nie [0..10] .each. [1..10] to tablica składająca się z jednego elementu - zakresu. (1..10) to sam zakres. A + each + zwraca oryginalną kolekcję, z której jest wywołana (w tym przypadku [1..10]), a nie wartości zwrócone przez blok. Chcemy + mapa + tutaj.
Chuck
0

Po prostu text.scan(/.{1,4}/m)rozwiązuje problem

Wiaczesław
źródło