Ruby - converte elegantemente a variável em um array, se ainda não for um array

119

Dada uma matriz, um único elemento ou nil, obtenha uma matriz - os dois últimos sendo uma matriz de elemento único e uma matriz vazia, respectivamente.

Achei por engano que Ruby funcionaria desta forma:

[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

Mas o que você realmente consegue é:

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

Então, para resolver isso, eu preciso usar outro método ou poderia meta-programar modificando o método to_a de todas as classes que pretendo usar - o que não é uma opção para mim.

Portanto, é um método:

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

O problema é que é uma bagunça. Existe uma maneira elegante de fazer isso? (Eu ficaria surpreso se esta for a maneira Ruby de resolver este problema)


Quais aplicativos isso tem? Por que até mesmo converter para um array?

No ActiveRecord do Rails, chamar, digamos, user.postsretornará uma matriz de postagens, uma única postagem ou nil. Ao escrever métodos que funcionam nos resultados disso, é mais fácil assumir que o método terá um array, que pode ter zero, um ou muitos elementos. Método de exemplo:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}
xxjjnn
fonte
2
user.postsnunca deve retornar uma única postagem. Pelo menos, eu nunca vi isso.
Sergio Tulentsev
1
Acho que nos seus primeiros dois blocos de código você quer dizer em ==vez de =, certo?
Patrick Oscity de
3
Btw, [1,2,3].to_ase não voltar [[1,2,3]]! Ele retorna [1,2,3].
Patrick Oscity de
Obrigado, remo, atualizarei a pergunta ... facepalms at self
xxjjnn

Respostas:

152

[*foo]ou Array(foo)funcionará na maioria das vezes, mas em alguns casos, como um hash, bagunça tudo.

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]]

A única maneira de pensar que funciona mesmo para um hash é definir um 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
fonte
2
em vez de ensure_arrayestenderto_a
Dan Grahn
9
@screenmutt Isso afetaria métodos que dependem do uso original de to_a. Por exemplo, {a: 1, b: 2}.each ...funcionaria de forma diferente.
sawa de
1
Você pode explicar essa sintaxe? Em muitos anos de Ruby, eu nunca havia encontrado esse tipo de invocação. O que os parênteses no nome de uma classe fazem? Não consigo encontrar isso nos documentos.
mastaBlasta
1
@mastaBlasta Array (arg) tenta criar um novo array chamando to_ary e depois to_a no argumento. Isso está documentado nos documentos oficiais do Ruby. Aprendi sobre isso no livro "Ruby Confiante" de Avdi.
mambo
2
@mambo Em algum momento depois de postar minha pergunta, encontrei a resposta. A parte difícil é que não tem nada a ver com a classe Array, mas é um método no módulo Kernel. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta
119

Com 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}]

Se você não estiver usando Rails, você pode definir seu próprio método semelhante ao fonte do 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
fonte
12
class Array; singleton_class.send(:alias_method, :hug, :wrap); endpara fofura extra.
rthbound de
21

A solução mais simples é usar [foo].flatten(1). Ao contrário de outras soluções propostas, funcionará bem para arrays (aninhados), hashes e 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
fonte
infelizmente, este tem um sério problema de desempenho em comparação com outras abordagens. Kernel#Arrayou seja, Array()é o mais rápido de todos eles. Comparação Ruby 2.5.1: Array (): 7936825,7 i / s. Array.wrap: 4199036,2 i / s - 1,89x mais lento. wrap: 644030,4 i / s - 12,32x mais lento
Wasif Hossain de
19

Array(whatever) deve fazer o truque

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

ActiveSupport (Rails)

ActiveSupport tem um método muito bom para isso. É carregado com Rails, então, definitivamente, a maneira mais agradável de fazer isso:

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

Splat (Ruby 1.9+)

O operador splat ( *) remove a matriz de uma matriz, se puder:

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

Obviamente, sem um array, ele faz coisas estranhas, e os objetos que você "splat" precisam ser colocados em arrays. É um pouco estranho, mas significa:

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

Se você não tem ActiveSupport, pode definir o método:

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

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

Embora, se você planeja ter matrizes grandes, e menos coisas que não sejam matrizes, você pode querer alterá-lo - o método acima é lento com matrizes grandes e pode até causar o estouro de seu Stack (omg so meta). De qualquer forma, você pode querer fazer isso:

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]

Também tenho alguns benchmarks com e sem o operador teneray.

Ben Aubin
fonte
Não funciona para matrizes grandes. SystemStackError: stack level too deeppara elementos 1M (rubi 2.2.3).
denis.peplin
@ denis.peplin parece que você obteve um erro StackOverflow: D - honestamente, não tenho certeza do que aconteceu. Desculpe.
Ben Aubin
Recentemente, tentei Hash#values_atcom 1 milhão de argumentos (usando splat) e gerou o mesmo erro.
denis.peplin
@ denis.peplin Funciona com object.is_a? Array ? object : [*object]?
Ben Aubin de
1
Array.wrap(nil)[]não retorna nil: /
Aeramor
7

E se

[].push(anything).flatten
Bruno Meira
fonte
2
Sim, acho que acabei usando [qualquer coisa] .flatten no meu caso ... mas para o caso geral, isso também nivelará qualquer estrutura de array aninhada
xxjjnn
1
[].push(anything).flatten(1)trabalharia! Ele não nivela arrays aninhados!
xxjjnn
2

Com o risco de afirmar o óbvio e sabendo que este não é o açúcar sintático mais saboroso já visto no planeta e arredores, este código parece fazer exatamente o que você descreve:

foo = foo.is_a?(Array) ? foo : foo.nil? ? [] : [foo]
O pellmeister
fonte
1

você pode sobrescrever o método de array de Object

class Object
    def to_a
        [self]
    end
end

tudo herda Object, portanto to_a agora será definido para tudo sob o sol

runub
fonte
3
macacos blasfemos remendos! Arrependam-se!
xxjjnn
1

Eu analisei todas as respostas e a maioria não funciona no Ruby 2+

Mas elado tem a solução mais elegante, ou seja,

Com 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}]

Infelizmente, mas isso também não funciona para ruby ​​2+, pois você receberá um erro

undefined method `wrap' for Array:Class

Então, para consertar isso, você precisa exigir.

requer 'active_support / deprecation'

requer 'active_support / core_ext / array / wrap'

Malware Skiddie
fonte
0

Como o método #to_ajá existe para as duas principais classes problemáticas ( Nile Hash), basta definir um método para o resto estendendo Object:

class Object
    def to_a
        [self]
    end
end

e então você pode facilmente chamar esse método em qualquer objeto:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []
Sapato
fonte
5
Eu realmente acho que o patching de uma classe central do Ruby, especialmente de objeto, deve ser evitado. Vou dar uma chance ao ActiveSupport, então me considere um hipócrita. As soluções acima por @sawa são muito mais viáveis ​​do que isso.
pho3nixf1re