Quelle est la meilleure façon de couper une chaîne en morceaux d'une longueur donnée en Ruby?

88

Je cherchais un moyen élégant et efficace de découper une chaîne en sous-chaînes d'une longueur donnée en Ruby.

Jusqu'à présent, le mieux que j'ai pu trouver est celui-ci:

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

Vous voudrez peut- chunk("", n)être revenir [""]au lieu de []. Si tel est le cas, ajoutez simplement ceci comme première ligne de la méthode:

return [""] if string.empty?

Recommanderiez-vous une meilleure solution?

Éditer

Merci à Jeremy Ruten pour cette solution élégante et efficace: [edit: PAS efficace!]

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

Éditer

La solution string.scan prend environ 60 secondes pour découper 512k en morceaux de 1k 10000 fois, par rapport à la solution d'origine basée sur les tranches qui ne prend que 2,4 secondes.

MiniQuark
la source
Votre solution originale est à peu près aussi efficace et élégante que possible: il n'est pas nécessaire d'inspecter chaque caractère de la chaîne pour savoir où le couper, ni de transformer le tout en un tableau, puis de revenir en arrière.
android.weasel

Réponses:

158

Utilisez 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
la source
Ok, maintenant c'est excellent! Je savais qu'il devait y avoir un meilleur moyen. Merci beaucoup Jeremy Ruten.
MiniQuark
3
def chunk (chaîne, taille); string.scan (/. {1, # {taille}} /); fin
MiniQuark
1
Wow, je me sens stupide maintenant. Je n'ai même jamais pris la peine de vérifier le fonctionnement du scan.
Chuck
18
Soyez prudent avec cette solution; ceci est une expression régulière, et le /.bit de celui-ci signifie qu'elle inclura tous les caractères SAUF les retours à la ligne \n. Si vous souhaitez inclure des nouvelles lignes, utilisezstring.scan(/.{4}/m)
professormeowingtons
1
Quelle solution intelligente! J'adore les regexps mais je n'aurais pas pensé utiliser le quantificateur à cette fin. Merci Jeremy Ruten
Cec
18

Voici une autre façon de procéder:

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

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

Jason
la source
15
Alternativement:"abcdefghijklmnopqrstuvwxyz".chars.each_slice(3).map(&:join)
Finbarr
3
J'aime celui-ci car il fonctionne sur des chaînes contenant des retours à la ligne.
Steve Davis
1
Cela devrait être la solution acceptée. L'utilisation de l'analyse peut supprimer le dernier jeton si la longueur ne correspond pas au modèle .
compter0
6

Je pense que c'est la solution la plus efficace si vous savez que votre chaîne est un multiple de la taille du morceau

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

et pour les pièces

def parts(string, count)
    size = string.length / count
    count.times.collect { |i| string[i * size, size] }
end
Davispuh
la source
3
Votre chaîne ne doit pas nécessairement être un multiple de la taille du bloc si vous remplacez string.length / sizepar (string.length + size - 1) / size- ce modèle est courant dans le code C qui doit gérer la troncature d'entiers.
azote
3

Voici une autre solution pour un cas légèrement différent, lors du traitement de grandes chaînes et il n'est pas nécessaire de stocker tous les blocs à la fois. De cette façon, il stocke un seul morceau à la fois et fonctionne beaucoup plus rapidement que le découpage de chaînes:

io = StringIO.new(string)
until io.eof?
  chunk = io.read(chunk_size)
  do_something(chunk)
end
prcu
la source
Pour les très grosses cordes, c'est de loin la meilleure façon de le faire . Cela évitera de lire la chaîne entière en mémoire et d'obtenir des Errno::EINVALerreurs comme Invalid argument @ io_freadet Invalid argument @ io_write.
Joshua Pinter
2

J'ai fait un petit test qui découpe environ 593 Mo de données en 18991 morceaux de 32 Ko. Votre version slice + map a fonctionné pendant au moins 15 minutes en utilisant 100% du processeur avant d'appuyer sur ctrl + C. Cette version utilisant String # unpack s'est terminée en 3,6 secondes:

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

Le rejet est nécessaire car il inclut sinon l'espace vide entre les ensembles. Mon regex-fu n'est pas tout à fait à la hauteur de voir comment résoudre ce problème du haut de ma tête.

Mandrin
la source
l'approche de numérisation oubliera les caractères non correspondants, c'est-à-dire: si vous essayez avec une tranche de chaîne de 10 longueurs sur 3 parties, vous aurez 3 parties et 1 élément sera abandonné, votre approche ne le fait pas, donc c'est mieux.
vinicius gati
1

Une meilleure solution qui prend en compte la dernière partie de la chaîne qui pourrait être inférieure à la taille du morceau:

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
la source
0

Avez-vous d'autres contraintes en tête? Sinon, je serais terriblement tenté de faire quelque chose de simple comme

[0..10].each {
   str[(i*w),w]
}
Charlie Martin
la source
Je n'ai pas vraiment de contrainte, à part avoir quelque chose de simple, élégant et efficace. J'aime votre idée, mais pourriez-vous la traduire en méthode s'il vous plaît? Le [0..10] deviendrait probablement un peu plus complexe.
MiniQuark
J'ai corrigé mon exemple pour utiliser str [i w, w] au lieu de str [i w ... (i + 1) * w]. Tx
MiniQuark
Cela devrait être (1..10) .collect plutôt que [0..10] .each. [1..10] est un tableau constitué d'un élément - une plage. (1..10) est la plage elle-même. And + each + renvoie la collection d'origine sur laquelle il est appelé ([1..10] dans ce cas) plutôt que les valeurs renvoyées par le bloc. Nous voulons + carte + ici.
Chuck
0

text.scan(/.{1,4}/m)Résout juste le problème

Vyacheslav
la source