Ruby: convierta elegantemente la variable en una matriz si no es una matriz ya

120

Dada una matriz, un solo elemento o nulo, obtenga una matriz; los dos últimos son una matriz de un solo elemento y una matriz vacía, respectivamente.

Pensé erróneamente que Ruby funcionaría de esta manera:

[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

Pero lo que realmente obtienes es:

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

Entonces, para resolver esto, necesito usar otro método, o podría programar un meta modificando el método to_a de todas las clases que pretendo usar, lo cual no es una opción para mí.

Entonces un método es:

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

El problema es que está un poco desordenado. ¿Existe una forma elegante de hacer esto? (Me sorprendería si esta es la forma Ruby-ish de resolver este problema)


¿Qué aplicaciones tiene esto? ¿Por qué incluso convertir a una matriz?

En ActiveRecord de Rails, llamar a say, user.postsdevolverá una matriz de publicaciones, una sola publicación o nil. Al escribir métodos que funcionan con los resultados de esto, es más fácil asumir que el método tomará una matriz, que puede tener cero, uno o muchos elementos. Método de ejemplo:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}
xxjjnn
fuente
2
user.postsnunca debe devolver una sola publicación. Al menos, nunca lo vi.
Sergio Tulentsev
1
Creo que en tus dos primeros bloques de código te refieres en ==lugar de =, ¿verdad?
Patrick Oscity
3
Por cierto, [1,2,3].to_ano no volver [[1,2,3]]! Vuelve [1,2,3].
Patrick Oscity
Gracias paddle, actualizaré la pregunta ... facepalms at self
xxjjnn

Respuestas:

153

[*foo]o Array(foo)funcionará la mayor parte del tiempo, pero en algunos casos, como un hash, lo estropea.

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 única forma en que puedo pensar que funciona incluso para un hash es definir un método.

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
fuente
2
en lugar de ensure_array, extenderto_a
Dan Grahn
9
@screenmutt Eso afectaría a los métodos que se basan en el uso original de to_a. Por ejemplo, {a: 1, b: 2}.each ...funcionaría de manera diferente.
Sawa
1
¿Puedes explicar esta sintaxis? En muchos años de Ruby nunca me había encontrado con este tipo de invocación. ¿Qué hacen los paréntesis en el nombre de una clase? No puedo encontrar esto en los documentos.
mastaBlasta
1
@mastaBlasta Array (arg) intenta crear una nueva matriz llamando a to_ary, luego a to_a en el argumento. Esto está documentado en documentos oficiales de ruby. Lo aprendí del libro "Confident Ruby" de Avdi.
mambo
2
@mambo En algún momento después de publicar mi pregunta encontré la respuesta. La parte difícil fue que no tiene nada que ver con la clase Array, pero es un método del módulo Kernel. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta
119

Con ActiveSupport (rieles): 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 no está utilizando Rails, puede definir su propio método similar al origen de 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
fuente
12
class Array; singleton_class.send(:alias_method, :hug, :wrap); endpara una ternura extra.
rthbound
21

La solución más sencilla es utilizar [foo].flatten(1). A diferencia de otras soluciones propuestas, funcionará bien para matrices (anidadas), hashes y 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
fuente
desafortunadamente, este tiene un problema de rendimiento serio en comparación con otros enfoques. es Kernel#Arraydecir, Array()es el más rápido de todos. Comparación de Ruby 2.5.1: Array (): 7936825.7 i / s. Array.wrap: 4199036.2 i / s - 1.89x más lento. wrap: 644030.4 i / s - 12.32x más lento
Wasif Hossain
19

Array(whatever) debería hacer el truco

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]
Benjamin Gruenbaum
fuente
14
no funcionará para Hash. Array ({a: 1, b: 2}) será [[: a, 1], [: b, 2]]
davispuh
13

ActiveSupport (rieles)

ActiveSupport tiene un método bastante bueno para esto. Está cargado con Rails, por lo que definitivamente es la mejor manera de hacer esto:

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

Splat (Ruby 1.9+)

El operador de splat ( *) desarma una matriz, si puede:

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

Por supuesto, sin una matriz, hace cosas raras, y los objetos que "splat" deben colocarse en matrices. Es algo extraño, pero significa:

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

Si no tiene ActiveSupport, puede definir el método:

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

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

Aunque, si planea tener arreglos grandes y menos cosas que no sean arreglos, es posible que desee cambiarlo: el método anterior es lento con arreglos grandes e incluso puede hacer que su pila se desborde (Dios mío, meta). De todos modos, es posible que desee hacer esto en su lugar:

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]

También tengo algunos puntos de referencia con y sin el operador teneray.

Ben Aubin
fuente
No funcionará para arreglos grandes. SystemStackError: stack level too deeppara 1M elementos (ruby 2.2.3).
denis.peplin
@ denis.peplin parece que tienes un error de StackOverflow: D - honestamente, no estoy seguro de qué sucedió. Lo siento.
Ben Aubin
Recientemente probé Hash#values_atcon 1 M de argumentos (usando splat) y arroja el mismo error.
denis.peplin
@ denis.peplin ¿Funciona con object.is_a? Array ? object : [*object]?
Ben Aubin
1
Array.wrap(nil)[]no devuelve nil: /
Aeramor
7

Qué tal si

[].push(anything).flatten
Bruno Meira
fuente
2
Sí, creo que terminé usando [cualquier cosa] .flatten en mi caso ... pero para el caso general, esto también aplanará cualquier estructura de matriz anidada
xxjjnn
1
[].push(anything).flatten(1)¡trabajaría! ¡No aplana las matrices anidadas!
xxjjnn
2

Con el riesgo de decir lo obvio, y sabiendo que este no es el azúcar sintáctico más sabroso jamás visto en el planeta y áreas circundantes, este código parece hacer exactamente lo que usted describe:

foo = foo.is_a?(Array) ? foo : foo.nil? ? [] : [foo]
El Pellmeister
fuente
1

puede sobrescribir el método de matriz de Object

class Object
    def to_a
        [self]
    end
end

todo hereda Object, por lo tanto to_a ahora se definirá para todo lo que hay bajo el sol

runub
fuente
3
parche de mono blasfemo! ¡Arrepiéntanse!
xxjjnn
1

He revisado todas las respuestas y la mayoría no funciona en ruby ​​2+

Pero elado tiene la solución más elegante, es decir

Con ActiveSupport (rieles): Array.wrap

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

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

Array.wrap (nulo) # => []

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

Lamentablemente, esto tampoco funciona para ruby ​​2+ ya que obtendrá un error

undefined method `wrap' for Array:Class

Entonces, para arreglar eso, necesita requerir.

requiere 'active_support / deprecation'

requiere 'active_support / core_ext / array / wrap'

Malware Skiddie
fuente
0

Dado que el método #to_aya existe para las dos clases problemáticas principales ( Nily Hash), simplemente defina un método para el resto extendiendo Object:

class Object
    def to_a
        [self]
    end
end

y luego puede llamar fácilmente a ese método en cualquier objeto:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []
Zapato
fuente
5
Realmente creo que se debe evitar que el mono parchee una clase central de Ruby, especialmente un objeto. Sin embargo, le daré un pase a ActiveSupport, así que considéreme un hipócrita. Las soluciones anteriores de @sawa son mucho más viables que esta.
pho3nixf1re