อะไรคือวิธีที่ดีที่สุดในการตัดสตริงเป็นชิ้น ๆ ตามความยาวที่กำหนดใน Ruby?


89

ฉันกำลังมองหาวิธีที่สวยงามและมีประสิทธิภาพในการรวมสตริงเป็นสตริงย่อยที่มีความยาวที่กำหนดใน 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?

คุณจะแนะนำวิธีแก้ปัญหาที่ดีกว่านี้หรือไม่?

แก้ไข

ขอบคุณ Jeremy Ruten สำหรับโซลูชันที่หรูหราและมีประสิทธิภาพนี้: [แก้ไข: ไม่มีประสิทธิภาพ!]

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

แก้ไข

โซลูชัน string.scan ใช้เวลาประมาณ 60 วินาทีในการสับ 512k เป็น 1k ชิ้น 10,000 ครั้งเมื่อเทียบกับโซลูชันที่ใช้สไลซ์ดั้งเดิมซึ่งใช้เวลาเพียง 2.4 วินาที


โซลูชันดั้งเดิมของคุณมีประสิทธิภาพและสวยงามที่สุดเท่าที่จะเป็นไปได้: ไม่จำเป็นต้องตรวจสอบอักขระแต่ละตัวของสตริงเพื่อให้ทราบว่าจะสับมันตรงไหนและไม่จำเป็นต้องเปลี่ยนสิ่งทั้งหมดให้เป็นอาร์เรย์แล้วกลับมาอีกครั้ง
android.weasel

คำตอบ:


159

ใช้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
MiniQuark

3
def ชิ้น (สตริงขนาด); string.scan (/. {1, # {size}} /); สิ้นสุด
MiniQuark

1
ว้าวตอนนี้ฉันรู้สึกโง่ ฉันไม่เคยใส่ใจที่จะตรวจสอบว่าการสแกนทำงานอย่างไร
Chuck

18
ระวังด้วยวิธีนี้ นี้อยู่กับ regexp และบิตของมันหมายความว่ามันจะรวมถึงตัวละครทุกตัวยกเว้นการขึ้นบรรทัดใหม่/. \nหากคุณต้องการรวมบรรทัดใหม่ให้ใช้string.scan(/.{4}/m)
Professormeowingtons

1
ช่างเป็นทางออกที่ชาญฉลาดจริงๆ! ฉันชอบ regexps แต่ฉันคงไม่ได้ใช้ตัวระบุจำนวนเพื่อจุดประสงค์นี้ ขอบคุณ Jeremy Ruten
Cec

18

นี่เป็นอีกวิธีหนึ่งที่ทำได้:

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

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


16
อีกทางเลือกหนึ่ง:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr

3
ฉันชอบอันนี้เพราะมันใช้ได้กับสตริงที่มีขึ้นบรรทัดใหม่
Steve Davis

1
นี่ควรเป็นทางออกที่ได้รับการยอมรับ โดยใช้การสแกนอาจวางโทเค็นที่ผ่านมาหากความยาวจะไม่ตรงกับรูปแบบ
นับ

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

ฉันทำการทดสอบเล็กน้อยที่สับข้อมูลประมาณ 593MB เป็นชิ้นส่วน 32KB 18991 เวอร์ชัน slice + map ของคุณทำงานเป็นเวลาอย่างน้อย 15 นาทีโดยใช้ CPU 100% ก่อนที่ฉันจะกด ctrl + C เวอร์ชันนี้ใช้ 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?}

การปฏิเสธเป็นสิ่งที่จำเป็นเนื่องจากมีช่องว่างระหว่างชุด regex-fu ของฉันไม่ค่อยเห็นวิธีการแก้ไขที่ด้านบนของหัว


การสแกนจะลืมเกี่ยวกับ caracteres ที่ไม่ตรงกันกล่าวคือ: ถ้าคุณลองใช้สตริงความยาว 10 ชิ้นใน 3 ส่วนคุณจะมี 3 ส่วนและ 1 องค์ประกอบจะหลุดออกไป aproach ของคุณจะไม่ทำเช่นนั้นดังนั้นดีที่สุด
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] แต่ละอัน [1..10] คืออาร์เรย์ที่ประกอบด้วยองค์ประกอบหนึ่ง - ช่วง (1..10) คือช่วงนั่นเอง และ + each + ส่งคืนคอลเลคชันดั้งเดิมที่เรียกใช้ ([1..10] ในกรณีนี้) แทนที่จะเป็นค่าที่บล็อกส่งคืน เราต้องการ + แผนที่ + ที่นี่
Chuck

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.