Was ist der beste Weg, um eine Schnur in Ruby in Stücke einer bestimmten Länge zu hacken?

85

Ich habe nach einer eleganten und effizienten Möglichkeit gesucht, eine Zeichenfolge in Ruby in Teilzeichenfolgen einer bestimmten Länge zu zerlegen.

Das Beste, was ich bisher finden konnte, ist Folgendes:

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

Vielleicht möchten Sie statt chunk("", n)zurückkehren . Wenn ja, fügen Sie dies einfach als erste Zeile der Methode hinzu:[""][]

return [""] if string.empty?

Würden Sie eine bessere Lösung empfehlen?

Bearbeiten

Vielen Dank an Jeremy Ruten für diese elegante und effiziente Lösung: [Bearbeiten: NICHT effizient!]

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

Bearbeiten

Die string.scan-Lösung benötigt ungefähr 60 Sekunden, um 512k 10000-mal in 1k-Chunks zu zerlegen, verglichen mit der ursprünglichen Slice-basierten Lösung, die nur 2,4 Sekunden dauert.

MiniQuark
quelle
Ihre ursprüngliche Lösung ist ungefähr so ​​effizient und elegant wie möglich: Sie müssen nicht jedes Zeichen der Zeichenfolge überprüfen, um zu wissen, wo sie zerhackt werden muss, und Sie müssen das Ganze nicht in ein Array verwandeln und dann wieder zurück.
android.weasel

Antworten:

156

Verwendung 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
quelle
Ok, jetzt ist das ausgezeichnet! Ich wusste, dass es einen besseren Weg geben musste. Vielen Dank Jeremy Ruten.
MiniQuark
3
def Chunk (String, Größe); string.scan (/. {1, # {size}} /); Ende
MiniQuark
1
Wow, ich fühle mich jetzt dumm. Ich habe mich noch nie darum gekümmert, zu überprüfen, wie der Scan funktioniert.
Chuck
17
Seien Sie vorsichtig mit dieser Lösung; Dies ist ein regulärer Ausdruck, und das /.Bit bedeutet, dass er alle Zeichen außer Zeilenumbrüchen enthält \n. Wenn Sie Zeilenumbrüche einfügen möchten, verwenden Siestring.scan(/.{4}/m)
professormeowingtons
1
Was für eine clevere Lösung! Ich liebe Regexps, aber ich hätte den Quantifizierer nicht für diesen Zweck verwendet. Vielen Dank, dass Sie Jeremy Ruten
Cec
18

Hier ist eine andere Möglichkeit:

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

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

Jason
quelle
15
Alternativ:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr
3
Ich mag dieses, weil es auf Zeichenfolgen funktioniert, die Zeilenumbrüche enthalten.
Steve Davis
1
Dies sollte die akzeptierte Lösung sein. Bei Verwendung des Scans wird möglicherweise das letzte Token gelöscht, wenn die Länge nicht mit dem Muster übereinstimmt .
count0
6

Ich denke, dies ist die effizienteste Lösung, wenn Sie wissen, dass Ihre Zeichenfolge ein Vielfaches der Blockgröße hat

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

und für Teile

def parts(string, count)
    size = string.length / count
    count.times.collect { |i| string[i * size, size] }
end
Davispuh
quelle
3
Ihre Zeichenfolge muss nicht ein Vielfaches von Chunkgröße sein , wenn Sie ersetzen string.length / sizemit (string.length + size - 1) / size- dieses Muster in C - Code gemeinsam ist , die mit ganzzahligen Abschneiden zu tun hat.
Stickstoff
2

Hier ist eine andere Lösung für einen etwas anderen Fall, wenn große Zeichenfolgen verarbeitet werden und nicht alle Blöcke gleichzeitig gespeichert werden müssen. Auf diese Weise speichert es jeweils einzelne Chunks und arbeitet viel schneller als das Schneiden von Strings:

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

Ich habe einen kleinen Test gemacht, bei dem ungefähr 593 MB Daten in 18991 32 KB große Teile zerlegt wurden. Ihre Slice + Map-Version lief mindestens 15 Minuten mit 100% CPU, bevor ich Strg + C drückte. Diese Version mit String # Unpack ist in 3,6 Sekunden fertig:

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

Die Zurückweisung ist erforderlich, da sie ansonsten das Leerzeichen zwischen den Sätzen enthält. Mein Regex-Fu ist nicht ganz in der Lage zu sehen, wie ich das direkt auf meinem Kopf beheben kann.

Futter
quelle
Der Scan-Ansatz vergisst nicht übereinstimmende Zeichen, dh: Wenn Sie es mit einem 10-Längen-String-Slice auf 3 Teilen versuchen, haben Sie 3 Teile und 1 Element wird gelöscht. Ihr Ansatz tut dies nicht, also ist es das Beste.
Vinicius Gati
1

Eine bessere Lösung, die den letzten Teil der Zeichenfolge berücksichtigt, der kleiner als die Blockgröße sein kann:

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
Kirkytulline
quelle
0

Gibt es noch andere Einschränkungen, an die Sie denken? Sonst wäre ich furchtbar versucht, so etwas Einfaches zu tun

[0..10].each {
   str[(i*w),w]
}
Charlie Martin
quelle
Ich habe keine wirklichen Einschränkungen, abgesehen von etwas Einfachem, Elegantem und Effizientem. Ich mag Ihre Idee, aber würde es Ihnen etwas ausmachen, sie in eine Methode zu übersetzen? Die [0..10] würde wahrscheinlich etwas komplexer werden.
MiniQuark
Ich habe mein Beispiel so korrigiert, dass str [i w, w] anstelle von str [i w ... (i + 1) * w] verwendet wird. Tx
MiniQuark
Dies sollte (1..10) .collect sein und nicht [0..10] .each. [1..10] ist ein Array, das aus einem Element besteht - einem Bereich. (1..10) ist der Bereich selbst. Und + jedes + gibt die ursprüngliche Sammlung zurück, auf die es aufgerufen wird (in diesem Fall [1..10]), und nicht die vom Block zurückgegebenen Werte. Wir wollen + Karte + hier.
Chuck
0

Der schnellste Weg ist mit regex.

"One way to get some characters of a string".scan(/.{number}/)
"One way to get some characters of a string".scan(/.{number}/)[0]

In der ersten Zeile wird das Ergebnis in einer Reihe von Array-Elementen gedruckt. Wenn Sie nur die erste haben möchten.

Beachten Sie, dass number dies eine Ganzzahl sein sollte. Ändern Sie auch unter [0]Berücksichtigung Ihrer Anforderung.

Esmaeil MIRZAEE
quelle