Ruby - convertit élégamment une variable en tableau si ce n'est déjà un tableau

120

Étant donné un tableau, un seul élément ou nil, obtenez un tableau - les deux derniers étant respectivement un tableau d'élément unique et un tableau vide.

J'ai pensé à tort que Ruby fonctionnerait de cette façon:

[1,2,3].to_a  #= [1,2,3]     # Already an array, so no change
1.to_a        #= [1]         # Creates an array and adds element
nil.to_a      #= []          # Creates empty array

Mais ce que vous obtenez vraiment, c'est:

[1,2,3].to_a  #= [1,2,3]         # Hooray
1.to_a        #= NoMethodError   # Do not want
nil.to_a      #= []              # Hooray

Donc, pour résoudre cela, je dois soit utiliser une autre méthode, soit je pourrais utiliser un méta-programme en modifiant la méthode to_a de toutes les classes que j'ai l'intention d'utiliser - ce qui n'est pas une option pour moi.

Donc une méthode c'est:

result = nums.class == "Array".constantize ? nums : (nums.class == "NilClass".constantize ? [] : ([]<<nums))

Le problème est que c'est un peu le bordel. Y a-t-il une manière élégante de faire cela? (Je serais étonné si c'est la façon rubis de résoudre ce problème)


Quelles applications cela a-t-il? Pourquoi même convertir en un tableau?

Dans ActiveRecord de Rails, appeler par exemple user.postsrenverra un tableau de messages, un seul article ou nil. Lors de l'écriture de méthodes qui fonctionnent sur les résultats de ceci, il est plus facile de supposer que la méthode prendra un tableau, qui peut avoir zéro, un ou plusieurs éléments. Exemple de méthode:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}
xxjjnn
la source
2
user.postsne devrait jamais renvoyer un seul message. Au moins, je ne l'ai jamais vu.
Sergio Tulentsev
1
Je pense que dans vos deux premiers blocs de code, vous voulez dire ==au lieu de =, non?
Patrick Oscity
3
Btw, [1,2,3].to_ane revient pas[[1,2,3]] ! Il revient [1,2,3].
Patrick Oscity
Merci paddle, mettra à jour la question ... facepalms at self
xxjjnn

Réponses:

153

[*foo]ou Array(foo)fonctionnera la plupart du temps, mais dans certains cas, comme un hachage, cela le gâche.

Array([1, 2, 3])    # => [1, 2, 3]
Array(1)            # => [1]
Array(nil)          # => []
Array({a: 1, b: 2}) # => [[:a, 1], [:b, 2]]

[*[1, 2, 3]]    # => [1, 2, 3]
[*1]            # => [1]
[*nil]          # => []
[*{a: 1, b: 2}] # => [[:a, 1], [:b, 2]]

La seule façon dont je peux penser que cela fonctionne même pour un hachage est de définir une méthode.

class Object; def ensure_array; [self] end end
class Array; def ensure_array; to_a end end
class NilClass; def ensure_array; to_a end end

[1, 2, 3].ensure_array    # => [1, 2, 3]
1.ensure_array            # => [1]
nil.ensure_array          # => []
{a: 1, b: 2}.ensure_array # => [{a: 1, b: 2}]
Sawa
la source
2
au lieu de ensure_array, prolongerto_a
Dan Grahn
9
@screenmutt Cela affecterait les méthodes qui reposent sur l'utilisation d'origine de to_a. Par exemple, {a: 1, b: 2}.each ...cela fonctionnerait différemment.
sawa
1
Pouvez-vous expliquer cette syntaxe? Depuis de nombreuses années de Ruby, je n'avais jamais rencontré ce type d'invocation. Que font les parenthèses sur un nom de classe? Je ne trouve pas cela dans la documentation.
mastaBlasta
1
@mastaBlasta Array (arg) tente de créer un nouveau tableau en appelant to_ary, puis to_a sur l'argument. Ceci est documenté dans la documentation officielle de Ruby. Je l'ai appris dans le livre "Confident Ruby" d'Avdi.
mambo le
2
@mambo À un moment donné, après avoir posté ma question, j'ai trouvé la réponse. Le plus dur était que cela n'avait rien à voir avec la classe Array mais c'est une méthode sur le module Kernel. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta
119

Avec ActiveSupport (Rails): Array.wrap

Array.wrap([1, 2, 3])     # => [1, 2, 3]
Array.wrap(1)             # => [1]
Array.wrap(nil)           # => []
Array.wrap({a: 1, b: 2})  # => [{:a=>1, :b=>2}]

Si vous n'utilisez pas de rails, vous pouvez définir votre propre méthode similaire à la source des rails .

class Array
  def self.wrap(object)
    if object.nil?
      []
    elsif object.respond_to?(:to_ary)
      object.to_ary || [object]
    else
      [object]
    end
  end
end
elado
la source
12
class Array; singleton_class.send(:alias_method, :hug, :wrap); endpour plus de gentillesse.
rthbound
21

La solution la plus simple est d'utiliser [foo].flatten(1). Contrairement aux autres solutions proposées, cela fonctionnera bien pour les tableaux (imbriqués), les hachages et nil:

def wrap(foo)
  [foo].flatten(1)
end

