Update certain key in hash - ruby

I only want to update the date element in a hash.
i.e.
hash = { 'name': 'Albert', 'date_1': "31-01-2017", 'date_2': "31-01-2017" }
## I only want to update `date_1` and `date_2`.
I have tried
hash.select {|k,v| ['date_1','date_2'].include? k }.transform_values!(&:to_date)
but because I do a select before, it will only return me
{ 'date_1': Tue, 31 Jan 2017, 'date_2': Tue, 31 Jan 2017 }
Any suggestion to do keep other attributes as well as transform the selected key?
i.e. { 'name': 'Albert', 'date_1': Tue, 31 Jan 2017, 'date_2': Tue, 31 Jan 2017 }

Just update a value for date_1 and date_2:
hash['date_1'] = hash['date_1'].to_date if hash['date_1']
hash['date_2'] = hash['date_2'].to_date if hash['date_2']
or you can do it with each if you have more a date keys:
date_keys = [:date_1, :date_2]
hash.each do |k,v|
hash[k] = v.to_date if date_keys.include?(k)
end

If you want something a bit more dynamic, without having to hardcode date_1 and date_2 :
require 'date'
hash = { name: 'Albert', date_1: "31-01-2017", date_2: "31-01-2017" }
hash.each do |key, value|
if key.to_s =~ /^date_/ && value.is_a?(String)
hash[key] = Date.strptime(value, '%d-%m-%Y')
end
end
p hash
#=> {:name=>"Albert", :date_1=>#<Date: 2017-01-31 ((2457785j,0s,0n),+0s,2299161j)>, :date_2=>#<Date: 2017-01-31 ((2457785j,0s,0n),+0s,2299161j)>}
Note that for your code :
hash.select {|k,v| ['date_1','date_2'].include? k }.transform_values!(&:to_date)
transform_values! tries to modify a Hash in place which is only short-lived (the new hash returned by select). transform_values would achieve the exact same result.

Quick dynamic way
hash = { 'name' => 'Albert', 'date_1' => "31-01-2017", 'date_2' => "31-01-2017" }
hash.map { |k,v| hash[k] = k['date_'] ? v.to_date : v }
Will work on any key that contains date_, like date_02, date_1234, date_lastmonth, etc

Related

How to iterate over a nested Ruby hash to add a new key/value pair based on existing key/value data?

