Wie erstelle ich in Ruby einen Hash aus einem Array?

76

Ich habe ein einfaches Array:

arr = ["apples", "bananas", "coconuts", "watermelons"]

Ich habe auch eine Funktion f, die eine Operation an einer einzelnen Zeichenfolgeneingabe ausführt und einen Wert zurückgibt. Diese Operation ist sehr teuer, daher möchte ich die Ergebnisse im Hash speichern.

Ich weiß, dass ich mit so etwas den gewünschten Hash erstellen kann:

h = {}
arr.each { |a| h[a] = f(a) }

Was ich tun möchte, ist, h nicht initialisieren zu müssen, damit ich einfach so etwas schreiben kann:

h = arr.(???) { |a| a => f(a) }

Kann das gemacht werden?

Wizzlewott
quelle

Antworten:

128

Angenommen, Sie haben eine Funktion mit einem funtastischen Namen: "f"

def f(fruit)
   fruit + "!"
end

arr = ["apples", "bananas", "coconuts", "watermelons"]
h = Hash[ *arr.collect { |v| [ v, f(v) ] }.flatten ]

werde dir geben:

{"watermelons"=>"watermelons!", "bananas"=>"bananas!", "apples"=>"apples!", "coconuts"=>"coconuts!"}

Aktualisiert:

Wie in den Kommentaren erwähnt, führt Ruby 1.8.7 eine schönere Syntax dafür ein:

h = Hash[arr.collect { |v| [v, f(v)] }]
Mikrospino
quelle
Ich denke du meintest ... { |v| [v, f(v)] }, aber das hat den Trick gemacht!
Wizzlewott
3
Nur eine Sache - warum gibt es eine *neben *arr.collect?
Jeriko
3
@ Jeriko - Der Splat-Operator *sammelt je nach Kontext eine Liste in einem Array oder wickelt ein Array in einer Liste ab. Hier wird das Array in eine Liste abgewickelt (die als Elemente für den neuen Hash verwendet werden soll).
Telemachos
2
Nachdem Sie sich Jörgs Antwort angesehen und darüber nachgedacht haben, beachten Sie, dass Sie beide entfernen können *und flattenfür eine einfachere Version : h = Hash[ arr.collect { |v| [ v, f(v) ] } ]. Ich bin mir nicht sicher, ob es einen Fall gibt, den ich nicht sehe.
Telemachos
3
In Ruby 1.8.7 ist das Hässliche Hash[*key_pairs.flatten]einfach Hash[key_pairs]. Viel schöner und require 'backports'wenn Sie noch nicht von 1.8.6 aktualisiert haben.
Marc-André Lafortune
55

Habe einige schnelle, schmutzige Benchmarks für einige der gegebenen Antworten durchgeführt. (Diese Ergebnisse sind möglicherweise nicht genau identisch mit Ihren, basierend auf der Ruby-Version, dem seltsamen Caching usw., aber die allgemeinen Ergebnisse sind ähnlich.)

arr ist eine Sammlung von ActiveRecord-Objekten.

Benchmark.measure {
    100000.times {
        Hash[arr.map{ |a| [a.id, a] }]
    }
}

Benchmark @ real = 0,860651, @ cstime = 0,0, @ cutime = 0,0, @ stime = 0,0, @ utime = 0,8500000000000005, @ total = 0,8500000000000005

Benchmark.measure { 
    100000.times {
        h = Hash[arr.collect { |v| [v.id, v] }]
    }
}

Benchmark @ real = 0,74612, @ cstime = 0,0, @ cutime = 0,0, @ stime = 0,010000000000000009, @ utime = 0,740000000000002, @ total = 0,750000000000002

Benchmark.measure {
    100000.times {
        hash = {}
        arr.each { |a| hash[a.id] = a }
    }
}

Benchmark @ real = 0,627355, @ cstime = 0,0, @ cutime = 0,0, @ stime = 0,010000000000000009, @ utime = 0,6199999999999974, @ total = 0,6299999999999975

Benchmark.measure {
    100000.times {
        arr.each_with_object({}) { |v, h| h[v.id] = v }
    }
}

