Related
I have the following Ruby code, which calls the same function multiple times with different arguments, and pushes the results into a common array.
people_relations = []
people.zip(people_addresses).map do |person, address|
people_relations.push(createRelation(person, address))
end
people.zip(people_ph_numbers).map do |person, phone_number|
people_relations.push(createRelation(person, phone_number))
end
people.zip(people_aliases).map do |person, _alias|
people_relations.push(createRelation(person, _alias))
end
def createRelation(e1, e2)
[true, false].sample ? CurrentRelation.new(e1, e2) : PastRelation.new(e1, e2)
end
This code works just fine, but I feel like this is not the idiomatic Ruby way of doing things, and can be improved by compressing the code into less lines or made to look cleaner.
Is there a better way to write the code that appears above?
You could create an array that contains all the people "attributes" you're going to use, and with Enumerable#each_with_object you can assign an initial array to fill with the result of each call to createRelation():
attributes = [people_addresses, people_ph_numbers, people_aliases]
relations = people.each_with_object([]).with_index do |(person, memo), index|
attributes.each do |attribute|
memo << createRelation(person, attribute[index])
end
end
I'd probably go with a transpose -> flat_map solution for this myself, for instance given:
def CreateRelation(person, relationship)
if [true, false].sample
"#{person} is currently related to #{relationship}"
else
"#{person} used to be related to #{relationship}"
end
end
people = ['Person 1', 'Person 2', 'Person 3']
addresses = ['Person 1 Address', 'Person 2 Address', 'Person 3 Address']
phone_numbers = ['Person 1 Phone', 'Person 2 Phone', 'Person 3 Phone']
aliases = ['Person 1 AKA', 'Person 2 AKA', 'Person 3 AKA']
We can stick those 4 arrays into a single array and then transpose them, so the first element of each ends up in an array with each other, the second in another, and the last in a third:
[people, addresses, phone_numbers, aliases].transpose # => [
# ["Person 1", "Person 1 Address", "Person 1 Phone", "Person 1 AKA"],
# ["Person 2", "Person 2 Address", "Person 2 Phone", "Person 2 AKA"],
# ["Person 3", "Person 3 Address", "Person 3 Phone", "Person 3 AKA"]]
and then you can flat_map those by calling CreateRelation:
result = [people, addresses, phone_numbers, aliases].transpose.flat_map do |person, *relations|
relations.map { |relationship| CreateRelation(person, relationship) }
end
#["Person 1 used to be related to Person 1 Address",
# "Person 1 used to be related to Person 1 Phone",
# "Person 1 used to be related to Person 1 AKA",
# "Person 2 is currently related to Person 2 Address",
# "Person 2 used to be related to Person 2 Phone",
# "Person 2 is currently related to Person 2 AKA",
# "Person 3 is currently related to Person 3 Address",
# "Person 3 used to be related to Person 3 Phone",
# "Person 3 used to be related to Person 3 AKA"]
Or, at that point you could stick with just iterating and pushing, if you don't want to map/flat_map.
The more I think about it, the more I think I'd go with transpose -> each_with_object, instead of flat_map...less "create an array and then throw it away", I'll leave this with flat_map though because it is another option and #Sebastian Palma has each_with_object covered.
Assuming the following data tuple containing a person's name, age and the books he has read:
list = [
["Peter", 21, ["Book 1", "Book 2", "Book 3", "Book 4"],
["Amy", 19, ["Book 3", "Book 4"],
["Sanders", 32, ["Book 1", "Book 2",],
["Charlie", 21, ["Book 4", "Book 5", "Book 6"],
["Amanda", 21, ["Book 2", "Book 5"]
]
What is the optimal way to extract names grouped by the books read, into the following format (basically a an array of arrays containing the book name and an array of names of people who read it)
results = [
["Book 1", ["Sanders", "Peter"]],
["Book 2", ["Sanders" "Amanda", "Peter"]],
["Book 3", ["Peter", "Amy"]],
["Book 4", ["Charlie", "Peter", "Amy"]],
["Book 5", ["Amanda","Charlie"]],
["Book 6", ["Charlie"]]
]
I've tried the following iterating method which extracts the lists of names and puts them into a hash, with the book title as the keys.
book_hash = Hash.new([])
list.each { |name,age,books|
books { |x| book_hash[x] = book_hash[x] + [name] }
}
results = book_hash.to_a.sort
However, the above method seems rather inefficient when handling large datasets containing millions of names. I've attempted to use the Array.group_by, but so far I'm unable to make it work with nested arrays.
Does anyone have any idea about the above?
Hash output. More suitable.
list.each_with_object({}) do |(name, age, books), hash|
books.each do |book|
(hash[book] ||= []) << name
end
end
If you must make it an array, then append a .to_a to the output of the above.
I have a ugly array that looks like this
["advert 0", "[1404915231, 1404920520]", "advert 4", "[1404915231]", "advert 5", "[1404915231]", "advert 6", "[1404915231]", "advert 7", "[1404915231]", "advert 8", "[1404915231]", "advert 9", "[1404915231]"]
I've tried to get it to look like this but I have been unsuccessful.
{advert1: [1404915231, 1404920520], advert4: [1404915231]}
Thanks
Here is a way(if it helps you) :-
require 'yaml'
arr = [
"advert 0", "[1404915231, 1404920520]",
"advert 4", "[1404915231]", "advert 5",
"[1404915231]", "advert 6", "[1404915231]",
"advert 7", "[1404915231]", "advert 8",
"[1404915231]", "advert 9", "[1404915231]"
]
Hash[arr.each_slice(2).map { |a, b| [a.gsub(/\s+/,'').to_sym, YAML.load(b)] }]
# => {:advert0=>[1404915231, 1404920520],
# :advert4=>[1404915231],
# :advert5=>[1404915231],
# :advert6=>[1404915231],
# :advert7=>[1404915231],
# :advert8=>[1404915231],
# :advert9=>[1404915231]}
Enumerable#each_slice(2) - Is sending 2 items from the collection, to the Enumerable#map block. Now OP wants, string to be converted as symbols. Thus, I used first, to removes the white spaces in between strings. It is done by String#gsub method. I passed the regex, /\s+/ as an Argument to the method #gsub, which as per the regex, will find each white space and replace them with empty strings(''). That means
"advert 0".gsub(/\s+/,'') # => "advert0"
Now as OP wants, all keys to be symbols, I apply String#to_sym.
"advert0".to_sym # => :advert0
So finally, I need to covert all string arrays to array, thus YAML::load will be helpful.
YAML::load "[1404915231, 1404920520]" # => [1404915231, 1404920520]
Till now what I said, will give us -
arr.each_slice(2).map { |a, b| [a.gsub(/\s+/,'').to_sym, YAML.load(b)] }
# => [[:advert0, [1404915231, 1404920520]],
# [:advert4, [1404915231]],
# [:advert5, [1404915231]],
# [:advert6, [1404915231]],
# [:advert7, [1404915231]],
# [:advert8, [1404915231]],
# [:advert9, [1404915231]]]
Now, I will make it a Hash. Look this Hash[ key, value, ... ] → new_hash.
Hash[:a,[1],:b, [2,3]] # => {:a=>[1], :b=>[2, 3]}
Hope it helps.
Arup gives a great answer, but if you're looking for alternatives, here are some thoughts I had:
Break down the problem
Let's break down the problem. We have ugly_array, and we want to use its even elements ("advert 0", "advert 2") as keys in a Hash, and its odd elements as the corresponding values. We want to transform both the keys (into symbols) and the values (into arrays of numbers) on the way.
Create a (temporary) Hash
A useful method in Ruby is Hash[], which will create a Hash using its odd arguments as keys and even arguments as values. For example:
Hash[ :foo, 1, :bar, 2 ]
# => { :foo => 1, :bar => 2 }
In order to use the items in ugly_array as separate arguments, we put * ("splat") before it:
ugly_hash = Hash[ *ugly_array ]
This is equivalent to Hash[ "advert 0", "[1404915231, 1404920520]", "advert 4", ... ], and it gives us this Hash:
{ "advert 0" => "[1404915231, 1404920520]",
"advert 4" => "[1404915231]",
"advert 5" => "[1404915231]",
# ...
}
Transform the keys
Now we need to transform the keys and values. First, the keys. You want to turn the string "advert 0" into the symbol :advert0, which we can do like this:
"advert 0".gsub(" ", "").to_sym
# => :advert0
To clarify gsub(" ", "") replaces all spaces (" ") with nothing, which effectively removes them. We could also use a regular expression like /\s+/, but it looks like the extra flexibility isn't really needed here. Now we have the string "advert0", and to_sym converts it into the symbol :advert0.
Transform the values
We want to turn a string like "[1404915231, 1404920520]" into an array of numbers like [1404915231, 1404920520]. There are a lot of different ways to do this, but one way that doesn't require using a module like JSON or YAML is this:
"[1404915231, 1404920520]".scan(/\d+/).map(&:to_i)
# => [ 1404915231, 1404920520 ]
This does two things. First, scan(/\d+/) uses a regular expression to find sequences of consecutive digits (\d) in the string and returns them as an array, yielding an array of strings: ["1404915231", "1404920520"]. Then we use map(&:to_i) to call the to_i method on each of the strings, yielding an array of numbers.
Transform the temporary Hash
Now that we know how to transform the keys and values, we can take our temporary Hash and build a new, transformed Hash from it. There are other ways to do this, but I'm fond of Enumerable#each_with_object. Suppose we do this:
ugly_hash.each_with_object({}) do |(key, val), hsh|
hsh[key] = val
end
Inside the block, hsh is the new, empty Hash ({}) we gave as the argument to each_with_object, and in each iteration we add val to it with the key key. This yields:
{ "advert 0" => "[1404915231, 1404920520]",
"advert 4" => "[1404915231]",
# ...
}
It looks exactly the same! But you can probably see that since we have key and val inside the block, we can transform them before using them.
Bring it all together
pretty_hash = Hash[ *ugly_array ].each_with_object({}) do |(key, val), hsh|
key = key.tr(" ", "").to_sym
val = val.scan(/\d+/).map(&:to_i)
hsh[key] = val
end
As you can see, this is the same code, except inside the block we're transforming key and val before using them to add an element to the Hash. And it gives us just what we're looking for:
p pretty_hash
# => { :advert0 => [ 1404915231, 1404920520 ],
# :advert4 => [ 1404915231 ],
# :advert5 => [ 1404915231 ],
# ...
# }
Apart from being relatively readable and not requiring external libraries, this method has the distinct advantage of iterating over the original data exactly once and performing only one (composite) operation on each key and value.
If you're so inclined, you can reduce the inside of the block to a single line, but be aware of how it impacts readability:
pretty_hash = Hash[*ugly_array].each_with_object({}) do |(key, val), hsh|
hsh[ key.tr(" ", "").to_sym ] = val.scan(/\d+/).map(&:to_i)
end
I hope that's helpful!
I've assumed you want the result:
{ advert0: [1404915231, 1404920520], advert4: [1404915231] }
That is, I assume you meant advert0 where you wrote advert1; specifically, that you wish to select those pairs "advert x", arr (arr being an array) that correspond to given values of x (here 0 and 4) and convert the collection of those pairs to a hash. If this is not want you want to do, you need read no further.
You could do this as follows:
Code
def doit(arr, *vals_at)
arr.each_slice(2)
.map {|s1,s2|
[s1.split.last.to_i,[s1.tr(' ','').to_sym, s2.scan(/\d+/).map(&:to_i)]]}
.to_h
.values_at(*vals_at)
.to_h
end
Example
arr = ["advert 0", "[1404915231, 1404920520]",
"advert 4", "[1404915231]",
"advert 5", "[1404915231]"
]
(We don't need the rest of arr.)
doit(arr, 0, 4)
#=> {:advert0=>[1404915231, 1404920520], :advert4=>[1404915231]}
Explanation
For the value of arr above, let:
b = a.each_slice(2)
#=> #<Enumerator: ["advert 0", "[1404915231, 1404920520]",
# "advert 4", "[1404915231]",
# "advert 5", "[1404915231]"]:each_slice(2)>
c = b.map
#<Enumerator: #<Enumerator: ["advert 0", "[1404915231, 1404920520]",
# "advert 4", "[1404915231]",
# "advert 5", "[1404915231]"]:each_slice(2)>:map>
c may be thought of as a "compound" enumerator. When we add a block, each will pass each value of the following array into the block:
c.to_a
#=> [["advert 0", "[1404915231, 1404920520]"],
# ["advert 4", "[1404915231]"],
# ["advert 5", "[1404915231]"]]
Note
b.to_a == c.to_a #=> true
Next,
d = c.each { |s1,s2| [s1.split.last.to_i,
[s1.tr(' ','').to_sym, s2.scan(/\d+/).map(&:to_i)]] }
#=> [[0, [:advert0, [1404915231, 1404920520]]],
# [4, [:advert4, [1404915231]]],
# [5, [:advert5, [1404915231]]]]
Note that c.each is equivalent to b.map:
d == b.map { |s1,s2| [s1.split.last.to_i,
[s1.tr(' ','').to_sym, s2.scan(/\d+/).map(&:to_i)]] }
#=> true
Let's pause a moment to see what's going on here. The first value the enumerator c passes into the block (["advert 0", "[1404915231, 1404920520]"]) is split into the two block variables by disambiguation:
s1 #=> "advert 0"
s2 #=> "[1404915231, 1404920520]"
Then:
s1.split.last.to_i
#=> 0
r = s2.scan(/\d+/)
#=> ["1404915231", "1404920520"]
r.map(&:to_i)
#=> [1404915231, 1404920520]
Ergo, the first element passed into the block:
["advert 0", "[1404915231, 1404920520]"]
is mapped to:
[0,[1404915231, 1404920520]]
Similarly, the second and the third elements of c are respectively mapped to:
[4=>[:advert4, [1404915231]]
and
[5=>[:advert5, [1404915231]]
Now that we have d,
e = d.to_h # or Hash[d] for Ruby versions < 2.0
#=> {0=>[:advert0, [1404915231, 1404920520]],
# 4=>[:advert4, [1404915231]],
# 5=>[:advert5, [1404915231]]}
Aha! Now you can see why I constructed e as I did:
f = e.values_at(0,4)
#=> [[:advert0, [1404915231, 1404920520]], [:advert4, [1404915231]]]
f.to_h
#=> {:advert0=>[1404915231, 1404920520], :advert4=>[1404915231]}
I'm having a problem sorting an array. My expected result is:
#list = [
["2 1/8\"", "23 13/32\"", "stile", "2"],
["2 1/8\"", "11 5/32\"", "rail", "6"],
["2 1/8\"", "7 13/32\"", "stile", "4"]
]
This is just an example since the array is dynamic and data are always changing, but the one thing that is constant is that most if not all single digit measurements end up at the beginning.
And here's the code I'm using:
#list = #list.sort {|a,b| b[1]<=>a[1]}
Here's the actual output from the code.
#list
# => [
# ["2 1/8\"", "7 13/32\"", "stile", "2"],
# ["2 1/8\"", "23 13/32\"", "rail", "6"],
# ["2 1/8\"", "11 5/32\"", "stile", "4"]
# ]
Any ideas why?
If the goal is to sort by the first numeric value of the second value of each array (7, 23, 11) converting the string to integer in the sort could be a solution:
#list = #list.sort {|a,b| b[1].to_i <=> a[1].to_i }
You can also make it shorter by using sort!:
#list.sort! {|a,b| b[1].to_i <=> a[1].to_i }
Convert this Array:
a = ["item 1", "item 2", "item 3", "item 4"]
...to a Hash:
{ "item 1" => "item 2", "item 3" => "item 4" }
i.e. elements at even indexes are keys and odd ones are values.
a = ["item 1", "item 2", "item 3", "item 4"]
h = Hash[*a] # => { "item 1" => "item 2", "item 3" => "item 4" }
That's it. The * is called the splat operator.
One caveat per #Mike Lewis (in the comments): "Be very careful with this. Ruby expands splats on the stack. If you do this with a large dataset, expect to blow out your stack."
So, for most general use cases this method is great, but use a different method if you want to do the conversion on lots of data. For example, #Łukasz Niemier (also in the comments) offers this method for large data sets:
h = Hash[a.each_slice(2).to_a]
Ruby 2.1.0 introduced a to_h method on Array that does what you require if your original array consists of arrays of key-value pairs: http://www.ruby-doc.org/core-2.1.0/Array.html#method-i-to_h.
[[:foo, :bar], [1, 2]].to_h
# => {:foo => :bar, 1 => 2}
Just use Hash.[] with the values in the array. For example:
arr = [1,2,3,4]
Hash[*arr] #=> gives {1 => 2, 3 => 4}
Or if you have an array of [key, value] arrays, you can do:
[[1, 2], [3, 4]].inject({}) do |r, s|
r.merge!({s[0] => s[1]})
end # => { 1 => 2, 3 => 4 }
This is what I was looking for when googling this:
[{a: 1}, {b: 2}].reduce({}) { |h, v| h.merge v }
=> {:a=>1, :b=>2}
Enumerator includes Enumerable. Since 2.1, Enumerable also has a method #to_h. That's why, we can write :-
a = ["item 1", "item 2", "item 3", "item 4"]
a.each_slice(2).to_h
# => {"item 1"=>"item 2", "item 3"=>"item 4"}
Because #each_slice without block gives us Enumerator, and as per the above explanation, we can call the #to_h method on the Enumerator object.
You could try like this, for single array
irb(main):019:0> a = ["item 1", "item 2", "item 3", "item 4"]
=> ["item 1", "item 2", "item 3", "item 4"]
irb(main):020:0> Hash[*a]
=> {"item 1"=>"item 2", "item 3"=>"item 4"}
for array of array
irb(main):022:0> a = [[1, 2], [3, 4]]
=> [[1, 2], [3, 4]]
irb(main):023:0> Hash[*a.flatten]
=> {1=>2, 3=>4}
a = ["item 1", "item 2", "item 3", "item 4"]
Hash[ a.each_slice( 2 ).map { |e| e } ]
or, if you hate Hash[ ... ]:
a.each_slice( 2 ).each_with_object Hash.new do |(k, v), h| h[k] = v end
or, if you are a lazy fan of broken functional programming:
h = a.lazy.each_slice( 2 ).tap { |a|
break Hash.new { |h, k| h[k] = a.find { |e, _| e == k }[1] }
}
#=> {}
h["item 1"] #=> "item 2"
h["item 3"] #=> "item 4"
All answers assume the starting array is unique. OP did not specify how to handle arrays with duplicate entries, which result in duplicate keys.
Let's look at:
a = ["item 1", "item 2", "item 3", "item 4", "item 1", "item 5"]
You will lose the item 1 => item 2 pair as it is overridden bij item 1 => item 5:
Hash[*a]
=> {"item 1"=>"item 5", "item 3"=>"item 4"}
All of the methods, including the reduce(&:merge!) result in the same removal.
It could be that this is exactly what you expect, though. But in other cases, you probably want to get a result with an Array for value instead:
{"item 1"=>["item 2", "item 5"], "item 3"=>["item 4"]}
The naïve way would be to create a helper variable, a hash that has a default value, and then fill that in a loop:
result = Hash.new {|hash, k| hash[k] = [] } # Hash.new with block defines unique defaults.
a.each_slice(2) {|k,v| result[k] << v }
a
=> {"item 1"=>["item 2", "item 5"], "item 3"=>["item 4"]}
It might be possible to use assoc and reduce to do above in one line, but that becomes much harder to reason about and read.