I have a nested hash of data:
munsters = {
"Herman" => { "age" => 32, "gender" => "male" },
"Lily" => { "age" => 30, "gender" => "female" },
"Grandpa" => { "age" => 402, "gender" => "male" },
"Eddie" => { "age" => 10, "gender" => "male" },
"Marilyn" => { "age" => 23, "gender" => "female"}
}
I want to loop through the hash for each member and look up their age in the nested hash.
For each member, I want to add a new key/value pair to the existing nested hash called "age_group".
The value of each member's "age_group" will be dependent on their age. For example, if the age is above 65 years I want to have their age_group read "senior", etc.
Problems I'm running into:
I'm confused how I would access just the "age" key_value pair of the nested hash when the first key is different for each member of the family. Meaning, I can't do something like munsters["age"] because that returns nil (assuming because "age" is nested).
If I had a simple (un-nested) hash it's pretty straightforward. Example of a non-nested hash:
ages = { "Herman" => 32, "Lily" => 30, "Grandpa" => 402, "Eddie" => 10 }
I would then likely loop though like this:
age_group = {}
ages.each do |k, v|
if v >= 65
puts "#{k}'s age of #{v} makes them a senior!"
age_group.merge!("age_group": "senior")
elsif v > 17 && v < 65
puts "#{k}'s age of #{v} makes them an adult"
age_group.merge!("age_group": "adult")
else
puts "#{k}'s age of #{v} makes them a kid."
age_group.merge!("age_group": "kid")
end
end
For the nested hash, I'm able to access the entire nested hash like this:
munsters.each do |k, v|
v.each do |k2, v2|
p "k2 is #{k2} and v2 is #{v2}"
end
end
But that still only returns the entire nested hash to the console instead of just the age:
k2 is age and v2 is 32
k2 is gender and v2 is male
In addition to #MurifoX answer, you can use #transform_values
ages.transform_values do |value|
if value["age"] >= 65
value["age_group"] = "senior"
elsif value["age"] > 17 && value["age"] < 65
value["age_group"] = "adult"
else
value["age_group"] = "kid"
end
end
The v is a hash too. So you can do it like this:
ages.each do |k, v|
if v["age"] > 60
ages[k]["age_group"] = "adult"
else
ages[k]["age_group"] = "kid"
end
end
You add a age_group key with a string value to the k position of your age hash.
def age_group(age)
case age
when 0..17 then "kid"
when 18..64 then "adult"
else "senior"
end
end
munsters.each_value { |h| h["age_group"] = age_group(h["age"]) }
#=> {"Herman" =>{"age"=>32, "gender"=>"male", "age_group"=>"adult"},
# "Lily" =>{"age"=>30, "gender"=>"female", "age_group"=>"adult"},
# "Grandpa"=>{"age"=>402, "gender"=>"male", "age_group"=>"senior"},
# "Eddie" =>{"age"=>10, "gender"=>"male", "age_group"=>"kid"},
# "Marilyn"=>{"age"=>23, "gender"=>"female", "age_group"=>"adult"}}
This return value is the new value of munsters.
The problem with your current attempt is that you try to do something for each key/value-pair of each munster. However you are only interested in the "age" and "age_group". So there is no need to iterate the key/value-pairs of munster. Instead work directly on munster.
Other examples mostly show mutating solution, so let me offer a non-mutating alternative (meaning that munsters doesn't change). I've also removed the if-statement to provide an alternative perspective.
threshholds = { "kid" => 0, "adult" => 18, "senior" => 65 }
age_groups = threshholds.each_key.sort_by(&threshholds)
new_munsters = munsters.transform_values do |munster|
age_group = age_groups
.take_while { |age_group| munster["age"] > threshholds[age_group] }
.last
munster.merge("age_group" => age_group)
end
#=> {
# "Herman"=>{"age"=>32, "gender"=>"male", "age_group"=>"adult"},
# "Lily"=>{"age"=>30, "gender"=>"female", "age_group"=>"adult"},
# "Grandpa"=>{"age"=>402, "gender"=>"male", "age_group"=>"senior"},
# "Eddie"=>{"age"=>10, "gender"=>"male", "age_group"=>"kid"},
# "Marilyn"=>{"age"=>23, "gender"=>"female", "age_group"=>"adult"}
# }
This solution uses transform_values to create a new version of munsters, containing new updated versions of munster (created with merge).

Reducing an array of hashes into new hash

I have an ActiveRecord relation that looks something like this:
[
{
timestamp: Tue, 02 Oct 2018 00:00:00 PDT -07:00,
user_id: 3,
organization_id: 1,
all_sales: 10,
direct_sales: 7,
referred_sales: 3,
},
{
timestamp: Wed, 03 Oct 2018 00:00:00 PDT -07:00,
user_id: 3,
organization_id: 1,
all_sales: 17,
direct_sales: 8,
referred_sales: 9,
},
{
timestamp: Thu, 04 Oct 2018 00:00:00 PDT -07:00,
user_id: 3,
all_sales: 3,
direct_sales: 3,
referred_sales: 0,
}
]
What I'd like to do is create a "sum" of all the keys pertaining to sales (for our purposes here, I don't need timestamp, user_id or organization_id, so basically, I'd like to end with something like this:
{
all_sales: 30
direct_sales: 18
referred_sales: 12
}
Is there an elegant ruby-ish way of doing this? I could easily create a set of variables for each sales category and augment them as I iterate over the original relation, but I wanted to see if the community had a cleaner method. In reality each of these hashes have far more than 3 relevant keys and so I fear that approach will get messy very quickly.
Edit: I also have checked out some other answers to similar questions here on SO (for example: Better way to sum values in an array of hashes), but ideally I wouldn't iterate so many times.
This will work:
arr.each_with_object({}) do |obj, hash|
%i[all_sales direct_sales referred_sales].each do |sym|
hash[sym] = hash[sym].to_i + obj[sym]
end
end
It's one iteration, you can write the nested loop as 3 different lines, but it's a bit cleaner this way in my opinion.
Note: calling to_i while getting previous value of hash[sym] as initially it is nil and nil.to_i == 0. Alternatively, you can initialize all unknown counts with 0, like this:
arr.each_with_object(Hash.new(0)) do |obj, hash|
%i[all_sales direct_sales referred_sales].each do |sym|
hash[sym] += obj[sym]
end
end
Since you're starting with an ActiveRecord Relation, you can use pluck to calculate all the sums with SQL and have it return an array with your totals:
SalesModel.pluck('SUM(all_sales)', 'SUM(direct_sales)', 'SUM(referred_sales)')
#=> [30, 18, 12]
Or use functional approach with reduce and merge methods:
keys = %i{all_sales direct_sales referred_sales}
total_sales = items.map {|item| item.select{|key, _| keys.include?(key)}}
.reduce({}) {|all, item| all.merge(item) {|_, sum, value| sum + value}}
# total_sales
# => {:all_sales=>30, :direct_sales=>18, :referred_sales=>12}
Or little bid clearer approach for Ruby 2.5.0 or higher, thanks to #Johan Wentholt
items.map {|item| item.slice(:all_sales, :direct_sales, :referred_sales)}
.reduce({}) {|all, item| all.merge(item) {|_, sum, value| sum + value}}
# => {:all_sales=>30, :direct_sales=>18, :referred_sales=>12}
Use Merge and Reduce function
value = arr.reduce do |h1, h2|
h1.merge(h2) do |k, v1, v2|
[:all_sales, :direct_sales, :referred_sales].include?(k) ? (v1 + v2) : nil
end
end.reject {|_, v| v.nil?}
p value
A couple of more verbose other options (that can be rendered more DRY and general):
result = { all_sales: (array.sum{ |e| e[:all_sales] }), direct_sales: (array.sum{ |e| e[:direct_sales] }), referred_sales: (array.sum{ |e| e[:referred_sales] }) }
or:
result = array.each.with_object(Hash.new(0)) do |h, obj|
obj[:all_sales] += h[:all_sales]
obj[:direct_sales] += h[:direct_sales]
obj[:referred_sales] += h[:referred_sales]
end
To be more DRY and general, starting with the array of the required keys
keys = [:all_sales, :direct_sales, :referred_sales]
The first becomes
keys.map.with_object(Hash.new) { |k, obj| obj[k] = array.sum { |e| e[k] } }
and the second:
array.each.with_object(Hash.new(0)) { |h, obj| keys.each { |k| obj[k] += h[k] } }

Ruby: how to replace key within multi dimensional hash without changing value [duplicate]

I have a condition that gets a hash.
hash = {"_id"=>"4de7140772f8be03da000018", .....}
Yet, I want to rename the key of that hash as follows.
hash = {"id"=>"4de7140772f8be03da000018", ......}
P.S. I don't know what keys are in the hash; they are random. Some keys are prefixed with an underscore that I would like to remove.
hash[:new_key] = hash.delete :old_key
rails Hash has standard method for it:
hash.transform_keys{ |key| key.to_s.upcase }
http://api.rubyonrails.org/classes/Hash.html#method-i-transform_keys
UPD: ruby 2.5 method
If all the keys are strings and all of them have the underscore prefix, then you can patch up the hash in place with this:
h.keys.each { |k| h[k[1, k.length - 1]] = h[k]; h.delete(k) }
The k[1, k.length - 1] bit grabs all of k except the first character. If you want a copy, then:
new_h = Hash[h.map { |k, v| [k[1, k.length - 1], v] }]
Or
new_h = h.inject({ }) { |x, (k,v)| x[k[1, k.length - 1]] = v; x }
You could also use sub if you don't like the k[] notation for extracting a substring:
h.keys.each { |k| h[k.sub(/\A_/, '')] = h[k]; h.delete(k) }
Hash[h.map { |k, v| [k.sub(/\A_/, ''), v] }]
h.inject({ }) { |x, (k,v)| x[k.sub(/\A_/, '')] = v; x }
And, if only some of the keys have the underscore prefix:
h.keys.each do |k|
if(k[0,1] == '_')
h[k[1, k.length - 1]] = h[k]
h.delete(k)
end
end
Similar modifications can be done to all the other variants above but these two:
Hash[h.map { |k, v| [k.sub(/\A_/, ''), v] }]
h.inject({ }) { |x, (k,v)| x[k.sub(/\A_/, '')] = v; x }
should be okay with keys that don't have underscore prefixes without extra modifications.
you can do
hash.inject({}){|option, (k,v) | option["id"] = v if k == "_id"; option}
This should work for your case!
If we want to rename a specific key in hash then we can do it as follows:
Suppose my hash is my_hash = {'test' => 'ruby hash demo'}
Now I want to replace 'test' by 'message', then:
my_hash['message'] = my_hash.delete('test')
For Ruby 2.5 or newer with transform_keys and delete_prefix / delete_suffix methods:
hash1 = { '_id' => 'random1' }
hash2 = { 'old_first' => '123456', 'old_second' => '234567' }
hash3 = { 'first_com' => 'google.com', 'second_com' => 'amazon.com' }
hash1.transform_keys { |key| key.delete_prefix('_') }
# => {"id"=>"random1"}
hash2.transform_keys { |key| key.delete_prefix('old_') }
# => {"first"=>"123456", "second"=>"234567"}
hash3.transform_keys { |key| key.delete_suffix('_com') }
# => {"first"=>"google.com", "second"=>"amazon.com"}
h.inject({}) { |m, (k,v)| m[k.sub(/^_/,'')] = v; m }
hash.each {|k,v| hash.delete(k) && hash[k[1..-1]]=v if k[0,1] == '_'}
I went overkill and came up with the following. My motivation behind this was to append to hash keys to avoid scope conflicts when merging together/flattening hashes.
Examples
Extend Hash Class
Adds rekey method to Hash instances.
# Adds additional methods to Hash
class ::Hash
# Changes the keys on a hash
# Takes a block that passes the current key
# Whatever the block returns becomes the new key
# If a hash is returned for the key it will merge the current hash
# with the returned hash from the block. This allows for nested rekeying.
def rekey
self.each_with_object({}) do |(key, value), previous|
new_key = yield(key, value)
if new_key.is_a?(Hash)
previous.merge!(new_key)
else
previous[new_key] = value
end
end
end
end
Prepend Example
my_feelings_about_icecreams = {
vanilla: 'Delicious',
chocolate: 'Too Chocolatey',
strawberry: 'It Is Alright...'
}
my_feelings_about_icecreams.rekey { |key| "#{key}_icecream".to_sym }
# => {:vanilla_icecream=>"Delicious", :chocolate_icecream=>"Too Chocolatey", :strawberry_icecream=>"It Is Alright..."}
Trim Example
{ _id: 1, ___something_: 'what?!' }.rekey do |key|
trimmed = key.to_s.tr('_', '')
trimmed.to_sym
end
# => {:id=>1, :something=>"what?!"}
Flattening and Appending a "Scope"
If you pass a hash back to rekey it will merge the hash which allows you to flatten collections. This allows us to add scope to our keys when flattening a hash to avoid overwriting a key upon merging.
people = {
bob: {
name: 'Bob',
toys: [
{ what: 'car', color: 'red' },
{ what: 'ball', color: 'blue' }
]
},
tom: {
name: 'Tom',
toys: [
{ what: 'house', color: 'blue; da ba dee da ba die' },
{ what: 'nerf gun', color: 'metallic' }
]
}
}
people.rekey do |person, person_info|
person_info.rekey do |key|
"#{person}_#{key}".to_sym
end
end
# =>
# {
# :bob_name=>"Bob",
# :bob_toys=>[
# {:what=>"car", :color=>"red"},
# {:what=>"ball", :color=>"blue"}
# ],
# :tom_name=>"Tom",
# :tom_toys=>[
# {:what=>"house", :color=>"blue; da ba dee da ba die"},
# {:what=>"nerf gun", :color=>"metallic"}
# ]
# }
Previous answers are good enough, but they might update original data.
In case if you don't want the original data to be affected, you can try my code.
newhash=hash.reject{|k| k=='_id'}.merge({id:hash['_id']})
First it will ignore the key '_id' then merge with the updated one.
Answering exactly what was asked:
hash = {"_id"=>"4de7140772f8be03da000018"}
hash.transform_keys { |key| key[1..] }
# => {"id"=>"4de7140772f8be03da000018"}
The method transform_keys exists in the Hash class since Ruby version 2.5.
https://blog.bigbinary.com/2018/01/09/ruby-2-5-adds-hash-transform_keys-method.html
If you had a hash inside a hash, something like
hash = {
"object" => {
"_id"=>"4de7140772f8be03da000018"
}
}
and if you wanted to change "_id" to something like"token"
you can use deep_transform_keys here and do it like so
hash.deep_transform_keys do |key|
key = "token" if key == "_id"
key
end
which results in
{
"object" => {
"token"=>"4de7140772f8be03da000018"
}
}
Even if you had a symbol key hash instead to start with, something like
hash = {
object: {
id: "4de7140772f8be03da000018"
}
}
you can combine all of these concepts to convert them into a string key hash
hash.deep_transform_keys do |key|
key = "token" if key == :id
key.to_s
end
If you only want to change only one key, there is a straightforward way to do it in Ruby 2.8+ using the transform_keys method. In this example, if you want to change _id to id, then you can:
hash.transform_keys({_id: :id})
Reference: https://bugs.ruby-lang.org/issues/16274

Create array of objects from hash keys and values

I have a collection of product codes in an array: #codes. I then check to see how many instances of each product I have:
#popular = Hash.new(0)
#codes.each do |v|
#popular[v] += 1
end
This produces a hash like { code1 => 5, code2 => 12}. What I really need is a nice array of the form:
[ {:code => code1, :frequency => 5}, {:code => code2, :frequency => 12} ]
How do I build an array like that from the hashes I'm producing? Alternatively, is there a more direct route? The objects in question are ActiveModel objects with :code as an attribute. Thanks in advance!
#popular.map { |k, v| { code: k, frequency: v } }
This will produce an array of Hashes. If you need an array of models, replace the inner {...} with an appropriate constructor.
Change your code to
#codes.each_with_object([]) do
|code, a|
if h = a.find{|h| h[:code] == code}
h[:frequency] += 1
else
a.push(code: code, frequency: 0)
end
end
For speed:
#codes.group_by{|e| e}.map{|k, v| {code: k, frequency: v.length}}
Not the most efficient, but this is another way:
def counts(codes)
codes.uniq.map { |e| { code: e, frequency: codes.count(e) } }
end
codes = %w{code5 code12 code5 code3 code5 code12 code7}
#=> ["code5", "code12", "code5", "code3", "code5", "code12", "code7"]
counts(codes)
#=> [{:code=>"code5", :frequency=>3}, {:code=>"code12", :frequency=>2},
# {:code=>"code3", :frequency=>1}, {:code=>"code7" , :frequency=>1}]

Ruby: What is the easiest method to update Hash values?

Say:
h = { 1 => 10, 2 => 20, 5 => 70, 8 => 90, 4 => 34 }
I would like to change each value v to foo(v), such that h will be:
h = { 1 => foo(10), 2 => foo(20), 5 => foo(70), 8 => foo(90), 4 => foo(34) }
What is the most elegant way to achieve this ?
You can use update (alias of merge!) to update each value using a block:
hash.update(hash) { |key, value| value * 2 }
Note that we're effectively merging hash with itself. This is needed because Ruby will call the block to resolve the merge for any keys that collide, setting the value with the return value of the block.
Rails (and Ruby 2.4+ natively) have Hash#transform_values, so you can now do {a:1, b:2, c:3}.transform_values{|v| foo(v)}
https://ruby-doc.org/core-2.4.0/Hash.html#method-i-transform_values
If you need it to work in nested hashes as well, Rails now has deep_transform_values(source):
hash = { person: { name: 'Rob', age: '28' } }
hash.deep_transform_values{ |value| value.to_s.upcase }
# => {person: {name: "ROB", age: "28"}}
This will do:
h.each {|k, v| h[k] = foo(v)}
The following is slightly faster than #Dan Cheail's for large hashes, and is slightly more functional-programming style:
new_hash = Hash[old_hash.map {|key, value| key, foo(value)}]
Hash#map creates an array of key value pairs, and Hash.[] converts the array of pairs into a hash.
There's a couple of ways to do it; the most straight-forward way would be to use Hash#each to update a new hash:
new_hash = {}
old_hash.each do |key, value|
new_hash[key] = foo(value)
end

Resources