Який найкращий спосіб рубати струну на шматки заданої довжини в 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?

Чи не порекомендували б ви краще рішення?

Редагувати

Дякуємо Джеремі Рутену за це елегантне та ефективне рішення: [редагувати: НЕ ефективно!]

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

Редагувати

Рішенню string.scan потрібно приблизно 60 секунд, щоб розділити 512 тис. На 1 тис. Фрагментів 10000 разів, порівняно з оригінальним рішенням на основі зрізів, яке займає лише 2,4 секунди.


Ваше оригінальне рішення є настільки ефективним і елегантним, наскільки це можливо: немає необхідності перевіряти кожен символ рядка, щоб знати, де його порубати, і не потрібно перетворювати все це в масив, а потім повертатися назад.
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

3
def chunk (рядок, розмір); string.scan (/. {1, # {size}} /); кінець
MiniQuark

1
Ого, зараз я почуваюся дурною. Я ніколи навіть не потрудився перевірити, як працює сканування.
Чак,

18
Будьте обережні з цим розчином; це регулярний вираз, і його /.біт означає, що він буде включати всі символи, окрім нових рядків \n. Якщо ви хочете включити нові рядки, використовуйтеstring.scan(/.{4}/m)
professormeowingtons

1
Яке розумне рішення! Я люблю регулярні вирази, але не хотів би використовувати квантор для цієї мети. Дякую, Джеремі Рутен
Чек,

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

3
Мені подобається цей, оскільки він працює на рядки, які містять нові рядки.
Стів Девіс,

1
Це має бути прийнятим рішенням. Використання сканування може скинути останній маркер, якщо довжина не відповідає шаблону .
відлік0

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

Ось ще одне рішення для дещо іншого випадку, коли обробляються великі рядки, і немає необхідності зберігати всі фрагменти одночасно. Таким чином, він зберігає окремі шматки за раз і виконує набагато швидше, ніж нарізання рядків:

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.
Джошуа Пінтер,

2

Я зробив невеликий тест, який розбиває близько 593 МБ даних на 18991 32 КБ. Ваша версія фрагмента + карти працювала принаймні 15 хвилин із використанням 100% процесора, перш ніж я натиснув ctrl + C. Ця версія за допомогою розпакування String # закінчилася за 3,6 секунди:

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

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

Відхилення необхідне, оскільки воно в іншому випадку включає порожній простір між наборами. Мій регулярний вираз-фу не зовсім готовий бачити, як це виправити прямо на маківці.


підхід сканування забуде про невідповідні каратетери, тобто: якщо ви спробуєте нарізати фрагмент рядка 10 довжиною на 3 частини, у вас буде 3 частини, а 1 елемент буде скинуто, ваш підхід цього не робить, тому найкраще.
vinicius gati

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

Я виправив свій приклад, щоб використовувати str [i w, w] замість str [i w ... (i + 1) * w]. Tx
MiniQuark

Це має бути (1..10) .collect, а не [0..10] .each. [1..10] - це масив, що складається з одного елемента - діапазону. (1..10) - це сам діапазон. І + кожний + повертає вихідну колекцію, до якої він викликаний ([1..10] у цьому випадку), а не значення, які повертає блок. Ми хочемо + карта + тут.
Чак,

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.