Benchmark @ real = 1.650568, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.12999999999999998, @ utime = 1.51, @ total = 1.64

Abschließend

Nur weil Ruby ausdrucksstark und dynamisch ist, heißt das nicht, dass Sie sich immer für die schönste Lösung entscheiden sollten. Die Basis jeder Schleife war die schnellste beim Erstellen eines Hash.

dmastylo
quelle
7
Du, mein Freund, bist großartig,
Alexander Bird
Etwas schneller, um eine manuell inkrementierte Schleifenvariable zu verwenden: Ich habe Ihr Dataset nicht - ich habe gerade ein triviales Objekt mit einem @ id-Accessor gekocht und mehr oder weniger Ihren Zahlen entsprochen - aber die direkte Iteration hat ein paar Prozent weniger gekostet. Stilistisch bevorzuge ich {} .tap {| h | ....} zum Zuweisen eines Hashs, weil ich gekapselte Chunks mag.
android.weasel
32
h = arr.each_with_object({}) { |v,h| h[v] = f(v) }
Megas
quelle
2
Dies liest sich viel prägnanter als die Verwendung von Hash [arr.collect {...}]
kaichanvong
1
Dies ist unglaublich langsam, siehe
dmastylo
11

Das würde ich wahrscheinlich schreiben:

h = Hash[arr.zip(arr.map(&method(:f)))]

Einfach, klar, offensichtlich, deklarativ. Was willst du mehr?

Jörg W Mittag
quelle
1
Ich mag zipgenauso wie der nächste Typ, aber da wir bereits anrufen map, warum lassen wir es nicht dabei? h = Hash[ arr.map { |v| [ v, f(v) ] } ]Gibt es einen Vorteil für Ihre Version, den ich nicht sehe?
Telemachos
@Telemachus: Mit all dem Haskell-Code, den ich gelesen habe, habe ich mich gerade an punktfreies Programmieren gewöhnt, das ist alles.
Jörg W Mittag
5

Ich mache es wie in diesem großartigen Artikel beschrieben: http://robots.thoughtbot.com/iteration-as-an-anti-pattern#build-a-hash-from-an-array

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h.merge(fruit => f(fruit)) }

Weitere Informationen zur injectMethode: http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-inject

Vlado Cingel
quelle
Dies gilt mergefür jeden Schritt der Iteration. Zusammenführen ist O (n), ebenso wie die Iteration. Dies ist also, O(n^2)während das Problem selbst offensichtlich linear ist. In absoluten Zahlen habe ich dies nur auf einem Array mit 100.000 Elementen versucht und es dauerte 730 seconds, während die anderen in diesem Thread erwähnten Methoden irgendwo von 0.7bis dauerten 1.1 seconds. Ja, das ist eine Verlangsamung um Faktor 700 !
Matthias Winkelmann
1

Ein anderer, meiner Meinung nach etwas klarer -

Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]

Länge als f () verwenden -

2.1.5 :026 > array = ["apples", "bananas", "coconuts", "watermelons"]
 => ["apples", "bananas", "coconuts", "watermelons"] 
2.1.5 :027 > Hash[*array.reduce([]) { |memo, fruit| memo << fruit << fruit.length }]
 => {"apples"=>6, "bananas"=>7, "coconuts"=>8, "watermelons"=>11} 
2.1.5 :028 >
Shreyas
quelle
1

Zusätzlich zur Antwort von Vlado Cingel (Ich kann noch keinen Kommentar hinzufügen, daher habe ich eine Antwort hinzugefügt).

Inject kann auch folgendermaßen verwendet werden: Der Block muss den Akkumulator zurückgeben. Nur die Zuordnung im Block gibt den Wert der Zuordnung zurück, und ein Fehler wird gemeldet.

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h[fruit]= f(fruit); h }
ruud
quelle
Ich habe die beiden Versionen verglichen: Die Verwendung von Merge verdoppelt die Ausführungszeit. Die obige Injektionsversion ist ein Vergleich mit der
Sammelversion