I do have this kind of params
params = { "people" =>
{
"fname" => ['john', 'megan'],
"lname" => ['doe', 'fox']
}
}
Wherein I loop through using this code
result = []
params["people"].each do |key, values|
values.each_with_index do |value, i|
result[i] = {}
result[i][key.to_sym] = value
end
end
The problem on my code is that it always gets the last key and value.
[
{ lname: 'doe' },
{ lname: 'fox' }
]
i want to convert it into
[
{fname: 'john', lname: 'doe'},
{fname: 'megan', lname: 'fox'}
]
so that i can loop through of them and save to database.
Your question has been answered but I'd like to mention an alternative calculation that does not employ indices:
keys, values = params["people"].to_a.transpose
#=> [["fname", "lname"], [["john", "megan"], ["doe", "fox"]]]
keys = keys.map(&:to_sym)
#=> [:fname, :lname]
values.transpose.map { |val| keys.zip(val).to_h }
#=> [{:fname=>"john", :lname=>"doe"},
# {:fname=>"megan", :lname=>"fox"}]
result[i] = {}
The problem is that you're doing this each loop iteration, which resets the value and deletes any existing keys you already put there. Instead, only set the value to {} if it doesn't already exist.
result[i] ||= {}
In your inner loop, you're resetting the i-th element to an empty hash:
result[i] = {}
So you only end up with the data from the last key-value-pair, i.e. lname.
Instead you can use this to only set it to an empty hash if it doesn't already exist:
result[i] ||= {}
So the first loop through, it gets set to {}, but after that, it just gets set to itself.
Alternatively, you can also use
result[i] = {} if !result[i]
which may or may not be more performant. I don't know.
Related
I'm stuck trying to safely navigate a hash from json.
The json could have a string, eg:
or it could be further nested:
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h2 = { location: 'Australia' }
h2.dig('location', 'formatted')
Then String does not have #dig method
Basically I'm trying to load the JSON then populate the rails model with the data available which may be optional. It seems backwards to check every nested step with an if.
Hash#dig has no magic. It reduces the arguments recursively calling Hash#[] on what was returned from the previous call.
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h1.dig :location, :code
#β "AU"
It works, because h1[:location] had returned a hash.
h2 = { location: 'Australia' }
h2.dig :location, :code
It raises, because h2[:location] had returned a String.
That said, the solution would be to reimplement Hash#dig, as usually :)
Explicitly taking into account that itβs extremely trivial. Just take a list of keys to dig and (surprise) reduce, returning either the value, or nil.
%i|location code|.reduce(h2) do |acc, e|
acc.is_a?(Hash) ? acc[e] : nil
end
#β nil
%i|location code|.reduce(h1) do |acc, e|
acc.is_a?(Hash) ? acc[e] : nil
end
#β "AU"
Shameless plug. You might find the gem iteraptor I had created for this exact purpose useful.
You can use a simple piece of code like that:
def nested_value(hash, key)
return hash if key == ''
keys = key.split('.')
value = hash[keys.first] || hash[keys.first.to_sym]
return value unless value.is_a?(Hash)
nested_value(value, keys[1..-1].join('.'))
end
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h2 = { 'location' => 'Australia' }
p nested_value(h1, 'location.formatted') # => Australia
p nested_value(h2, 'location.formatted') # => Australia
You can also use that method for getting any nested value of a hash by providing key in format foo.bar.baz.qux. Also the method doesn't worry whether a hash has string keys or symbol keys.
I don't know if this lead to the expected behaviour (see examples below) but you can define a patch for the Hash class as follow:
module MyHashPatch
def safe_dig(params) # ok, call as you like..
tmp = self
res = nil
params.each do |param|
if (tmp.is_a? Hash) && (tmp.has_key? param)
tmp = tmp[param]
res = tmp
else
break
end
end
res
end
end
Hash.include MyHashPatch
Then test on your hashes:
h1 = { location: { formatted: 'Australia', code: 'AU' } }
h2 = { location: 'Australia' }
h1.safe_dig([:location, :formatted]) #=> "Australia"
h2.safe_dig([:location, :formatted]) #=> "Australia"
h1.safe_dig([:location, :code]) #=> "AU"
h2.safe_dig([:location, :code]) #=> "Australia"
I'm learning coding, and one of the assignments is to return keys is return the names of people who like the same TV show.
I have managed to get it working and to pass TDD, but I'm wondering if I've taken the 'long way around' and that maybe there is a simpler solution?
Here is the setup and test:
class TestFriends < MiniTest::Test
def setup
#person1 = {
name: "Rick",
age: 12,
monies: 1,
friends: ["Jay","Keith","Dave", "Val"],
favourites: {
tv_show: "Friends",
things_to_eat: ["charcuterie"]
}
}
#person2 = {
name: "Jay",
age: 15,
monies: 2,
friends: ["Keith"],
favourites: {
tv_show: "Friends",
things_to_eat: ["soup","bread"]
}
}
#person3 = {
name: "Val",
age: 18,
monies: 20,
friends: ["Rick", "Jay"],
favourites: {
tv_show: "Pokemon",
things_to_eat: ["ratatouille", "stew"]
}
}
#people = [#person1, #person2, #person3]
end
def test_shared_tv_shows
expected = ["Rick", "Jay"]
actual = tv_show(#people)
assert_equal(expected, actual)
end
end
And here is the solution that I found:
def tv_show(people_list)
tv_friends = {}
for person in people_list
if tv_friends.key?(person[:favourites][:tv_show]) == false
tv_friends[person[:favourites][:tv_show]] = [person[:name]]
else
tv_friends[person[:favourites][:tv_show]] << person[:name]
end
end
for array in tv_friends.values()
if array.length() > 1
return array
end
end
end
It passes, but is there a better way of doing this?
I think you could replace those for loops with the Array#each. But in your case, as you're creating a hash with the values in people_list, then you could use the Enumerable#each_with_object assigning a new Hash as its object argument, this way you have your own person hash from the people_list and also a new "empty" hash to start filling as you need.
To check if your inner hash has a key with the value person[:favourites][:tv_show] you can check for its value just as a boolean one, the comparison with false can be skipped, the value will be evaluated as false or true by your if statement.
You can create the variables tv_show and name to reduce a little bit the code, and then over your tv_friends hash to select among its values the one that has a length greater than 1. As this will give you an array inside an array you can get from this the first element with first (or [0]).
def tv_show(people_list)
tv_friends = people_list.each_with_object(Hash.new({})) do |person, hash|
tv_show = person[:favourites][:tv_show]
name = person[:name]
hash.key?(tv_show) ? hash[tv_show] << name : hash[tv_show] = [name]
end
tv_friends.values.select { |value| value.length > 1 }.first
end
Also you can omit parentheses when the method call doesn't have arguments.
I am trying to group data I am getting from an API to serve to our front application. I mean group "time" by "date".
dates: {date1: [time1, time2, timeN], date2: [time1...]}
My input is like this:
{"date"=>"2017-04-04T00:00:00", "time"=>"1754-01-01T13:00:00"}
{"date"=>"2017-04-04T00:00:00", "time"=>"1754-01-01T14:00:00"}
{"date"=>"2017-04-05T00:00:00", "time"=>"1754-01-01T12:00:00"}
{"date"=>"2017-04-05T00:00:00", "time"=>"1754-01-01T13:00:00"}
And my output should be like this:
dates: [{date: "2017-04-04T00:00:00", availableTimes: ["1754-01-01T13:00:00", "1754-01-01T14:00:00"]}, {date: "2017-04-05T00:00:00", availableTimes: ["1754-01-01T12:00:00", "1754-01-01T13:00:00"]}]
I am trying to do this this way but without going into loop madness. I have the following:
dates = Hash[input_data.map{|sd| [sd.date, [""]]}]
This gives me the data outpout like this:
{"2017-04-04T00:00:00"=>[""],
"2017-04-05T00:00:00"=>[""],
"2017-04-11T00:00:00"=>[""],
"2017-04-12T00:00:00"=>[""],
"2017-04-18T00:00:00"=>[""],
"2017-04-19T00:00:00"=>[""],
"2017-04-25T00:00:00"=>[""],
"2017-04-26T00:00:00"=>[""]}
Just one possible way:
input.each_with_object(Hash.new { |h, k| h[k] = [] }) do |h, m|
m[h['date']] << h['time']
end.map { |k, v| { date: k, avaliable_times: v } }
#=> [{:date=>"2017-04-04T00:00:00", :avaliable_times=>["1754-01-01T13:00:00", "1754-01-01T14:00:00"]},
# {:date=>"2017-04-05T00:00:00", :avaliable_times=>["1754-01-01T12:00:00", "1754-01-01T13:00:00"]}]
Actually, it seems like your data structure would be more concise without last map, I mean:
#=> {"2017-04-04T00:00:00"=>["1754-01-01T13:00:00", "1754-01-01T14:00:00"],
# "2017-04-05T00:00:00"=>["1754-01-01T12:00:00", "1754-01-01T13:00:00"]}
You are getting that output because your map function is not actually modifying any sort of data structure. It is simply returning a new array full of arrays that contain the date and an array with an empty string. Basically, this isn't going to be done with just a single map call.
So, the basic algorithm would be:
Find array of all unique dates
Loop through unique dates and use select to only get the date/time pairs for the current date in the loop iteration
Set up the data in the format you prefer
This code will have filteredDates be in the format you need the data
filteredDates = { dates: [] }
uniqueDates = input_data.map { |d| d["date"] }.uniq # This is an array of only unique dates
uniqueDates.each do |date|
dateTimes = input_data.select { |d| d["date"] == date }
newObj = { date: date }
newObj[:availableTimes] = dateTimes.map { |d| d["time"] }
filteredDates[:dates].push(newObj)
end
Here is what filteredDates will look like:
{:dates=>[{:date=>"2017-04-04T00:00:00", :availableTimes=>["1754-01-01T13:00:00", "1754-01-01T14:00:00"]}, {:date=>"2017-04-05T00:00:00", :availableTimes=>["1754-01-01T12:00:00", "1754-01-01T13:00:00"]}]}
There is many ways you can do this, one way is to create a new hash, and set the default value to be an array, then loop over the results and insert the dates:
dates = Hash.new { |hash, key| hash[key] = [] }
input_data.each{ |sd| dates[sd["date"]] << sd["time"] }
I would use Enumerable#group_by.
dates = [{"date"=>"2017-04-04T00:00:00", "time"=>"1754-01-01T13:00:00"},
{"date"=>"2017-04-04T00:00:00", "time"=>"1754-01-01T14:00:00"},
{"date"=>"2017-04-05T00:00:00", "time"=>"1754-01-01T12:00:00"},
{"date"=>"2017-04-05T00:00:00", "time"=>"1754-01-01T13:00:00"}]
dates.group_by { |g| g["date"] }.
map { |k,v| { date: k, available_times: v.map { |h| h["time"] } } }
#=> [{:date=>"2017-04-04T00:00:00",
# :available_times=>["1754-01-01T13:00:00", "1754-01-01T14:00:00"]},
# {:date=>"2017-04-05T00:00:00",
# :available_times=>["1754-01-01T12:00:00", "1754-01-01T13:00:00"]}]
The first step produces the following intermediate value:
dates.group_by { |g| g["date"] }
#=> {"2017-04-04T00:00:00"=>
# [{"date"=>"2017-04-04T00:00:00", "time"=>"1754-01-01T13:00:00"},
# {"date"=>"2017-04-04T00:00:00", "time"=>"1754-01-01T14:00:00"}],
# "2017-04-05T00:00:00"=>
# [{"date"=>"2017-04-05T00:00:00", "time"=>"1754-01-01T12:00:00"},
# {"date"=>"2017-04-05T00:00:00", "time"=>"1754-01-01T13:00:00"}]}
There are probably more elegant ways, but
results = Hash.new
dates.each do |date|
d, t = date['date'].split('T') # (clean up/split date and time formatting)
results.key?(d) ? nil : results[d] = Array.new
results[d] << t
end
puts results
# => {"2017-04-04"=>["13:00:00", "14:00:00"], "2017-04-05"=>["12:00:00", "13:00:00"]}
I have two hashes:
a = {"0"=>"name", "1"=>"email"}
b = {"0"=>"source", "1"=>"info", "2"=>"extra", "3"=>"name"}
I want a hash created by doing the following:
1) When the two hashes contain identical values, keep value of original hash and discard value of second hash.
2) When the values of second hash are not in first hash, just add to the end of new hash, making sure that the key is ordered.
with this result:
{"0"=>"name", "1"=>"email", "2"=>"source", "3"=>"info", "4"=>"extra"}
I did it this ugly way:
l1 = a.keys.length
l2 = b.keys.length
max = l1 > l2 ? l1 : l2
counter = l1
result = {}
max.times do |i|
unless a.values.include? b[i.to_s]
result[counter.to_s] = b[i.to_s]
counter += 1
end
end
a.merge!(result)
Is there a built-in ruby method or utility that could achieve this same task in a cleaner fashion?
(a.values + b.values).uniq.map.with_index{|v, i| [i.to_s, v]}.to_h
# => {"0"=>"name", "1"=>"email", "2"=>"source", "3"=>"info", "4"=>"extra"}
First create an array containing the values in the hash. This can be accomplished with the concat method. Now that we have an array, we can call the uniq method to retrieve all unique values. This also preserves the order.
a = { "0" => "name", "1" => "email" }
b = { "0" => "source", "1" => "info", "2" => "extra", "3" => "name" }
values = a.values.concat(b.values).uniq
A shortcut to generating a hash in Ruby is with this trick.
Hash[[*0..values.length-1].zip(values)]
Output:
{0=>"name", 1=>"email", 2=>"source", 3=>"info", 4=>"extra"}
a = {"0"=>"name", "1"=>"email"}
b = {"0"=>"source", "1"=>"info", "2"=>"extra", "3"=>"name"}
key = (a.size-1).to_s
#=> "1"
b.each_value.with_object(a) { |v,h| (h[key.next!] = v) unless h.value?(v) }
#=> {"0"=>"name", "1"=>"email", "2"=>"source", "3"=>"info", "4"=>"extra"}
I'm creating a nested hash in ruby rexml and want to update the hash when i enter a loop.
My code is like:
hash = {}
doc.elements.each(//address) do |n|
a = # ...
b = # ...
hash = { "NAME" => { a => { "ADDRESS" => b } } }
end
When I execute the above code the hash gets overwritten and I get only the info in the last iteration of the loop.
I don't want to use the following way as it makes my code verbose
hash["NAME"] = {}
hash["NAME"][a] = {}
and so on...
So could someone help me out on how to make this work...
Assuming the names are unique:
hash.merge!({"NAME" => { a => { "ADDRESS" => b } } })
You always create a new hash in each iteration, which gets saved in hash.
Just assign the key directly in the existing hash:
hash["NAME"] = { a => { "ADDRESS" => b } }
hash = {"NAME" => {}}
doc.elements.each('//address') do |n|
a = ...
b = ...
hash['NAME'][a] = {'ADDRESS' => b, 'PLACE' => ...}
end
blk = proc { |hash, key| hash[key] = Hash.new(&blk) }
hash = Hash.new(&blk)
doc.elements.each('//address').each do |n|
a = # ...
b = # ...
hash["NAME"][a]["ADDRESS"] = b
end
Basically creates a lazily instantiated infinitely recurring hash of hashes.
EDIT: Just thought of something that could work, this is only tested with a couple of very simple hashes so may have some problems.
class Hash
def can_recursively_merge? other
Hash === other
end
def recursive_merge! other
other.each do |key, value|
if self.include? key and self[key].can_recursively_merge? value
self[key].recursive_merge! value
else
self[key] = value
end
end
self
end
end
Then use hash.recursive_merge! { "NAME" => { a => { "ADDRESS" => b } } } in your code block.
This simply recursively merges a heirachy of hashes, and any other types if you define the recursive_merge! and can_recusively_merge? methods on them.