wrap([1,2,3])         #= [1,2,3]
wrap([[1,2],[3,4]])   #= [[1,2],[3,4]]
wrap(1)               #= [1]
wrap(nil)             #= [nil]
wrap({key: 'value'})  #= [{key: 'value'}]
Oli
la source
Malheureusement, celui-ci a de sérieux problèmes de performances par rapport aux autres approches. Kernel#Arrayc'est-à Array()- dire le plus rapide de tous. Comparaison Ruby 2.5.1: Array (): 7936825.7 i / s. Array.wrap: 4199036.2 i / s - 1.89x plus lent. wrap: 644030.4 i / s - 12.32x plus lent
Wasif Hossain
19

Array(whatever) devrait faire l'affaire

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]
Benjamin Gruenbaum
la source
14
ne fonctionnera pas pour Hash. Array ({a: 1, b: 2}) sera [[: a, 1], [: b, 2]]
davispuh
13

ActiveSupport (rails)

ActiveSupport a une méthode assez intéressante pour cela. Il est chargé de Rails, donc la meilleure façon de le faire:

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> nil

Splat (Ruby 1.9+)

L'opérateur splat ( *) annule le tableau d'un tableau, s'il le peut:

*[1,2,3] #=> 1, 2, 3 (notice how this DOES not have braces)

Bien sûr, sans tableau, il fait des choses bizarres, et les objets que vous "splatez" doivent être placés dans des tableaux. C'est un peu bizarre, mais cela signifie:

[*[1,2,3]] #=> [1, 2, 3]
[*5] #=> [5]
[*nil] #=> []
[*{meh: "meh"}] #=> [[:meh, "meh"], [:meh2, "lol"]]

Si vous ne disposez pas d'ActiveSupport, vous pouvez définir la méthode:

class Array
    def self.wrap(object)
        [*object]
    end
end

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> nil

Bien que, si vous prévoyez d'avoir de grands tableaux et moins de choses non-tableau, vous voudrez peut-être le changer - la méthode ci-dessus est lente avec de grands tableaux et peut même provoquer un débordement de votre pile (omg so meta). Quoi qu'il en soit, vous voudrez peut-être faire ceci à la place:

class Array
    def self.wrap(object)
        object.is_a? Array ? object : [*object]
    end
end

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> [nil]

J'ai aussi quelques benchmarks avec et sans l'opérateur teneray.

Ben Aubin
la source
Ne fonctionnera pas pour les grands tableaux. SystemStackError: stack level too deeppour les éléments 1M (rubis 2.2.3).
denis.peplin
@ denis.peplin semble que vous ayez une erreur StackOverflow: D - honnêtement, je ne suis pas sûr de ce qui s'est passé. Désolé.
Ben Aubin
J'ai récemment essayé Hash#values_atavec 1M d'arguments (en utilisant splat), et cela génère la même erreur.
denis.peplin
@ denis.peplin Fonctionne-t-il avec object.is_a? Array ? object : [*object]?
Ben Aubin
1
Array.wrap(nil)retourne []pas nil: /
Aeramor
7

Que diriez-vous

[].push(anything).flatten
Bruno Meira
la source
2
Oui, je pense que j'ai fini par utiliser [n'importe quoi] .flatten dans mon cas ... mais pour le cas général, cela
aplatira
1
[].push(anything).flatten(1)travaillerait! Il n'aplatit pas les tableaux imbriqués!
xxjjnn
2

Avec le risque de déclarer l'évidence, et sachant que ce n'est pas le sucre syntaxique le plus savoureux jamais vu sur la planète et les régions environnantes, ce code semble faire exactement ce que vous décrivez:

foo = foo.is_a?(Array) ? foo : foo.nil? ? [] : [foo]
Le Pellmeister
la source
1

vous pouvez écraser la méthode de tableau de Object

class Object
    def to_a
        [self]
    end
end

tout hérite de Object, donc to_a sera maintenant défini pour tout sous le soleil

runub
la source
3
patching de singe blasphématoire! Repentez-vous!
xxjjnn
1

J'ai parcouru toutes les réponses et la plupart du temps ne fonctionne pas dans ruby ​​2+

Mais elado a la solution la plus élégante à savoir

Avec ActiveSupport (Rails): Array.wrap

Array.wrap ([1, 2, 3]) # => [1, 2, 3]

Array.wrap (1) # => [1]

Array.wrap (nil) # => []

Array.wrap ({a: 1, b: 2}) # => [{: a => 1,: b => 2}]

Malheureusement, mais cela ne fonctionne pas non plus pour ruby ​​2+ car vous obtiendrez une erreur

undefined method `wrap' for Array:Class

Donc, pour résoudre ce problème, vous devez exiger.

nécessitent 'active_support / deprecation'

nécessitent 'active_support / core_ext / array / wrap'

Malware Skiddie
la source
0

Puisque la méthode #to_aexiste déjà pour les deux principales classes problématiques ( Nilet Hash), définissez simplement une méthode pour le reste en étendant Object:

class Object
    def to_a
        [self]
    end
end

et vous pouvez facilement appeler cette méthode sur n'importe quel objet:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []
Chaussure
la source
5
Je pense vraiment que monkey patcher une classe Ruby de base, en particulier un objet, doit être évité. Je vais donner un laissez-passer à ActiveSupport, alors considérez-moi comme un hypocrite. Les solutions ci-dessus par @sawa sont beaucoup plus viables que cela.
pho3nixf1re